зеркало из https://github.com/mozilla/gecko-dev.git
5159 строки
189 KiB
C++
5159 строки
189 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||
/* vim: set sw=2 ts=8 et tw=80 : */
|
||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
||
#include "mozilla/AntiTrackingCommon.h"
|
||
#include "mozilla/Attributes.h"
|
||
#include "mozilla/ClearOnShutdown.h"
|
||
#include "mozilla/DebugOnly.h"
|
||
#include "mozilla/Likely.h"
|
||
#include "mozilla/Printf.h"
|
||
#include "mozilla/StorageAccess.h"
|
||
#include "mozilla/Unused.h"
|
||
|
||
#include "mozilla/net/CookieSettings.h"
|
||
#include "mozilla/net/CookieServiceChild.h"
|
||
#include "mozilla/net/NeckoCommon.h"
|
||
|
||
#include "nsCookieService.h"
|
||
#include "nsContentUtils.h"
|
||
#include "nsIClassifiedChannel.h"
|
||
#include "nsIServiceManager.h"
|
||
#include "nsIScriptSecurityManager.h"
|
||
#include "nsIWebProgressListener.h"
|
||
#include "nsIHttpChannel.h"
|
||
|
||
#include "nsIIOService.h"
|
||
#include "nsIPermissionManager.h"
|
||
#include "nsIProtocolHandler.h"
|
||
#include "nsIPrefBranch.h"
|
||
#include "nsIPrefService.h"
|
||
#include "nsIScriptError.h"
|
||
#include "nsCookiePermission.h"
|
||
#include "nsIURI.h"
|
||
#include "nsIURL.h"
|
||
#include "nsIChannel.h"
|
||
#include "nsIFile.h"
|
||
#include "nsIObserverService.h"
|
||
#include "nsILineInputStream.h"
|
||
#include "nsIEffectiveTLDService.h"
|
||
#include "nsIIDNService.h"
|
||
#include "nsIThread.h"
|
||
#include "mozIThirdPartyUtil.h"
|
||
|
||
#include "nsTArray.h"
|
||
#include "nsCOMArray.h"
|
||
#include "nsIMutableArray.h"
|
||
#include "nsArrayEnumerator.h"
|
||
#include "nsEnumeratorUtils.h"
|
||
#include "nsAutoPtr.h"
|
||
#include "nsReadableUtils.h"
|
||
#include "nsCRT.h"
|
||
#include "prprf.h"
|
||
#include "nsNetUtil.h"
|
||
#include "nsNetCID.h"
|
||
#include "nsISimpleEnumerator.h"
|
||
#include "nsIInputStream.h"
|
||
#include "nsAppDirectoryServiceDefs.h"
|
||
#include "nsNetCID.h"
|
||
#include "mozilla/storage.h"
|
||
#include "mozilla/AutoRestore.h"
|
||
#include "mozilla/FileUtils.h"
|
||
#include "mozilla/ScopeExit.h"
|
||
#include "mozilla/StaticPrefs_network.h"
|
||
#include "mozilla/StaticPrefs_privacy.h"
|
||
#include "mozilla/Telemetry.h"
|
||
#include "mozilla/TextUtils.h"
|
||
#include "nsIConsoleService.h"
|
||
#include "nsTPriorityQueue.h"
|
||
#include "nsVariant.h"
|
||
|
||
using namespace mozilla;
|
||
using namespace mozilla::net;
|
||
|
||
// Create key from baseDomain that will access the default cookie namespace.
|
||
// TODO: When we figure out what the API will look like for nsICookieManager{2}
|
||
// on content processes (see bug 777620), change to use the appropriate app
|
||
// namespace. For now those IDLs aren't supported on child processes.
|
||
#define DEFAULT_APP_KEY(baseDomain) nsCookieKey(baseDomain, OriginAttributes())
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* useful types & constants
|
||
******************************************************************************/
|
||
|
||
static StaticRefPtr<nsCookieService> gCookieService;
|
||
|
||
// XXX_hack. See bug 178993.
|
||
// This is a hack to hide HttpOnly cookies from older browsers
|
||
#define HTTP_ONLY_PREFIX "#HttpOnly_"
|
||
|
||
#define COOKIES_FILE "cookies.sqlite"
|
||
#define COOKIES_SCHEMA_VERSION 10
|
||
|
||
// parameter indexes; see |Read|
|
||
#define IDX_NAME 0
|
||
#define IDX_VALUE 1
|
||
#define IDX_HOST 2
|
||
#define IDX_PATH 3
|
||
#define IDX_EXPIRY 4
|
||
#define IDX_LAST_ACCESSED 5
|
||
#define IDX_CREATION_TIME 6
|
||
#define IDX_SECURE 7
|
||
#define IDX_HTTPONLY 8
|
||
#define IDX_BASE_DOMAIN 9
|
||
#define IDX_ORIGIN_ATTRIBUTES 10
|
||
#define IDX_SAME_SITE 11
|
||
#define IDX_RAW_SAME_SITE 12
|
||
|
||
#define TOPIC_CLEAR_ORIGIN_DATA "clear-origin-attributes-data"
|
||
|
||
static const int64_t kCookiePurgeAge =
|
||
int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds
|
||
|
||
#define OLD_COOKIE_FILE_NAME "cookies.txt"
|
||
|
||
#undef LIMIT
|
||
#define LIMIT(x, low, high, default) \
|
||
((x) >= (low) && (x) <= (high) ? (x) : (default))
|
||
|
||
#undef ADD_TEN_PERCENT
|
||
#define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
|
||
|
||
// default limits for the cookie list. these can be tuned by the
|
||
// network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively.
|
||
static const uint32_t kMaxNumberOfCookies = 3000;
|
||
static const uint32_t kMaxCookiesPerHost = 180;
|
||
static const uint32_t kCookieQuotaPerHost = 150;
|
||
static const uint32_t kMaxBytesPerCookie = 4096;
|
||
static const uint32_t kMaxBytesPerPath = 1024;
|
||
|
||
// pref string constants
|
||
static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber";
|
||
static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost";
|
||
static const char kPrefCookieQuotaPerHost[] = "network.cookie.quotaPerHost";
|
||
static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge";
|
||
|
||
static void bindCookieParameters(mozIStorageBindingParamsArray* aParamsArray,
|
||
const nsCookieKey& aKey,
|
||
const nsCookie* aCookie);
|
||
|
||
// stores the nsCookieEntry entryclass and an index into the cookie array
|
||
// within that entryclass, for purposes of storing an iteration state that
|
||
// points to a certain cookie.
|
||
struct nsListIter {
|
||
// default (non-initializing) constructor.
|
||
nsListIter() = default;
|
||
|
||
// explicit constructor to a given iterator state with entryclass 'aEntry'
|
||
// and index 'aIndex'.
|
||
explicit nsListIter(nsCookieEntry* aEntry, nsCookieEntry::IndexType aIndex)
|
||
: entry(aEntry), index(aIndex) {}
|
||
|
||
// get the nsCookie * the iterator currently points to.
|
||
nsCookie* Cookie() const { return entry->GetCookies()[index]; }
|
||
|
||
nsCookieEntry* entry;
|
||
nsCookieEntry::IndexType index;
|
||
};
|
||
|
||
/******************************************************************************
|
||
* Cookie logging handlers
|
||
* used for logging in nsCookieService
|
||
******************************************************************************/
|
||
|
||
// logging handlers
|
||
#ifdef MOZ_LOGGING
|
||
// in order to do logging, the following environment variables need to be set:
|
||
//
|
||
// set MOZ_LOG=cookie:3 -- shows rejected cookies
|
||
// set MOZ_LOG=cookie:4 -- shows accepted and rejected cookies
|
||
// set MOZ_LOG_FILE=cookie.log
|
||
//
|
||
# include "mozilla/Logging.h"
|
||
#endif
|
||
|
||
// define logging macros for convenience
|
||
#define SET_COOKIE true
|
||
#define GET_COOKIE false
|
||
|
||
static LazyLogModule gCookieLog("cookie");
|
||
|
||
#define COOKIE_LOGFAILURE(a, b, c, d) LogFailure(a, b, c, d)
|
||
#define COOKIE_LOGSUCCESS(a, b, c, d, e) LogSuccess(a, b, c, d, e)
|
||
|
||
#define COOKIE_LOGEVICTED(a, details) \
|
||
PR_BEGIN_MACRO \
|
||
if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) LogEvicted(a, details); \
|
||
PR_END_MACRO
|
||
|
||
#define COOKIE_LOGSTRING(lvl, fmt) \
|
||
PR_BEGIN_MACRO \
|
||
MOZ_LOG(gCookieLog, lvl, fmt); \
|
||
MOZ_LOG(gCookieLog, lvl, ("\n")); \
|
||
PR_END_MACRO
|
||
|
||
static void LogFailure(bool aSetCookie, nsIURI* aHostURI,
|
||
const nsACString& aCookieString, const char* aReason) {
|
||
// if logging isn't enabled, return now to save cycles
|
||
if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) return;
|
||
|
||
nsAutoCString spec;
|
||
if (aHostURI) aHostURI->GetAsciiSpec(spec);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning,
|
||
("===== %s =====\n",
|
||
aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT"));
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning, ("request URL: %s\n", spec.get()));
|
||
if (aSetCookie)
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning,
|
||
("cookie string: %s\n", aCookieString.BeginReading()));
|
||
|
||
PRExplodedTime explodedTime;
|
||
PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
|
||
char timeString[40];
|
||
PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning, ("current time: %s", timeString));
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning, ("rejected because %s\n", aReason));
|
||
MOZ_LOG(gCookieLog, LogLevel::Warning, ("\n"));
|
||
}
|
||
|
||
static void LogCookie(nsCookie* aCookie) {
|
||
PRExplodedTime explodedTime;
|
||
PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
|
||
char timeString[40];
|
||
PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("current time: %s", timeString));
|
||
|
||
if (aCookie) {
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("----------------\n"));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("name: %s\n", aCookie->Name().get()));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("value: %s\n", aCookie->Value().get()));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("%s: %s\n", aCookie->IsDomain() ? "domain" : "host",
|
||
aCookie->Host().get()));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("path: %s\n", aCookie->Path().get()));
|
||
|
||
PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC),
|
||
PR_GMTParameters, &explodedTime);
|
||
PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("expires: %s%s", timeString,
|
||
aCookie->IsSession() ? " (at end of session)" : ""));
|
||
|
||
PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime);
|
||
PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("created: %s", timeString));
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("is secure: %s\n", aCookie->IsSecure() ? "true" : "false"));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false"));
|
||
|
||
nsAutoCString suffix;
|
||
aCookie->OriginAttributesRef().CreateSuffix(suffix);
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("origin attributes: %s\n",
|
||
suffix.IsEmpty() ? "{empty}" : suffix.get()));
|
||
}
|
||
}
|
||
|
||
static void LogSuccess(bool aSetCookie, nsIURI* aHostURI,
|
||
const nsACString& aCookieString, nsCookie* aCookie,
|
||
bool aReplacing) {
|
||
// if logging isn't enabled, return now to save cycles
|
||
if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) {
|
||
return;
|
||
}
|
||
|
||
nsAutoCString spec;
|
||
if (aHostURI) aHostURI->GetAsciiSpec(spec);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT"));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("request URL: %s\n", spec.get()));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("cookie string: %s\n", aCookieString.BeginReading()));
|
||
if (aSetCookie)
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug,
|
||
("replaces existing cookie: %s\n", aReplacing ? "true" : "false"));
|
||
|
||
LogCookie(aCookie);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
|
||
}
|
||
|
||
static void LogEvicted(nsCookie* aCookie, const char* details) {
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("===== COOKIE EVICTED =====\n"));
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("%s\n", details));
|
||
|
||
LogCookie(aCookie);
|
||
|
||
MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
|
||
}
|
||
|
||
#ifdef DEBUG
|
||
# define NS_ASSERT_SUCCESS(res) \
|
||
PR_BEGIN_MACRO \
|
||
nsresult __rv = res; /* Do not evaluate |res| more than once! */ \
|
||
if (NS_FAILED(__rv)) { \
|
||
SmprintfPointer msg = mozilla::Smprintf( \
|
||
"NS_ASSERT_SUCCESS(%s) failed with result 0x%" PRIX32, #res, \
|
||
static_cast<uint32_t>(__rv)); \
|
||
NS_ASSERTION(NS_SUCCEEDED(__rv), msg.get()); \
|
||
} \
|
||
PR_END_MACRO
|
||
#else
|
||
# define NS_ASSERT_SUCCESS(res) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO
|
||
#endif
|
||
|
||
/******************************************************************************
|
||
* DBListenerErrorHandler impl:
|
||
* Parent class for our async storage listeners that handles the logging of
|
||
* errors.
|
||
******************************************************************************/
|
||
class DBListenerErrorHandler : public mozIStorageStatementCallback {
|
||
protected:
|
||
explicit DBListenerErrorHandler(DBState* dbState) : mDBState(dbState) {}
|
||
RefPtr<DBState> mDBState;
|
||
virtual const char* GetOpType() = 0;
|
||
|
||
public:
|
||
NS_IMETHOD HandleError(mozIStorageError* aError) override {
|
||
if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) {
|
||
int32_t result = -1;
|
||
aError->GetResult(&result);
|
||
|
||
nsAutoCString message;
|
||
aError->GetMessage(message);
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("DBListenerErrorHandler::HandleError(): Error %d occurred while "
|
||
"performing operation '%s' with message '%s'; rebuilding database.",
|
||
result, GetOpType(), message.get()));
|
||
}
|
||
|
||
// Rebuild the database.
|
||
gCookieService->HandleCorruptDB(mDBState);
|
||
|
||
return NS_OK;
|
||
}
|
||
};
|
||
|
||
/******************************************************************************
|
||
* InsertCookieDBListener impl:
|
||
* mozIStorageStatementCallback used to track asynchronous insertion operations.
|
||
******************************************************************************/
|
||
class InsertCookieDBListener final : public DBListenerErrorHandler {
|
||
private:
|
||
const char* GetOpType() override { return "INSERT"; }
|
||
|
||
~InsertCookieDBListener() = default;
|
||
|
||
public:
|
||
NS_DECL_ISUPPORTS
|
||
|
||
explicit InsertCookieDBListener(DBState* dbState)
|
||
: DBListenerErrorHandler(dbState) {}
|
||
NS_IMETHOD HandleResult(mozIStorageResultSet*) override {
|
||
MOZ_ASSERT_UNREACHABLE(
|
||
"Unexpected call to "
|
||
"InsertCookieDBListener::HandleResult");
|
||
return NS_OK;
|
||
}
|
||
NS_IMETHOD HandleCompletion(uint16_t aReason) override {
|
||
// If we were rebuilding the db and we succeeded, make our corruptFlag say
|
||
// so.
|
||
if (mDBState->corruptFlag == DBState::REBUILDING &&
|
||
aReason == mozIStorageStatementCallback::REASON_FINISHED) {
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Debug,
|
||
("InsertCookieDBListener::HandleCompletion(): rebuild complete"));
|
||
mDBState->corruptFlag = DBState::OK;
|
||
}
|
||
|
||
// This notification is just for testing.
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-saved-on-disk", nullptr);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback)
|
||
|
||
/******************************************************************************
|
||
* UpdateCookieDBListener impl:
|
||
* mozIStorageStatementCallback used to track asynchronous update operations.
|
||
******************************************************************************/
|
||
class UpdateCookieDBListener final : public DBListenerErrorHandler {
|
||
private:
|
||
const char* GetOpType() override { return "UPDATE"; }
|
||
|
||
~UpdateCookieDBListener() = default;
|
||
|
||
public:
|
||
NS_DECL_ISUPPORTS
|
||
|
||
explicit UpdateCookieDBListener(DBState* dbState)
|
||
: DBListenerErrorHandler(dbState) {}
|
||
NS_IMETHOD HandleResult(mozIStorageResultSet*) override {
|
||
MOZ_ASSERT_UNREACHABLE(
|
||
"Unexpected call to "
|
||
"UpdateCookieDBListener::HandleResult");
|
||
return NS_OK;
|
||
}
|
||
NS_IMETHOD HandleCompletion(uint16_t aReason) override { return NS_OK; }
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback)
|
||
|
||
/******************************************************************************
|
||
* RemoveCookieDBListener impl:
|
||
* mozIStorageStatementCallback used to track asynchronous removal operations.
|
||
******************************************************************************/
|
||
class RemoveCookieDBListener final : public DBListenerErrorHandler {
|
||
private:
|
||
const char* GetOpType() override { return "REMOVE"; }
|
||
|
||
~RemoveCookieDBListener() = default;
|
||
|
||
public:
|
||
NS_DECL_ISUPPORTS
|
||
|
||
explicit RemoveCookieDBListener(DBState* dbState)
|
||
: DBListenerErrorHandler(dbState) {}
|
||
NS_IMETHOD HandleResult(mozIStorageResultSet*) override {
|
||
MOZ_ASSERT_UNREACHABLE(
|
||
"Unexpected call to "
|
||
"RemoveCookieDBListener::HandleResult");
|
||
return NS_OK;
|
||
}
|
||
NS_IMETHOD HandleCompletion(uint16_t aReason) override { return NS_OK; }
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback)
|
||
|
||
/******************************************************************************
|
||
* CloseCookieDBListener imp:
|
||
* Static mozIStorageCompletionCallback used to notify when the database is
|
||
* successfully closed.
|
||
******************************************************************************/
|
||
class CloseCookieDBListener final : public mozIStorageCompletionCallback {
|
||
~CloseCookieDBListener() = default;
|
||
|
||
public:
|
||
explicit CloseCookieDBListener(DBState* dbState) : mDBState(dbState) {}
|
||
RefPtr<DBState> mDBState;
|
||
NS_DECL_ISUPPORTS
|
||
|
||
NS_IMETHOD Complete(nsresult, nsISupports*) override {
|
||
gCookieService->HandleDBClosed(mDBState);
|
||
return NS_OK;
|
||
}
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback)
|
||
|
||
namespace {
|
||
|
||
class AppClearDataObserver final : public nsIObserver {
|
||
~AppClearDataObserver() = default;
|
||
|
||
public:
|
||
NS_DECL_ISUPPORTS
|
||
|
||
// nsIObserver implementation.
|
||
NS_IMETHOD
|
||
Observe(nsISupports* aSubject, const char* aTopic,
|
||
const char16_t* aData) override {
|
||
MOZ_ASSERT(!nsCRT::strcmp(aTopic, TOPIC_CLEAR_ORIGIN_DATA));
|
||
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
|
||
nsCOMPtr<nsICookieManager> cookieManager =
|
||
do_GetService(NS_COOKIEMANAGER_CONTRACTID);
|
||
MOZ_ASSERT(cookieManager);
|
||
|
||
return cookieManager->RemoveCookiesWithOriginAttributes(
|
||
nsDependentString(aData), EmptyCString());
|
||
}
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(AppClearDataObserver, nsIObserver)
|
||
|
||
// comparator class for sorting cookies by entry and index.
|
||
class CompareCookiesByIndex {
|
||
public:
|
||
bool Equals(const nsListIter& a, const nsListIter& b) const {
|
||
NS_ASSERTION(a.entry != b.entry || a.index != b.index,
|
||
"cookie indexes should never be equal");
|
||
return false;
|
||
}
|
||
|
||
bool LessThan(const nsListIter& a, const nsListIter& b) const {
|
||
// compare by entryclass pointer, then by index.
|
||
if (a.entry != b.entry) return a.entry < b.entry;
|
||
|
||
return a.index < b.index;
|
||
}
|
||
};
|
||
|
||
} // namespace
|
||
|
||
size_t nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
|
||
size_t amount = nsCookieKey::SizeOfExcludingThis(aMallocSizeOf);
|
||
|
||
amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
|
||
for (uint32_t i = 0; i < mCookies.Length(); ++i) {
|
||
amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
|
||
}
|
||
|
||
return amount;
|
||
}
|
||
|
||
size_t DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
|
||
size_t amount = 0;
|
||
|
||
amount += aMallocSizeOf(this);
|
||
amount += hostTable.SizeOfExcludingThis(aMallocSizeOf);
|
||
|
||
return amount;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* singleton instance ctor/dtor methods
|
||
******************************************************************************/
|
||
|
||
already_AddRefed<nsICookieService> nsCookieService::GetXPCOMSingleton() {
|
||
if (IsNeckoChild()) return CookieServiceChild::GetSingleton();
|
||
|
||
return GetSingleton();
|
||
}
|
||
|
||
already_AddRefed<nsCookieService> nsCookieService::GetSingleton() {
|
||
NS_ASSERTION(!IsNeckoChild(), "not a parent process");
|
||
|
||
if (gCookieService) {
|
||
return do_AddRef(gCookieService);
|
||
}
|
||
|
||
// Create a new singleton nsCookieService.
|
||
// We AddRef only once since XPCOM has rules about the ordering of module
|
||
// teardowns - by the time our module destructor is called, it's too late to
|
||
// Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC
|
||
// cycles have already been completed and would result in serious leaks.
|
||
// See bug 209571.
|
||
gCookieService = new nsCookieService();
|
||
if (gCookieService) {
|
||
if (NS_SUCCEEDED(gCookieService->Init())) {
|
||
ClearOnShutdown(&gCookieService);
|
||
} else {
|
||
gCookieService = nullptr;
|
||
}
|
||
}
|
||
|
||
return do_AddRef(gCookieService);
|
||
}
|
||
|
||
/* static */
|
||
void nsCookieService::AppClearDataObserverInit() {
|
||
nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
|
||
nsCOMPtr<nsIObserver> obs = new AppClearDataObserver();
|
||
observerService->AddObserver(obs, TOPIC_CLEAR_ORIGIN_DATA,
|
||
/* ownsWeak= */ false);
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* public methods
|
||
******************************************************************************/
|
||
|
||
NS_IMPL_ISUPPORTS(nsCookieService, nsICookieService, nsICookieManager,
|
||
nsIObserver, nsISupportsWeakReference, nsIMemoryReporter)
|
||
|
||
nsCookieService::nsCookieService()
|
||
: mDBState(nullptr),
|
||
mMaxNumberOfCookies(kMaxNumberOfCookies),
|
||
mMaxCookiesPerHost(kMaxCookiesPerHost),
|
||
mCookieQuotaPerHost(kCookieQuotaPerHost),
|
||
mCookiePurgeAge(kCookiePurgeAge),
|
||
mThread(nullptr),
|
||
mMonitor("CookieThread"),
|
||
mInitializedDBStates(false),
|
||
mInitializedDBConn(false) {}
|
||
|
||
nsresult nsCookieService::Init() {
|
||
nsresult rv;
|
||
mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// init our pref and observer
|
||
nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
|
||
if (prefBranch) {
|
||
prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
|
||
prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
|
||
prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
|
||
PrefChanged(prefBranch);
|
||
}
|
||
|
||
mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Init our default, and possibly private DBStates.
|
||
InitDBStates();
|
||
|
||
RegisterWeakMemoryReporter(this);
|
||
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
NS_ENSURE_STATE(os);
|
||
os->AddObserver(this, "profile-before-change", true);
|
||
os->AddObserver(this, "profile-do-change", true);
|
||
os->AddObserver(this, "last-pb-context-exited", true);
|
||
|
||
mPermissionService = nsCookiePermission::GetOrCreate();
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
void nsCookieService::InitDBStates() {
|
||
NS_ASSERTION(!mDBState, "already have a DBState");
|
||
NS_ASSERTION(!mDefaultDBState, "already have a default DBState");
|
||
NS_ASSERTION(!mPrivateDBState, "already have a private DBState");
|
||
NS_ASSERTION(!mInitializedDBStates, "already initialized");
|
||
NS_ASSERTION(!mThread, "already have a cookie thread");
|
||
|
||
// Create a new default DBState and set our current one.
|
||
mDefaultDBState = new DBState();
|
||
mDBState = mDefaultDBState;
|
||
|
||
mPrivateDBState = new DBState();
|
||
|
||
// Get our cookie file.
|
||
nsresult rv = NS_GetSpecialDirectory(
|
||
NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mDefaultDBState->cookieFile));
|
||
if (NS_FAILED(rv)) {
|
||
// We've already set up our DBStates appropriately; nothing more to do.
|
||
COOKIE_LOGSTRING(LogLevel::Warning,
|
||
("InitDBStates(): couldn't get cookie file"));
|
||
|
||
mInitializedDBConn = true;
|
||
mInitializedDBStates = true;
|
||
return;
|
||
}
|
||
mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE));
|
||
|
||
NS_ENSURE_SUCCESS_VOID(NS_NewNamedThread("Cookie", getter_AddRefs(mThread)));
|
||
|
||
nsCOMPtr<nsIRunnable> runnable =
|
||
NS_NewRunnableFunction("InitDBStates.TryInitDB", [] {
|
||
NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDBState &&
|
||
gCookieService->mDefaultDBState);
|
||
|
||
MonitorAutoLock lock(gCookieService->mMonitor);
|
||
|
||
// Attempt to open and read the database. If TryInitDB() returns
|
||
// RESULT_RETRY, do so.
|
||
OpenDBResult result = gCookieService->TryInitDB(false);
|
||
if (result == RESULT_RETRY) {
|
||
// Database may be corrupt. Synchronously close the connection, clean
|
||
// up the default DBState, and try again.
|
||
COOKIE_LOGSTRING(LogLevel::Warning,
|
||
("InitDBStates(): retrying TryInitDB()"));
|
||
gCookieService->CleanupCachedStatements();
|
||
gCookieService->CleanupDefaultDBConnection();
|
||
result = gCookieService->TryInitDB(true);
|
||
if (result == RESULT_RETRY) {
|
||
// We're done. Change the code to failure so we clean up below.
|
||
result = RESULT_FAILURE;
|
||
}
|
||
}
|
||
|
||
if (result == RESULT_FAILURE) {
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("InitDBStates(): TryInitDB() failed, closing connection"));
|
||
|
||
// Connection failure is unrecoverable. Clean up our connection. We
|
||
// can run fine without persistent storage -- e.g. if there's no
|
||
// profile.
|
||
gCookieService->CleanupCachedStatements();
|
||
gCookieService->CleanupDefaultDBConnection();
|
||
|
||
// No need to initialize dbConn
|
||
gCookieService->mInitializedDBConn = true;
|
||
}
|
||
|
||
gCookieService->mInitializedDBStates = true;
|
||
|
||
NS_DispatchToMainThread(
|
||
NS_NewRunnableFunction("TryInitDB.InitDBConn", [] {
|
||
NS_ENSURE_TRUE_VOID(gCookieService);
|
||
gCookieService->InitDBConn();
|
||
}));
|
||
gCookieService->mMonitor.Notify();
|
||
});
|
||
|
||
mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
|
||
}
|
||
|
||
namespace {
|
||
|
||
class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction {
|
||
~ConvertAppIdToOriginAttrsSQLFunction() = default;
|
||
|
||
NS_DECL_ISUPPORTS
|
||
NS_DECL_MOZISTORAGEFUNCTION
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction);
|
||
|
||
NS_IMETHODIMP
|
||
ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall(
|
||
mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
|
||
nsresult rv;
|
||
int32_t inIsolatedMozBrowser;
|
||
|
||
rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Create an originAttributes object by inIsolatedMozBrowser.
|
||
// Then create the originSuffix string from this object.
|
||
OriginAttributes attrs(inIsolatedMozBrowser ? true : false);
|
||
nsAutoCString suffix;
|
||
attrs.CreateSuffix(suffix);
|
||
|
||
RefPtr<nsVariant> outVar(new nsVariant());
|
||
rv = outVar->SetAsAUTF8String(suffix);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
outVar.forget(aResult);
|
||
return NS_OK;
|
||
}
|
||
|
||
class SetAppIdFromOriginAttributesSQLFunction final
|
||
: public mozIStorageFunction {
|
||
~SetAppIdFromOriginAttributesSQLFunction() = default;
|
||
|
||
NS_DECL_ISUPPORTS
|
||
NS_DECL_MOZISTORAGEFUNCTION
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction);
|
||
|
||
NS_IMETHODIMP
|
||
SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall(
|
||
mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
|
||
nsresult rv;
|
||
nsAutoCString suffix;
|
||
OriginAttributes attrs;
|
||
|
||
rv = aFunctionArguments->GetUTF8String(0, suffix);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
bool success = attrs.PopulateFromSuffix(suffix);
|
||
NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
|
||
|
||
RefPtr<nsVariant> outVar(new nsVariant());
|
||
rv = outVar->SetAsInt32(0); // deprecated appId!
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
outVar.forget(aResult);
|
||
return NS_OK;
|
||
}
|
||
|
||
class SetInBrowserFromOriginAttributesSQLFunction final
|
||
: public mozIStorageFunction {
|
||
~SetInBrowserFromOriginAttributesSQLFunction() = default;
|
||
|
||
NS_DECL_ISUPPORTS
|
||
NS_DECL_MOZISTORAGEFUNCTION
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction,
|
||
mozIStorageFunction);
|
||
|
||
NS_IMETHODIMP
|
||
SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall(
|
||
mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
|
||
nsresult rv;
|
||
nsAutoCString suffix;
|
||
OriginAttributes attrs;
|
||
|
||
rv = aFunctionArguments->GetUTF8String(0, suffix);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
bool success = attrs.PopulateFromSuffix(suffix);
|
||
NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
|
||
|
||
RefPtr<nsVariant> outVar(new nsVariant());
|
||
rv = outVar->SetAsInt32(attrs.mInIsolatedMozBrowser);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
outVar.forget(aResult);
|
||
return NS_OK;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
/* Attempt to open and read the database. If 'aRecreateDB' is true, try to
|
||
* move the existing database file out of the way and create a new one.
|
||
*
|
||
* @returns RESULT_OK if opening or creating the database succeeded;
|
||
* RESULT_RETRY if the database cannot be opened, is corrupt, or some
|
||
* other failure occurred that might be resolved by recreating the
|
||
* database; or RESULT_FAILED if there was an unrecoverable error and
|
||
* we must run without a database.
|
||
*
|
||
* If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform
|
||
* cleanup of the default DBState.
|
||
*/
|
||
OpenDBResult nsCookieService::TryInitDB(bool aRecreateDB) {
|
||
NS_ASSERTION(!mDefaultDBState->dbConn, "nonnull dbConn");
|
||
NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert");
|
||
NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener");
|
||
NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn");
|
||
NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread");
|
||
|
||
// Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't
|
||
// want to delete it outright, since it may be useful for debugging purposes,
|
||
// so we move it out of the way.
|
||
nsresult rv;
|
||
if (aRecreateDB) {
|
||
nsCOMPtr<nsIFile> backupFile;
|
||
mDefaultDBState->cookieFile->Clone(getter_AddRefs(backupFile));
|
||
rv = backupFile->MoveToNative(nullptr,
|
||
NS_LITERAL_CSTRING(COOKIES_FILE ".bak"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_FAILURE);
|
||
}
|
||
|
||
// This block provides scope for the Telemetry AutoTimer
|
||
{
|
||
Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS>
|
||
telemetry;
|
||
ReadAheadFile(mDefaultDBState->cookieFile);
|
||
|
||
// open a connection to the cookie database, and only cache our connection
|
||
// and statements upon success. The connection is opened unshared to
|
||
// eliminate cache contention between the main and background threads.
|
||
rv = mStorageService->OpenUnsharedDatabase(
|
||
mDefaultDBState->cookieFile, getter_AddRefs(mDefaultDBState->syncConn));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
|
||
auto guard = MakeScopeExit([&] { mDefaultDBState->syncConn = nullptr; });
|
||
|
||
bool tableExists = false;
|
||
mDefaultDBState->syncConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"),
|
||
&tableExists);
|
||
if (!tableExists) {
|
||
rv = CreateTable();
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
} else {
|
||
// table already exists; check the schema version before reading
|
||
int32_t dbSchemaVersion;
|
||
rv = mDefaultDBState->syncConn->GetSchemaVersion(&dbSchemaVersion);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Start a transaction for the whole migration block.
|
||
mozStorageTransaction transaction(mDefaultDBState->syncConn, true);
|
||
|
||
switch (dbSchemaVersion) {
|
||
// Upgrading.
|
||
// Every time you increment the database schema, you need to implement
|
||
// the upgrading code from the previous version to the new one. If
|
||
// migration fails for any reason, it's a bug -- so we return RESULT_RETRY
|
||
// such that the original database will be saved, in the hopes that we
|
||
// might one day see it and fix it.
|
||
case 1: {
|
||
// Add the lastAccessed column to the table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies ADD lastAccessed INTEGER"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
// Fall through to the next upgrade.
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 2: {
|
||
// Add the baseDomain column and index to the table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("ALTER TABLE moz_cookies ADD baseDomain TEXT"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Compute the baseDomains for the table. This must be done eagerly
|
||
// otherwise we won't be able to synchronously read in individual
|
||
// domains on demand.
|
||
const int64_t SCHEMA2_IDX_ID = 0;
|
||
const int64_t SCHEMA2_IDX_HOST = 1;
|
||
nsCOMPtr<mozIStorageStatement> select;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING("SELECT id, host FROM moz_cookies"),
|
||
getter_AddRefs(select));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
nsCOMPtr<mozIStorageStatement> update;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING("UPDATE moz_cookies SET baseDomain = "
|
||
":baseDomain WHERE id = :id"),
|
||
getter_AddRefs(update));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
nsCString baseDomain, host;
|
||
bool hasResult;
|
||
while (true) {
|
||
rv = select->ExecuteStep(&hasResult);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
if (!hasResult) break;
|
||
|
||
int64_t id = select->AsInt64(SCHEMA2_IDX_ID);
|
||
select->GetUTF8String(SCHEMA2_IDX_HOST, host);
|
||
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
mozStorageStatementScoper scoper(update);
|
||
|
||
rv = update->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
|
||
baseDomain);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
rv = update->BindInt64ByName(NS_LITERAL_CSTRING("id"), id);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = update->ExecuteStep(&hasResult);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
|
||
// Create an index on baseDomain.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
// Fall through to the next upgrade.
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 3: {
|
||
// Add the creationTime column to the table, and create a unique index
|
||
// on (name, host, path). Before we do this, we have to purge the table
|
||
// of expired cookies such that we know that the (name, host, path)
|
||
// index is truly unique -- otherwise we can't create the index. Note
|
||
// that we can't just execute a statement to delete all rows where the
|
||
// expiry column is in the past -- doing so would rely on the clock
|
||
// (both now and when previous cookies were set) being monotonic.
|
||
|
||
// Select the whole table, and order by the fields we're interested in.
|
||
// This means we can simply do a linear traversal of the results and
|
||
// check for duplicates as we go.
|
||
const int64_t SCHEMA3_IDX_ID = 0;
|
||
const int64_t SCHEMA3_IDX_NAME = 1;
|
||
const int64_t SCHEMA3_IDX_HOST = 2;
|
||
const int64_t SCHEMA3_IDX_PATH = 3;
|
||
nsCOMPtr<mozIStorageStatement> select;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING(
|
||
"SELECT id, name, host, path FROM moz_cookies "
|
||
"ORDER BY name ASC, host ASC, path ASC, expiry ASC"),
|
||
getter_AddRefs(select));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
nsCOMPtr<mozIStorageStatement> deleteExpired;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING("DELETE FROM moz_cookies WHERE id = :id"),
|
||
getter_AddRefs(deleteExpired));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Read the first row.
|
||
bool hasResult;
|
||
rv = select->ExecuteStep(&hasResult);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
if (hasResult) {
|
||
nsCString name1, host1, path1;
|
||
int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID);
|
||
select->GetUTF8String(SCHEMA3_IDX_NAME, name1);
|
||
select->GetUTF8String(SCHEMA3_IDX_HOST, host1);
|
||
select->GetUTF8String(SCHEMA3_IDX_PATH, path1);
|
||
|
||
nsCString name2, host2, path2;
|
||
while (true) {
|
||
// Read the second row.
|
||
rv = select->ExecuteStep(&hasResult);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
if (!hasResult) break;
|
||
|
||
int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID);
|
||
select->GetUTF8String(SCHEMA3_IDX_NAME, name2);
|
||
select->GetUTF8String(SCHEMA3_IDX_HOST, host2);
|
||
select->GetUTF8String(SCHEMA3_IDX_PATH, path2);
|
||
|
||
// If the two rows match in (name, host, path), we know the earlier
|
||
// row has an earlier expiry time. Delete it.
|
||
if (name1 == name2 && host1 == host2 && path1 == path2) {
|
||
mozStorageStatementScoper scoper(deleteExpired);
|
||
|
||
rv =
|
||
deleteExpired->BindInt64ByName(NS_LITERAL_CSTRING("id"), id1);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = deleteExpired->ExecuteStep(&hasResult);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
|
||
// Make the second row the first for the next iteration.
|
||
name1 = name2;
|
||
host1 = host2;
|
||
path1 = path2;
|
||
id1 = id2;
|
||
}
|
||
}
|
||
|
||
// Add the creationTime column to the table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies ADD creationTime INTEGER"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Copy the id of each row into the new creationTime column.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("UPDATE moz_cookies SET creationTime = "
|
||
"(SELECT id WHERE id = moz_cookies.id)"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Create a unique index on (name, host, path) to allow fast lookup.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("CREATE UNIQUE INDEX moz_uniqueid "
|
||
"ON moz_cookies (name, host, path)"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
// Fall through to the next upgrade.
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 4: {
|
||
// We need to add appId/inBrowserElement, plus change a constraint on
|
||
// the table (unique entries now include appId/inBrowserElement):
|
||
// this requires creating a new table and copying the data to it. We
|
||
// then rename the new table to the old name.
|
||
//
|
||
// Why we made this change: appId/inBrowserElement allow "cookie jars"
|
||
// for Firefox OS. We create a separate cookie namespace per {appId,
|
||
// inBrowserElement}. When upgrading, we convert existing cookies
|
||
// (which imply we're on desktop/mobile) to use {0, false}, as that is
|
||
// the only namespace used by a non-Firefox-OS implementation.
|
||
|
||
// Rename existing table
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Drop existing index (CreateTable will create new one for new table)
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Create new table (with new fields and new unique constraint)
|
||
rv = CreateTableForSchemaVersion5();
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Copy data from old table, using appId/inBrowser=0 for existing rows
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"INSERT INTO moz_cookies "
|
||
"(baseDomain, appId, inBrowserElement, name, value, host, path, "
|
||
"expiry,"
|
||
" lastAccessed, creationTime, isSecure, isHttpOnly) "
|
||
"SELECT baseDomain, 0, 0, name, value, host, path, expiry,"
|
||
" lastAccessed, creationTime, isSecure, isHttpOnly "
|
||
"FROM moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Drop old table
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP TABLE moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 5"));
|
||
}
|
||
// Fall through to the next upgrade.
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 5: {
|
||
// Change in the version: Replace the columns |appId| and
|
||
// |inBrowserElement| by a single column |originAttributes|.
|
||
//
|
||
// Why we made this change: FxOS new security model (NSec) encapsulates
|
||
// "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to
|
||
// make it easier to modify the contents of this structure in the
|
||
// future.
|
||
//
|
||
// We do the migration in several steps:
|
||
// 1. Rename the old table.
|
||
// 2. Create a new table.
|
||
// 3. Copy data from the old table to the new table; convert appId and
|
||
// inBrowserElement to originAttributes in the meantime.
|
||
|
||
// Rename existing table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Drop existing index (CreateTable will create new one for new table).
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Create new table with new fields and new unique constraint.
|
||
rv = CreateTableForSchemaVersion6();
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Copy data from old table without the two deprecated columns appId and
|
||
// inBrowserElement.
|
||
nsCOMPtr<mozIStorageFunction> convertToOriginAttrs(
|
||
new ConvertAppIdToOriginAttrsSQLFunction());
|
||
NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY);
|
||
|
||
NS_NAMED_LITERAL_CSTRING(convertToOriginAttrsName,
|
||
"CONVERT_TO_ORIGIN_ATTRIBUTES");
|
||
|
||
rv = mDefaultDBState->syncConn->CreateFunction(convertToOriginAttrsName,
|
||
2, convertToOriginAttrs);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"INSERT INTO moz_cookies "
|
||
"(baseDomain, originAttributes, name, value, host, path, expiry,"
|
||
" lastAccessed, creationTime, isSecure, isHttpOnly) "
|
||
"SELECT baseDomain, "
|
||
" CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement),"
|
||
" name, value, host, path, expiry, lastAccessed, creationTime, "
|
||
" isSecure, isHttpOnly "
|
||
"FROM moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv =
|
||
mDefaultDBState->syncConn->RemoveFunction(convertToOriginAttrsName);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Drop old table
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP TABLE moz_cookies_old"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 6"));
|
||
}
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 6: {
|
||
// We made a mistake in schema version 6. We cannot remove expected
|
||
// columns of any version (checked in the default case) from cookie
|
||
// database, because doing this would destroy the possibility of
|
||
// downgrading database.
|
||
//
|
||
// This version simply restores appId and inBrowserElement columns in
|
||
// order to fix downgrading issue even though these two columns are no
|
||
// longer used in the latest schema.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Compute and populate the values of appId and inBrwoserElement from
|
||
// originAttributes.
|
||
nsCOMPtr<mozIStorageFunction> setAppId(
|
||
new SetAppIdFromOriginAttributesSQLFunction());
|
||
NS_ENSURE_TRUE(setAppId, RESULT_RETRY);
|
||
|
||
NS_NAMED_LITERAL_CSTRING(setAppIdName, "SET_APP_ID");
|
||
|
||
rv = mDefaultDBState->syncConn->CreateFunction(setAppIdName, 1,
|
||
setAppId);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
nsCOMPtr<mozIStorageFunction> setInBrowser(
|
||
new SetInBrowserFromOriginAttributesSQLFunction());
|
||
NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY);
|
||
|
||
NS_NAMED_LITERAL_CSTRING(setInBrowserName, "SET_IN_BROWSER");
|
||
|
||
rv = mDefaultDBState->syncConn->CreateFunction(setInBrowserName, 1,
|
||
setInBrowser);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), "
|
||
"inBrowserElement = SET_IN_BROWSER(originAttributes);"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = mDefaultDBState->syncConn->RemoveFunction(setAppIdName);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = mDefaultDBState->syncConn->RemoveFunction(setInBrowserName);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 7"));
|
||
}
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 7: {
|
||
// Remove the appId field from moz_cookies.
|
||
//
|
||
// Unfortunately sqlite doesn't support dropping columns using ALTER
|
||
// TABLE, so we need to go through the procedure documented in
|
||
// https://www.sqlite.org/lang_altertable.html.
|
||
|
||
// Drop existing index
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Create a new_moz_cookies table without the appId field.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("CREATE TABLE new_moz_cookies("
|
||
"id INTEGER PRIMARY KEY, "
|
||
"baseDomain TEXT, "
|
||
"originAttributes TEXT NOT NULL DEFAULT '', "
|
||
"name TEXT, "
|
||
"value TEXT, "
|
||
"host TEXT, "
|
||
"path TEXT, "
|
||
"expiry INTEGER, "
|
||
"lastAccessed INTEGER, "
|
||
"creationTime INTEGER, "
|
||
"isSecure INTEGER, "
|
||
"isHttpOnly INTEGER, "
|
||
"inBrowserElement INTEGER DEFAULT 0, "
|
||
"CONSTRAINT moz_uniqueid UNIQUE (name, host, "
|
||
"path, originAttributes)"
|
||
")"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Move the data over.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("INSERT INTO new_moz_cookies ("
|
||
"id, "
|
||
"baseDomain, "
|
||
"originAttributes, "
|
||
"name, "
|
||
"value, "
|
||
"host, "
|
||
"path, "
|
||
"expiry, "
|
||
"lastAccessed, "
|
||
"creationTime, "
|
||
"isSecure, "
|
||
"isHttpOnly, "
|
||
"inBrowserElement "
|
||
") SELECT "
|
||
"id, "
|
||
"baseDomain, "
|
||
"originAttributes, "
|
||
"name, "
|
||
"value, "
|
||
"host, "
|
||
"path, "
|
||
"expiry, "
|
||
"lastAccessed, "
|
||
"creationTime, "
|
||
"isSecure, "
|
||
"isHttpOnly, "
|
||
"inBrowserElement "
|
||
"FROM moz_cookies;"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Drop the old table
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP TABLE moz_cookies;"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Rename new_moz_cookies to moz_cookies.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE new_moz_cookies RENAME TO moz_cookies;"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Recreate our index.
|
||
rv = CreateIndex();
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 8"));
|
||
}
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 8: {
|
||
// Add the sameSite column to the table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("ALTER TABLE moz_cookies ADD sameSite INTEGER"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 9"));
|
||
}
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case 9: {
|
||
// Add the rawSameSite column to the table.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"ALTER TABLE moz_cookies ADD rawSameSite INTEGER"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Copy the current sameSite value into rawSameSite.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"UPDATE moz_cookies SET rawSameSite = sameSite"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Upgraded database to schema version 10"));
|
||
|
||
// No more upgrades. Update the schema version.
|
||
rv =
|
||
mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
MOZ_FALLTHROUGH;
|
||
|
||
case COOKIES_SCHEMA_VERSION:
|
||
break;
|
||
|
||
case 0: {
|
||
NS_WARNING("couldn't get schema version!");
|
||
|
||
// the table may be usable; someone might've just clobbered the schema
|
||
// version. we can treat this case like a downgrade using the codepath
|
||
// below, by verifying the columns we care about are all there. for now,
|
||
// re-set the schema version in the db, in case the checks succeed (if
|
||
// they don't, we're dropping the table anyway).
|
||
rv =
|
||
mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
}
|
||
// fall through to downgrade check
|
||
MOZ_FALLTHROUGH;
|
||
|
||
// downgrading.
|
||
// if columns have been added to the table, we can still use the ones we
|
||
// understand safely. if columns have been deleted or altered, just
|
||
// blow away the table and start from scratch! if you change the way
|
||
// a column is interpreted, make sure you also change its name so this
|
||
// check will catch it.
|
||
default: {
|
||
// check if all the expected columns exist
|
||
nsCOMPtr<mozIStorageStatement> stmt;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING("SELECT "
|
||
"id, "
|
||
"baseDomain, "
|
||
"originAttributes, "
|
||
"name, "
|
||
"value, "
|
||
"host, "
|
||
"path, "
|
||
"expiry, "
|
||
"lastAccessed, "
|
||
"creationTime, "
|
||
"isSecure, "
|
||
"isHttpOnly, "
|
||
"sameSite, "
|
||
"rawSameSite "
|
||
"FROM moz_cookies"),
|
||
getter_AddRefs(stmt));
|
||
if (NS_SUCCEEDED(rv)) break;
|
||
|
||
// our columns aren't there - drop the table!
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DROP TABLE moz_cookies"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
rv = CreateTable();
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
} break;
|
||
}
|
||
}
|
||
|
||
// if we deleted a corrupt db, don't attempt to import - return now
|
||
if (aRecreateDB) {
|
||
return RESULT_OK;
|
||
}
|
||
|
||
// check whether to import or just read in the db
|
||
if (tableExists) {
|
||
return Read();
|
||
}
|
||
|
||
nsCOMPtr<nsIRunnable> runnable =
|
||
NS_NewRunnableFunction("TryInitDB.ImportCookies", [] {
|
||
NS_ENSURE_TRUE_VOID(gCookieService);
|
||
NS_ENSURE_TRUE_VOID(gCookieService->mDefaultDBState);
|
||
nsCOMPtr<nsIFile> oldCookieFile;
|
||
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
|
||
getter_AddRefs(oldCookieFile));
|
||
if (NS_FAILED(rv)) {
|
||
return;
|
||
}
|
||
|
||
// Import cookies, and clean up the old file regardless of success or
|
||
// failure. Note that we have to switch out our DBState temporarily, in
|
||
// case we're in private browsing mode; otherwise ImportCookies() won't
|
||
// be happy.
|
||
DBState* initialState = gCookieService->mDBState;
|
||
gCookieService->mDBState = gCookieService->mDefaultDBState;
|
||
oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME));
|
||
gCookieService->ImportCookies(oldCookieFile);
|
||
oldCookieFile->Remove(false);
|
||
gCookieService->mDBState = initialState;
|
||
});
|
||
|
||
NS_DispatchToMainThread(runnable);
|
||
|
||
return RESULT_OK;
|
||
}
|
||
|
||
void nsCookieService::InitDBConn() {
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
|
||
// We should skip InitDBConn if we close profile during initializing DBStates
|
||
// and then InitDBConn is called after we close the DBStates.
|
||
if (!mInitializedDBStates || mInitializedDBConn || !mDefaultDBState) {
|
||
return;
|
||
}
|
||
|
||
for (uint32_t i = 0; i < mReadArray.Length(); ++i) {
|
||
CookieDomainTuple& tuple = mReadArray[i];
|
||
MOZ_ASSERT(!tuple.cookie->isSession());
|
||
|
||
RefPtr<nsCookie> cookie = nsCookie::Create(
|
||
tuple.cookie->name(), tuple.cookie->value(), tuple.cookie->host(),
|
||
tuple.cookie->path(), tuple.cookie->expiry(),
|
||
tuple.cookie->lastAccessed(), tuple.cookie->creationTime(),
|
||
tuple.cookie->isSession(), tuple.cookie->isSecure(),
|
||
tuple.cookie->isHttpOnly(), tuple.originAttributes,
|
||
tuple.cookie->sameSite(), tuple.cookie->rawSameSite());
|
||
|
||
AddCookieToList(tuple.key, cookie, mDefaultDBState, nullptr, false);
|
||
}
|
||
|
||
if (NS_FAILED(InitDBConnInternal())) {
|
||
COOKIE_LOGSTRING(LogLevel::Warning,
|
||
("InitDBConn(): retrying InitDBConnInternal()"));
|
||
CleanupCachedStatements();
|
||
CleanupDefaultDBConnection();
|
||
if (NS_FAILED(InitDBConnInternal())) {
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("InitDBConn(): InitDBConnInternal() failed, closing connection"));
|
||
|
||
// Game over, clean the connections.
|
||
CleanupCachedStatements();
|
||
CleanupDefaultDBConnection();
|
||
}
|
||
}
|
||
mInitializedDBConn = true;
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("InitDBConn(): mInitializedDBConn = true"));
|
||
mEndInitDBConn = mozilla::TimeStamp::Now();
|
||
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-read", nullptr);
|
||
mReadArray.Clear();
|
||
}
|
||
}
|
||
|
||
nsresult nsCookieService::InitDBConnInternal() {
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
|
||
nsresult rv = mStorageService->OpenUnsharedDatabase(
|
||
mDefaultDBState->cookieFile, getter_AddRefs(mDefaultDBState->dbConn));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Set up our listeners.
|
||
mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState);
|
||
mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState);
|
||
mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState);
|
||
mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState);
|
||
|
||
// Grow cookie db in 512KB increments
|
||
mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString());
|
||
|
||
// make operations on the table asynchronous, for performance
|
||
mDefaultDBState->dbConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("PRAGMA synchronous = OFF"));
|
||
|
||
// Use write-ahead-logging for performance. We cap the autocheckpoint limit at
|
||
// 16 pages (around 500KB).
|
||
mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL"));
|
||
mDefaultDBState->dbConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = 16"));
|
||
|
||
// cache frequently used statements (for insertion, deletion, and updating)
|
||
rv = mDefaultDBState->dbConn->CreateAsyncStatement(
|
||
NS_LITERAL_CSTRING("INSERT INTO moz_cookies ("
|
||
"baseDomain, "
|
||
"originAttributes, "
|
||
"name, "
|
||
"value, "
|
||
"host, "
|
||
"path, "
|
||
"expiry, "
|
||
"lastAccessed, "
|
||
"creationTime, "
|
||
"isSecure, "
|
||
"isHttpOnly, "
|
||
"sameSite, "
|
||
"rawSameSite "
|
||
") VALUES ("
|
||
":baseDomain, "
|
||
":originAttributes, "
|
||
":name, "
|
||
":value, "
|
||
":host, "
|
||
":path, "
|
||
":expiry, "
|
||
":lastAccessed, "
|
||
":creationTime, "
|
||
":isSecure, "
|
||
":isHttpOnly, "
|
||
":sameSite, "
|
||
":rawSameSite "
|
||
")"),
|
||
getter_AddRefs(mDefaultDBState->stmtInsert));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
rv = mDefaultDBState->dbConn->CreateAsyncStatement(
|
||
NS_LITERAL_CSTRING("DELETE FROM moz_cookies "
|
||
"WHERE name = :name AND host = :host AND path = :path "
|
||
"AND originAttributes = :originAttributes"),
|
||
getter_AddRefs(mDefaultDBState->stmtDelete));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
rv = mDefaultDBState->dbConn->CreateAsyncStatement(
|
||
NS_LITERAL_CSTRING("UPDATE moz_cookies SET lastAccessed = :lastAccessed "
|
||
"WHERE name = :name AND host = :host AND path = :path "
|
||
"AND originAttributes = :originAttributes"),
|
||
getter_AddRefs(mDefaultDBState->stmtUpdate));
|
||
return rv;
|
||
}
|
||
|
||
// Sets the schema version and creates the moz_cookies table.
|
||
nsresult nsCookieService::CreateTableWorker(const char* aName) {
|
||
// Create the table.
|
||
// We default originAttributes to empty string: this is so if users revert to
|
||
// an older Firefox version that doesn't know about this field, any cookies
|
||
// set will still work once they upgrade back.
|
||
nsAutoCString command("CREATE TABLE ");
|
||
command.Append(aName);
|
||
command.AppendLiteral(
|
||
" ("
|
||
"id INTEGER PRIMARY KEY, "
|
||
"baseDomain TEXT, "
|
||
"originAttributes TEXT NOT NULL DEFAULT '', "
|
||
"name TEXT, "
|
||
"value TEXT, "
|
||
"host TEXT, "
|
||
"path TEXT, "
|
||
"expiry INTEGER, "
|
||
"lastAccessed INTEGER, "
|
||
"creationTime INTEGER, "
|
||
"isSecure INTEGER, "
|
||
"isHttpOnly INTEGER, "
|
||
"inBrowserElement INTEGER DEFAULT 0, "
|
||
"sameSite INTEGER DEFAULT 0, "
|
||
"rawSameSite INTEGER DEFAULT 0, "
|
||
"CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
|
||
")");
|
||
return mDefaultDBState->syncConn->ExecuteSimpleSQL(command);
|
||
}
|
||
|
||
// Sets the schema version and creates the moz_cookies table.
|
||
nsresult nsCookieService::CreateTable() {
|
||
// Set the schema version, before creating the table.
|
||
nsresult rv =
|
||
mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
rv = CreateTableWorker("moz_cookies");
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
return CreateIndex();
|
||
}
|
||
|
||
nsresult nsCookieService::CreateIndex() {
|
||
// Create an index on baseDomain.
|
||
return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
|
||
"originAttributes)"));
|
||
}
|
||
|
||
// Sets the schema version and creates the moz_cookies table.
|
||
nsresult nsCookieService::CreateTableForSchemaVersion6() {
|
||
// Set the schema version, before creating the table.
|
||
nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(6);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
// Create the table.
|
||
// We default originAttributes to empty string: this is so if users revert to
|
||
// an older Firefox version that doesn't know about this field, any cookies
|
||
// set will still work once they upgrade back.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"CREATE TABLE moz_cookies ("
|
||
"id INTEGER PRIMARY KEY, "
|
||
"baseDomain TEXT, "
|
||
"originAttributes TEXT NOT NULL DEFAULT '', "
|
||
"name TEXT, "
|
||
"value TEXT, "
|
||
"host TEXT, "
|
||
"path TEXT, "
|
||
"expiry INTEGER, "
|
||
"lastAccessed INTEGER, "
|
||
"creationTime INTEGER, "
|
||
"isSecure INTEGER, "
|
||
"isHttpOnly INTEGER, "
|
||
"CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
|
||
")"));
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
// Create an index on baseDomain.
|
||
return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
|
||
"originAttributes)"));
|
||
}
|
||
|
||
// Sets the schema version and creates the moz_cookies table.
|
||
nsresult nsCookieService::CreateTableForSchemaVersion5() {
|
||
// Set the schema version, before creating the table.
|
||
nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(5);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
// Create the table. We default appId/inBrowserElement to 0: this is so if
|
||
// users revert to an older Firefox version that doesn't know about these
|
||
// fields, any cookies set will still work once they upgrade back.
|
||
rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("CREATE TABLE moz_cookies ("
|
||
"id INTEGER PRIMARY KEY, "
|
||
"baseDomain TEXT, "
|
||
"appId INTEGER DEFAULT 0, "
|
||
"inBrowserElement INTEGER DEFAULT 0, "
|
||
"name TEXT, "
|
||
"value TEXT, "
|
||
"host TEXT, "
|
||
"path TEXT, "
|
||
"expiry INTEGER, "
|
||
"lastAccessed INTEGER, "
|
||
"creationTime INTEGER, "
|
||
"isSecure INTEGER, "
|
||
"isHttpOnly INTEGER, "
|
||
"CONSTRAINT moz_uniqueid UNIQUE (name, host, path, "
|
||
"appId, inBrowserElement)"
|
||
")"));
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
// Create an index on baseDomain.
|
||
return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||
"CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
|
||
"appId, "
|
||
"inBrowserElement)"));
|
||
}
|
||
|
||
void nsCookieService::CloseDBStates() {
|
||
// return if we already closed
|
||
if (!mDBState) {
|
||
return;
|
||
}
|
||
|
||
if (mThread) {
|
||
mThread->Shutdown();
|
||
mThread = nullptr;
|
||
}
|
||
|
||
// Null out our private and pointer DBStates regardless.
|
||
mPrivateDBState = nullptr;
|
||
mDBState = nullptr;
|
||
|
||
// If we don't have a default DBState, we're done.
|
||
if (!mDefaultDBState) return;
|
||
|
||
// Cleanup cached statements before we can close anything.
|
||
CleanupCachedStatements();
|
||
|
||
if (mDefaultDBState->dbConn) {
|
||
// Asynchronously close the connection. We will null it below.
|
||
mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
|
||
}
|
||
|
||
CleanupDefaultDBConnection();
|
||
|
||
mDefaultDBState = nullptr;
|
||
mInitializedDBConn = false;
|
||
mInitializedDBStates = false;
|
||
}
|
||
|
||
// Null out the statements.
|
||
// This must be done before closing the connection.
|
||
void nsCookieService::CleanupCachedStatements() {
|
||
mDefaultDBState->stmtInsert = nullptr;
|
||
mDefaultDBState->stmtDelete = nullptr;
|
||
mDefaultDBState->stmtUpdate = nullptr;
|
||
}
|
||
|
||
// Null out the listeners, and the database connection itself. This
|
||
// will not null out the statements, cancel a pending read or
|
||
// asynchronously close the connection -- these must be done
|
||
// beforehand if necessary.
|
||
void nsCookieService::CleanupDefaultDBConnection() {
|
||
MOZ_ASSERT(!mDefaultDBState->stmtInsert, "stmtInsert has been cleaned up");
|
||
MOZ_ASSERT(!mDefaultDBState->stmtDelete, "stmtDelete has been cleaned up");
|
||
MOZ_ASSERT(!mDefaultDBState->stmtUpdate, "stmtUpdate has been cleaned up");
|
||
|
||
// Null out the database connections. If 'dbConn' has not been used for any
|
||
// asynchronous operations yet, this will synchronously close it; otherwise,
|
||
// it's expected that the caller has performed an AsyncClose prior.
|
||
mDefaultDBState->dbConn = nullptr;
|
||
|
||
// Manually null out our listeners. This is necessary because they hold a
|
||
// strong ref to the DBState itself. They'll stay alive until whatever
|
||
// statements are still executing complete.
|
||
mDefaultDBState->insertListener = nullptr;
|
||
mDefaultDBState->updateListener = nullptr;
|
||
mDefaultDBState->removeListener = nullptr;
|
||
mDefaultDBState->closeListener = nullptr;
|
||
}
|
||
|
||
void nsCookieService::HandleDBClosed(DBState* aDBState) {
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("HandleDBClosed(): DBState %p closed", aDBState));
|
||
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
|
||
switch (aDBState->corruptFlag) {
|
||
case DBState::OK: {
|
||
// Database is healthy. Notify of closure.
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
|
||
}
|
||
break;
|
||
}
|
||
case DBState::CLOSING_FOR_REBUILD: {
|
||
// Our close finished. Start the rebuild, and notify of db closure later.
|
||
RebuildCorruptDB(aDBState);
|
||
break;
|
||
}
|
||
case DBState::REBUILDING: {
|
||
// We encountered an error during rebuild, closed the database, and now
|
||
// here we are. We already have a 'cookies.sqlite.bak' from the original
|
||
// dead database; we don't want to overwrite it, so let's move this one to
|
||
// 'cookies.sqlite.bak-rebuild'.
|
||
nsCOMPtr<nsIFile> backupFile;
|
||
aDBState->cookieFile->Clone(getter_AddRefs(backupFile));
|
||
nsresult rv = backupFile->MoveToNative(
|
||
nullptr, NS_LITERAL_CSTRING(COOKIES_FILE ".bak-rebuild"));
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Warning,
|
||
("HandleDBClosed(): DBState %p encountered error "
|
||
"rebuilding db; move to "
|
||
"'cookies.sqlite.bak-rebuild' gave rv 0x%" PRIx32,
|
||
aDBState, static_cast<uint32_t>(rv)));
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void nsCookieService::HandleCorruptDB(DBState* aDBState) {
|
||
if (mDefaultDBState != aDBState) {
|
||
// We've either closed the state or we've switched profiles. It's getting
|
||
// a bit late to rebuild -- bail instead.
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("HandleCorruptDB(): DBState %p is already closed, aborting",
|
||
aDBState));
|
||
return;
|
||
}
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("HandleCorruptDB(): DBState %p has corruptFlag %u",
|
||
aDBState, aDBState->corruptFlag));
|
||
|
||
// Mark the database corrupt, so the close listener can begin reconstructing
|
||
// it.
|
||
switch (mDefaultDBState->corruptFlag) {
|
||
case DBState::OK: {
|
||
// Move to 'closing' state.
|
||
mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD;
|
||
|
||
CleanupCachedStatements();
|
||
mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
|
||
CleanupDefaultDBConnection();
|
||
break;
|
||
}
|
||
case DBState::CLOSING_FOR_REBUILD: {
|
||
// We had an error while waiting for close completion. That's OK, just
|
||
// ignore it -- we're rebuilding anyway.
|
||
return;
|
||
}
|
||
case DBState::REBUILDING: {
|
||
// We had an error while rebuilding the DB. Game over. Close the database
|
||
// and let the close handler do nothing; then we'll move it out of the
|
||
// way.
|
||
CleanupCachedStatements();
|
||
if (mDefaultDBState->dbConn) {
|
||
mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
|
||
}
|
||
CleanupDefaultDBConnection();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void nsCookieService::RebuildCorruptDB(DBState* aDBState) {
|
||
NS_ASSERTION(!aDBState->dbConn, "shouldn't have an open db connection");
|
||
NS_ASSERTION(aDBState->corruptFlag == DBState::CLOSING_FOR_REBUILD,
|
||
"should be in CLOSING_FOR_REBUILD state");
|
||
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
|
||
aDBState->corruptFlag = DBState::REBUILDING;
|
||
|
||
if (mDefaultDBState != aDBState) {
|
||
// We've either closed the state or we've switched profiles. It's getting
|
||
// a bit late to rebuild -- bail instead. In any case, we were waiting
|
||
// on rebuild completion to notify of the db closure, which won't happen --
|
||
// do so now.
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("RebuildCorruptDB(): DBState %p is stale, aborting", aDBState));
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
|
||
}
|
||
return;
|
||
}
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("RebuildCorruptDB(): creating new database"));
|
||
|
||
nsCOMPtr<nsIRunnable> runnable =
|
||
NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [] {
|
||
NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDefaultDBState);
|
||
|
||
// The database has been closed, and we're ready to rebuild. Open a
|
||
// connection.
|
||
OpenDBResult result = gCookieService->TryInitDB(true);
|
||
|
||
nsCOMPtr<nsIRunnable> innerRunnable = NS_NewRunnableFunction(
|
||
"RebuildCorruptDB.TryInitDBComplete", [result] {
|
||
NS_ENSURE_TRUE_VOID(gCookieService &&
|
||
gCookieService->mDefaultDBState);
|
||
|
||
nsCOMPtr<nsIObserverService> os =
|
||
mozilla::services::GetObserverService();
|
||
if (result != RESULT_OK) {
|
||
// We're done. Reset our DB connection and statements, and
|
||
// notify of closure.
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Warning,
|
||
("RebuildCorruptDB(): TryInitDB() failed with result %u",
|
||
result));
|
||
gCookieService->CleanupCachedStatements();
|
||
gCookieService->CleanupDefaultDBConnection();
|
||
gCookieService->mDefaultDBState->corruptFlag = DBState::OK;
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Notify observers that we're beginning the rebuild.
|
||
if (os) {
|
||
os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr);
|
||
}
|
||
|
||
gCookieService->InitDBConnInternal();
|
||
|
||
// Enumerate the hash, and add cookies to the params array.
|
||
mozIStorageAsyncStatement* stmt =
|
||
gCookieService->mDefaultDBState->stmtInsert;
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
|
||
stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
|
||
for (auto iter =
|
||
gCookieService->mDefaultDBState->hostTable.Iter();
|
||
!iter.Done(); iter.Next()) {
|
||
nsCookieEntry* entry = iter.Get();
|
||
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length();
|
||
++i) {
|
||
nsCookie* cookie = cookies[i];
|
||
|
||
if (!cookie->IsSession()) {
|
||
bindCookieParameters(paramsArray, nsCookieKey(entry),
|
||
cookie);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make sure we've got something to write. If we don't, we're
|
||
// done.
|
||
uint32_t length;
|
||
paramsArray->GetLength(&length);
|
||
if (length == 0) {
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Debug,
|
||
("RebuildCorruptDB(): nothing to write, rebuild complete"));
|
||
gCookieService->mDefaultDBState->corruptFlag = DBState::OK;
|
||
return;
|
||
}
|
||
|
||
// Execute the statement. If any errors crop up, we won't try
|
||
// again.
|
||
DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(
|
||
gCookieService->mDefaultDBState->insertListener,
|
||
getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
});
|
||
NS_DispatchToMainThread(innerRunnable);
|
||
});
|
||
mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
|
||
}
|
||
|
||
nsCookieService::~nsCookieService() {
|
||
CloseDBStates();
|
||
|
||
UnregisterWeakMemoryReporter(this);
|
||
|
||
gCookieService = nullptr;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::Observe(nsISupports* aSubject, const char* aTopic,
|
||
const char16_t* aData) {
|
||
// check the topic
|
||
if (!strcmp(aTopic, "profile-before-change")) {
|
||
// The profile is about to change,
|
||
// or is going away because the application is shutting down.
|
||
|
||
// Close the default DB connection and null out our DBStates before
|
||
// changing.
|
||
CloseDBStates();
|
||
|
||
} else if (!strcmp(aTopic, "profile-do-change")) {
|
||
NS_ASSERTION(!mDefaultDBState, "shouldn't have a default DBState");
|
||
NS_ASSERTION(!mPrivateDBState, "shouldn't have a private DBState");
|
||
|
||
// the profile has already changed; init the db from the new location.
|
||
// if we are in the private browsing state, however, we do not want to read
|
||
// data into it - we should instead put it into the default state, so it's
|
||
// ready for us if and when we switch back to it.
|
||
InitDBStates();
|
||
|
||
} else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
|
||
nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
|
||
if (prefBranch) PrefChanged(prefBranch);
|
||
|
||
} else if (!strcmp(aTopic, "last-pb-context-exited")) {
|
||
// Flush all the cookies stored by private browsing contexts
|
||
mozilla::OriginAttributesPattern pattern;
|
||
pattern.mPrivateBrowsingId.Construct(1);
|
||
RemoveCookiesWithOriginAttributes(pattern, EmptyCString());
|
||
mPrivateDBState = new DBState();
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetCookieString(nsIURI* aHostURI, nsIChannel* aChannel,
|
||
nsACString& aCookie) {
|
||
return GetCookieStringCommon(aHostURI, aChannel, false, aCookie);
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetCookieStringFromHttp(nsIURI* aHostURI, nsIURI* aFirstURI,
|
||
nsIChannel* aChannel,
|
||
nsACString& aCookie) {
|
||
return GetCookieStringCommon(aHostURI, aChannel, true, aCookie);
|
||
}
|
||
|
||
nsresult nsCookieService::GetCookieStringCommon(nsIURI* aHostURI,
|
||
nsIChannel* aChannel,
|
||
bool aHttpBound,
|
||
nsACString& aCookie) {
|
||
NS_ENSURE_ARG(aHostURI);
|
||
|
||
aCookie.Truncate();
|
||
|
||
// Determine whether the request is foreign. Failure is acceptable.
|
||
bool isForeign = true;
|
||
|
||
if (aChannel) {
|
||
mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
|
||
}
|
||
|
||
bool isTrackingResource = false;
|
||
bool firstPartyStorageAccessGranted = false;
|
||
uint32_t rejectedReason = 0;
|
||
nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
|
||
do_QueryInterface(aChannel);
|
||
if (classifiedChannel) {
|
||
isTrackingResource = classifiedChannel->IsTrackingResource();
|
||
|
||
// Check first-party storage access even for non-tracking resources, since
|
||
// we will need the result when computing the access rights for the reject
|
||
// foreign cookie behavior mode.
|
||
if (AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor(
|
||
aChannel, aHostURI, &rejectedReason)) {
|
||
firstPartyStorageAccessGranted = true;
|
||
}
|
||
}
|
||
|
||
OriginAttributes attrs;
|
||
if (aChannel) {
|
||
NS_GetOriginAttributes(aChannel, attrs,
|
||
true /* considering storage principal */);
|
||
}
|
||
|
||
bool isSafeTopLevelNav = NS_IsSafeTopLevelNav(aChannel);
|
||
bool isSameSiteForeign = NS_IsSameSiteForeign(aChannel, aHostURI);
|
||
GetCookieStringInternal(aHostURI, aChannel, isForeign, isTrackingResource,
|
||
firstPartyStorageAccessGranted, rejectedReason,
|
||
isSafeTopLevelNav, isSameSiteForeign, aHttpBound,
|
||
attrs, aCookie);
|
||
return NS_OK;
|
||
}
|
||
|
||
// static
|
||
already_AddRefed<nsICookieSettings> nsCookieService::GetCookieSettings(
|
||
nsIChannel* aChannel) {
|
||
nsCOMPtr<nsICookieSettings> cookieSettings;
|
||
if (aChannel) {
|
||
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
|
||
nsresult rv = loadInfo->GetCookieSettings(getter_AddRefs(cookieSettings));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
cookieSettings = CookieSettings::CreateBlockingAll();
|
||
}
|
||
} else {
|
||
cookieSettings = CookieSettings::Create();
|
||
}
|
||
|
||
MOZ_ASSERT(cookieSettings);
|
||
return cookieSettings.forget();
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::SetCookieString(nsIURI* aHostURI, nsIPrompt* aPrompt,
|
||
const nsACString& aCookieHeader,
|
||
nsIChannel* aChannel) {
|
||
// The aPrompt argument is deprecated and unused. Avoid introducing new
|
||
// code that uses this argument by warning if the value is non-null.
|
||
MOZ_ASSERT(!aPrompt);
|
||
if (aPrompt) {
|
||
nsCOMPtr<nsIConsoleService> aConsoleService =
|
||
do_GetService("@mozilla.org/consoleservice;1");
|
||
if (aConsoleService) {
|
||
aConsoleService->LogStringMessage(
|
||
u"Non-null prompt ignored by nsCookieService.");
|
||
}
|
||
}
|
||
return SetCookieStringCommon(aHostURI, aCookieHeader, VoidCString(), aChannel,
|
||
false);
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::SetCookieStringFromHttp(nsIURI* aHostURI, nsIURI* aFirstURI,
|
||
nsIPrompt* aPrompt,
|
||
const nsACString& aCookieHeader,
|
||
const nsACString& aServerTime,
|
||
nsIChannel* aChannel) {
|
||
// The aPrompt argument is deprecated and unused. Avoid introducing new
|
||
// code that uses this argument by warning if the value is non-null.
|
||
MOZ_ASSERT(!aPrompt);
|
||
if (aPrompt) {
|
||
nsCOMPtr<nsIConsoleService> aConsoleService =
|
||
do_GetService("@mozilla.org/consoleservice;1");
|
||
if (aConsoleService) {
|
||
aConsoleService->LogStringMessage(
|
||
u"Non-null prompt ignored by nsCookieService.");
|
||
}
|
||
}
|
||
return SetCookieStringCommon(aHostURI, aCookieHeader, aServerTime, aChannel,
|
||
true);
|
||
}
|
||
|
||
int64_t nsCookieService::ParseServerTime(const nsACString& aServerTime) {
|
||
// parse server local time. this is not just done here for efficiency
|
||
// reasons - if there's an error parsing it, and we need to default it
|
||
// to the current time, we must do it here since the current time in
|
||
// SetCookieInternal() will change for each cookie processed (e.g. if the
|
||
// user is prompted).
|
||
PRTime tempServerTime;
|
||
int64_t serverTime;
|
||
PRStatus result =
|
||
PR_ParseTimeString(aServerTime.BeginReading(), true, &tempServerTime);
|
||
if (result == PR_SUCCESS) {
|
||
serverTime = tempServerTime / int64_t(PR_USEC_PER_SEC);
|
||
} else {
|
||
serverTime = PR_Now() / PR_USEC_PER_SEC;
|
||
}
|
||
|
||
return serverTime;
|
||
}
|
||
|
||
nsresult nsCookieService::SetCookieStringCommon(nsIURI* aHostURI,
|
||
const nsACString& aCookieHeader,
|
||
const nsACString& aServerTime,
|
||
nsIChannel* aChannel,
|
||
bool aFromHttp) {
|
||
NS_ENSURE_ARG(aHostURI);
|
||
|
||
// Determine whether the request is foreign. Failure is acceptable.
|
||
bool isForeign = true;
|
||
|
||
if (aChannel) {
|
||
mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
|
||
}
|
||
|
||
bool isTrackingResource = false;
|
||
bool firstPartyStorageAccessGranted = false;
|
||
uint32_t rejectedReason = 0;
|
||
nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
|
||
do_QueryInterface(aChannel);
|
||
if (classifiedChannel) {
|
||
isTrackingResource = classifiedChannel->IsTrackingResource();
|
||
|
||
// Check first-party storage access even for non-tracking resources, since
|
||
// we will need the result when computing the access rights for the reject
|
||
// foreign cookie behavior mode.
|
||
if (AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor(
|
||
aChannel, aHostURI, &rejectedReason)) {
|
||
firstPartyStorageAccessGranted = true;
|
||
}
|
||
}
|
||
|
||
OriginAttributes attrs;
|
||
if (aChannel) {
|
||
NS_GetOriginAttributes(aChannel, attrs,
|
||
true /* considering storage principal */);
|
||
}
|
||
|
||
nsCString cookieString(aCookieHeader);
|
||
SetCookieStringInternal(
|
||
aHostURI, isForeign, isTrackingResource, firstPartyStorageAccessGranted,
|
||
rejectedReason, cookieString, aServerTime, aFromHttp, attrs, aChannel);
|
||
return NS_OK;
|
||
}
|
||
|
||
void nsCookieService::SetCookieStringInternal(
|
||
nsIURI* aHostURI, bool aIsForeign, bool aIsTrackingResource,
|
||
bool aFirstPartyStorageAccessGranted, uint32_t aRejectedReason,
|
||
nsCString& aCookieHeader, const nsACString& aServerTime, bool aFromHttp,
|
||
const OriginAttributes& aOriginAttrs, nsIChannel* aChannel) {
|
||
NS_ASSERTION(aHostURI, "null host!");
|
||
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState =
|
||
(aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
|
||
|
||
// get the base domain for the host URI.
|
||
// e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
|
||
// file:// URI's (i.e. with an empty host) are allowed, but any other
|
||
// scheme must have a non-empty host. A trailing dot in the host
|
||
// is acceptable.
|
||
bool requireHostMatch;
|
||
nsAutoCString baseDomain;
|
||
nsresult rv =
|
||
GetBaseDomain(mTLDService, aHostURI, baseDomain, requireHostMatch);
|
||
if (NS_FAILED(rv)) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"couldn't get base domain from URI");
|
||
return;
|
||
}
|
||
|
||
nsCookieKey key(baseDomain, aOriginAttrs);
|
||
nsCOMPtr<nsICookieSettings> cookieSettings = GetCookieSettings(aChannel);
|
||
|
||
// check default prefs
|
||
uint32_t priorCookieCount = 0;
|
||
uint32_t rejectedReason = aRejectedReason;
|
||
nsAutoCString hostFromURI;
|
||
aHostURI->GetHost(hostFromURI);
|
||
CountCookiesFromHost(hostFromURI, &priorCookieCount);
|
||
CookieStatus cookieStatus =
|
||
CheckPrefs(cookieSettings, aHostURI, aIsForeign, aIsTrackingResource,
|
||
aFirstPartyStorageAccessGranted, aCookieHeader,
|
||
priorCookieCount, aOriginAttrs, &rejectedReason);
|
||
|
||
MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED);
|
||
|
||
// fire a notification if third party or if cookie was rejected
|
||
// (but not if there was an error)
|
||
switch (cookieStatus) {
|
||
case STATUS_REJECTED:
|
||
NotifyRejected(aHostURI, aChannel, rejectedReason, OPERATION_WRITE);
|
||
return; // Stop here
|
||
case STATUS_REJECTED_WITH_ERROR:
|
||
return;
|
||
case STATUS_ACCEPTED: // Fallthrough
|
||
case STATUS_ACCEPT_SESSION:
|
||
NotifyAccepted(aChannel);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
int64_t serverTime = ParseServerTime(aServerTime);
|
||
|
||
// process each cookie in the header
|
||
while (SetCookieInternal(aHostURI, key, requireHostMatch, cookieStatus,
|
||
aCookieHeader, serverTime, aFromHttp, aChannel)) {
|
||
// document.cookie can only set one cookie at a time
|
||
if (!aFromHttp) break;
|
||
}
|
||
}
|
||
|
||
void nsCookieService::NotifyAccepted(nsIChannel* aChannel) {
|
||
AntiTrackingCommon::NotifyBlockingDecision(
|
||
aChannel, AntiTrackingCommon::BlockingDecision::eAllow, 0);
|
||
}
|
||
|
||
// notify observers that a cookie was rejected due to the users' prefs.
|
||
void nsCookieService::NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel,
|
||
uint32_t aRejectedReason,
|
||
CookieOperation aOperation) {
|
||
if (aOperation == OPERATION_WRITE) {
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
if (os) {
|
||
os->NotifyObservers(aHostURI, "cookie-rejected", nullptr);
|
||
}
|
||
} else {
|
||
MOZ_ASSERT(aOperation == OPERATION_READ);
|
||
}
|
||
|
||
AntiTrackingCommon::NotifyBlockingDecision(
|
||
aChannel, AntiTrackingCommon::BlockingDecision::eBlock, aRejectedReason);
|
||
}
|
||
|
||
// notify observers that the cookie list changed. there are five possible
|
||
// values for aData:
|
||
// "deleted" means a cookie was deleted. aSubject is the deleted cookie.
|
||
// "added" means a cookie was added. aSubject is the added cookie.
|
||
// "changed" means a cookie was altered. aSubject is the new cookie.
|
||
// "cleared" means the entire cookie list was cleared. aSubject is null.
|
||
// "batch-deleted" means a set of cookies was purged. aSubject is the list of
|
||
// cookies.
|
||
void nsCookieService::NotifyChanged(nsISupports* aSubject,
|
||
const char16_t* aData,
|
||
bool aOldCookieIsSession, bool aFromHttp) {
|
||
const char* topic =
|
||
mDBState == mPrivateDBState ? "private-cookie-changed" : "cookie-changed";
|
||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||
if (!os) {
|
||
return;
|
||
}
|
||
// Notify for topic "private-cookie-changed" or "cookie-changed"
|
||
os->NotifyObservers(aSubject, topic, aData);
|
||
|
||
// Notify for topic "session-cookie-changed" to update the copy of session
|
||
// cookies in session restore component.
|
||
// Ignore private session cookies since they will not be restored.
|
||
if (mDBState == mPrivateDBState) {
|
||
return;
|
||
}
|
||
// Filter out notifications for individual non-session cookies.
|
||
if (NS_LITERAL_STRING("changed").Equals(aData) ||
|
||
NS_LITERAL_STRING("deleted").Equals(aData) ||
|
||
NS_LITERAL_STRING("added").Equals(aData)) {
|
||
nsCOMPtr<nsICookie> xpcCookie = do_QueryInterface(aSubject);
|
||
MOZ_ASSERT(xpcCookie);
|
||
auto cookie = static_cast<nsCookie*>(xpcCookie.get());
|
||
if (!cookie->IsSession() && !aOldCookieIsSession) {
|
||
return;
|
||
}
|
||
}
|
||
os->NotifyObservers(aSubject, "session-cookie-changed", aData);
|
||
}
|
||
|
||
already_AddRefed<nsIArray> nsCookieService::CreatePurgeList(
|
||
nsICookie* aCookie) {
|
||
nsCOMPtr<nsIMutableArray> removedList =
|
||
do_CreateInstance(NS_ARRAY_CONTRACTID);
|
||
removedList->AppendElement(aCookie);
|
||
return removedList.forget();
|
||
}
|
||
|
||
void nsCookieService::CreateOrUpdatePurgeList(nsIArray** aPurgedList,
|
||
nsICookie* aCookie) {
|
||
if (!*aPurgedList) {
|
||
COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list"));
|
||
nsCOMPtr<nsIArray> purgedList = CreatePurgeList(aCookie);
|
||
purgedList.forget(aPurgedList);
|
||
return;
|
||
}
|
||
|
||
nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(*aPurgedList);
|
||
if (purgedList) {
|
||
COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list"));
|
||
purgedList->AppendElement(aCookie);
|
||
} else {
|
||
COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!"));
|
||
}
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService:
|
||
* public transaction helper impl
|
||
******************************************************************************/
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::RunInTransaction(nsICookieTransactionCallback* aCallback) {
|
||
NS_ENSURE_ARG(aCallback);
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
if (NS_WARN_IF(!mDefaultDBState->dbConn)) {
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
mozStorageTransaction transaction(mDefaultDBState->dbConn, true);
|
||
|
||
if (NS_FAILED(aCallback->Callback())) {
|
||
Unused << transaction.Rollback();
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService:
|
||
* pref observer impl
|
||
******************************************************************************/
|
||
|
||
void nsCookieService::PrefChanged(nsIPrefBranch* aPrefBranch) {
|
||
int32_t val;
|
||
if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val)))
|
||
mMaxNumberOfCookies = (uint16_t)LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
|
||
|
||
if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) {
|
||
mCookieQuotaPerHost =
|
||
(uint16_t)LIMIT(val, 1, mMaxCookiesPerHost - 1, kCookieQuotaPerHost);
|
||
}
|
||
|
||
if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) {
|
||
mMaxCookiesPerHost = (uint16_t)LIMIT(val, mCookieQuotaPerHost + 1, 0xFFFF,
|
||
kMaxCookiesPerHost);
|
||
}
|
||
|
||
if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
|
||
mCookiePurgeAge =
|
||
int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
|
||
}
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsICookieManager impl:
|
||
* nsICookieManager
|
||
******************************************************************************/
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::RemoveAll() {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
RemoveAllFromMemory();
|
||
|
||
// clear the cookie file
|
||
if (mDBState->dbConn) {
|
||
NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state");
|
||
|
||
nsCOMPtr<mozIStorageAsyncStatement> stmt;
|
||
nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(
|
||
NS_LITERAL_CSTRING("DELETE FROM moz_cookies"), getter_AddRefs(stmt));
|
||
if (NS_SUCCEEDED(rv)) {
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(mDefaultDBState->removeListener,
|
||
getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
} else {
|
||
// Recreate the database.
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("RemoveAll(): corruption detected with rv 0x%" PRIx32,
|
||
static_cast<uint32_t>(rv)));
|
||
HandleCorruptDB(mDefaultDBState);
|
||
}
|
||
}
|
||
|
||
NotifyChanged(nullptr, u"cleared");
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetEnumerator(nsISimpleEnumerator** aEnumerator) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
const nsCookieEntry::ArrayType& cookies = iter.Get()->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
cookieList.AppendObject(cookies[i]);
|
||
}
|
||
}
|
||
|
||
return NS_NewArrayEnumerator(aEnumerator, cookieList, NS_GET_IID(nsICookie));
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetSessionEnumerator(nsISimpleEnumerator** aEnumerator) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
const nsCookieEntry::ArrayType& cookies = iter.Get()->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
nsCookie* cookie = cookies[i];
|
||
// Filter out non-session cookies.
|
||
if (cookie->IsSession()) {
|
||
cookieList.AppendObject(cookie);
|
||
}
|
||
}
|
||
}
|
||
|
||
return NS_NewArrayEnumerator(aEnumerator, cookieList, NS_GET_IID(nsICookie));
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::Add(const nsACString& aHost, const nsACString& aPath,
|
||
const nsACString& aName, const nsACString& aValue,
|
||
bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
|
||
int64_t aExpiry, JS::HandleValue aOriginAttributes,
|
||
int32_t aSameSite, JSContext* aCx) {
|
||
OriginAttributes attrs;
|
||
|
||
if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly,
|
||
aIsSession, aExpiry, &attrs, aSameSite);
|
||
}
|
||
|
||
NS_IMETHODIMP_(nsresult)
|
||
nsCookieService::AddNative(const nsACString& aHost, const nsACString& aPath,
|
||
const nsACString& aName, const nsACString& aValue,
|
||
bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
|
||
int64_t aExpiry, OriginAttributes* aOriginAttributes,
|
||
int32_t aSameSite) {
|
||
if (NS_WARN_IF(!aOriginAttributes)) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState
|
||
: mDefaultDBState;
|
||
|
||
// first, normalize the hostname, and fail if it contains illegal characters.
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// get the base domain for the host URI.
|
||
// e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
int64_t currentTimeInUsec = PR_Now();
|
||
nsCookieKey key = nsCookieKey(baseDomain, *aOriginAttributes);
|
||
|
||
RefPtr<nsCookie> cookie = nsCookie::Create(
|
||
aName, aValue, host, aPath, aExpiry, currentTimeInUsec,
|
||
nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), aIsSession,
|
||
aIsSecure, aIsHttpOnly, key.mOriginAttributes, aSameSite, aSameSite);
|
||
if (!cookie) {
|
||
return NS_ERROR_OUT_OF_MEMORY;
|
||
}
|
||
|
||
AddInternal(key, cookie, currentTimeInUsec, nullptr, VoidCString(), true);
|
||
return NS_OK;
|
||
}
|
||
|
||
nsresult nsCookieService::Remove(const nsACString& aHost,
|
||
const OriginAttributes& aAttrs,
|
||
const nsACString& aName,
|
||
const nsACString& aPath) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState =
|
||
(aAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
|
||
|
||
// first, normalize the hostname, and fail if it contains illegal characters.
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
if (!host.IsEmpty()) {
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
}
|
||
|
||
nsListIter matchIter;
|
||
RefPtr<nsCookie> cookie;
|
||
if (FindCookie(nsCookieKey(baseDomain, aAttrs), host,
|
||
PromiseFlatCString(aName), PromiseFlatCString(aPath),
|
||
matchIter)) {
|
||
cookie = matchIter.Cookie();
|
||
RemoveCookieFromList(matchIter);
|
||
}
|
||
|
||
if (cookie) {
|
||
// Everything's done. Notify observers.
|
||
NotifyChanged(cookie, u"deleted");
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::Remove(const nsACString& aHost, const nsACString& aName,
|
||
const nsACString& aPath,
|
||
JS::HandleValue aOriginAttributes, JSContext* aCx) {
|
||
OriginAttributes attrs;
|
||
|
||
if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
return RemoveNative(aHost, aName, aPath, &attrs);
|
||
}
|
||
|
||
NS_IMETHODIMP_(nsresult)
|
||
nsCookieService::RemoveNative(const nsACString& aHost, const nsACString& aName,
|
||
const nsACString& aPath,
|
||
OriginAttributes* aOriginAttributes) {
|
||
if (NS_WARN_IF(!aOriginAttributes)) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
return rv;
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* private file I/O functions
|
||
******************************************************************************/
|
||
|
||
// Extract data from a single result row and create an nsCookie.
|
||
mozilla::UniquePtr<CookieStruct> nsCookieService::GetCookieFromRow(
|
||
mozIStorageStatement* aRow) {
|
||
// Skip reading 'baseDomain' -- up to the caller.
|
||
nsCString name, value, host, path;
|
||
DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
rv = aRow->GetUTF8String(IDX_VALUE, value);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
rv = aRow->GetUTF8String(IDX_HOST, host);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
rv = aRow->GetUTF8String(IDX_PATH, path);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
int64_t expiry = aRow->AsInt64(IDX_EXPIRY);
|
||
int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED);
|
||
int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME);
|
||
bool isSecure = 0 != aRow->AsInt32(IDX_SECURE);
|
||
bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY);
|
||
int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE);
|
||
int32_t rawSameSite = aRow->AsInt32(IDX_RAW_SAME_SITE);
|
||
|
||
// Create a new constCookie and assign the data.
|
||
return mozilla::MakeUnique<CookieStruct>(
|
||
name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly,
|
||
false, isSecure, sameSite, rawSameSite);
|
||
}
|
||
|
||
void nsCookieService::EnsureReadComplete(bool aInitDBConn) {
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
|
||
bool isAccumulated = false;
|
||
|
||
if (!mInitializedDBStates) {
|
||
TimeStamp startBlockTime = TimeStamp::Now();
|
||
MonitorAutoLock lock(mMonitor);
|
||
|
||
while (!mInitializedDBStates) {
|
||
mMonitor.Wait();
|
||
}
|
||
Telemetry::AccumulateTimeDelta(
|
||
Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2, startBlockTime);
|
||
Telemetry::Accumulate(
|
||
Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
|
||
isAccumulated = true;
|
||
} else if (!mEndInitDBConn.IsNull()) {
|
||
// We didn't block main thread, and here comes the first cookie request.
|
||
// Collect how close we're going to block main thread.
|
||
Telemetry::Accumulate(
|
||
Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS,
|
||
(TimeStamp::Now() - mEndInitDBConn).ToMilliseconds());
|
||
// Nullify the timestamp so wo don't accumulate this telemetry probe again.
|
||
mEndInitDBConn = TimeStamp();
|
||
isAccumulated = true;
|
||
} else if (!mInitializedDBConn && aInitDBConn) {
|
||
// A request comes while we finished cookie thread task and InitDBConn is
|
||
// on the way from cookie thread to main thread. We're very close to block
|
||
// main thread.
|
||
Telemetry::Accumulate(
|
||
Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
|
||
isAccumulated = true;
|
||
}
|
||
|
||
if (!mInitializedDBConn && aInitDBConn && mDefaultDBState) {
|
||
InitDBConn();
|
||
if (isAccumulated) {
|
||
// Nullify the timestamp so wo don't accumulate this telemetry probe
|
||
// again.
|
||
mEndInitDBConn = TimeStamp();
|
||
}
|
||
}
|
||
}
|
||
|
||
OpenDBResult nsCookieService::Read() {
|
||
MOZ_ASSERT(NS_GetCurrentThread() == mThread);
|
||
|
||
// Set up a statement to delete any rows with a nullptr 'baseDomain'
|
||
// column. This takes care of any cookies set by browsers that don't
|
||
// understand the 'baseDomain' column, where the database schema version
|
||
// is from one that does. (This would occur when downgrading.)
|
||
nsresult rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
|
||
NS_LITERAL_CSTRING("DELETE FROM moz_cookies WHERE baseDomain ISNULL"));
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
// Read in the data synchronously.
|
||
// see IDX_NAME, etc. for parameter indexes
|
||
nsCOMPtr<mozIStorageStatement> stmt;
|
||
rv = mDefaultDBState->syncConn->CreateStatement(
|
||
NS_LITERAL_CSTRING("SELECT "
|
||
"name, "
|
||
"value, "
|
||
"host, "
|
||
"path, "
|
||
"expiry, "
|
||
"lastAccessed, "
|
||
"creationTime, "
|
||
"isSecure, "
|
||
"isHttpOnly, "
|
||
"baseDomain, "
|
||
"originAttributes, "
|
||
"sameSite, "
|
||
"rawSameSite "
|
||
"FROM moz_cookies "
|
||
"WHERE baseDomain NOTNULL"),
|
||
getter_AddRefs(stmt));
|
||
|
||
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
|
||
|
||
if (NS_WARN_IF(!mReadArray.IsEmpty())) {
|
||
mReadArray.Clear();
|
||
}
|
||
mReadArray.SetCapacity(kMaxNumberOfCookies);
|
||
|
||
nsCString baseDomain, name, value, host, path;
|
||
bool hasResult;
|
||
while (true) {
|
||
rv = stmt->ExecuteStep(&hasResult);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
mReadArray.Clear();
|
||
return RESULT_RETRY;
|
||
}
|
||
|
||
if (!hasResult) break;
|
||
|
||
// Make sure we haven't already read the data.
|
||
stmt->GetUTF8String(IDX_BASE_DOMAIN, baseDomain);
|
||
|
||
nsAutoCString suffix;
|
||
OriginAttributes attrs;
|
||
stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix);
|
||
// If PopulateFromSuffix failed we just ignore the OA attributes
|
||
// that we don't support
|
||
Unused << attrs.PopulateFromSuffix(suffix);
|
||
|
||
nsCookieKey key(baseDomain, attrs);
|
||
CookieDomainTuple* tuple = mReadArray.AppendElement();
|
||
tuple->key = std::move(key);
|
||
tuple->originAttributes = attrs;
|
||
tuple->cookie = GetCookieFromRow(stmt);
|
||
}
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("Read(): %zu cookies read", mReadArray.Length()));
|
||
|
||
return RESULT_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::ImportCookies(nsIFile* aCookieFile) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
// Make sure we're in the default DB state. We don't want people importing
|
||
// cookies into a private browsing session!
|
||
if (mDBState != mDefaultDBState) {
|
||
NS_WARNING("Trying to import cookies in a private browsing session!");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
nsresult rv;
|
||
nsCOMPtr<nsIInputStream> fileInputStream;
|
||
rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
nsCOMPtr<nsILineInputStream> lineInputStream =
|
||
do_QueryInterface(fileInputStream, &rv);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
static const char kTrue[] = "TRUE";
|
||
|
||
nsAutoCString buffer, baseDomain;
|
||
bool isMore = true;
|
||
int32_t hostIndex, isDomainIndex, pathIndex, secureIndex, expiresIndex,
|
||
nameIndex, cookieIndex;
|
||
int32_t numInts;
|
||
int64_t expires;
|
||
bool isDomain, isHttpOnly = false;
|
||
uint32_t originalCookieCount = mDefaultDBState->cookieCount;
|
||
|
||
int64_t currentTimeInUsec = PR_Now();
|
||
int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
|
||
// we use lastAccessedCounter to keep cookies in recently-used order,
|
||
// so we start by initializing to currentTime (somewhat arbitrary)
|
||
int64_t lastAccessedCounter = currentTimeInUsec;
|
||
|
||
/* file format is:
|
||
*
|
||
* host \t isDomain \t path \t secure \t expires \t name \t cookie
|
||
*
|
||
* if this format isn't respected we move onto the next line in the file.
|
||
* isDomain is "TRUE" or "FALSE" (default to "FALSE")
|
||
* isSecure is "TRUE" or "FALSE" (default to "TRUE")
|
||
* expires is a int64_t integer
|
||
* note 1: cookie can contain tabs.
|
||
* note 2: cookies will be stored in order of lastAccessed time:
|
||
* most-recently used come first; least-recently-used come last.
|
||
*/
|
||
|
||
/*
|
||
* ...but due to bug 178933, we hide HttpOnly cookies from older code
|
||
* in a comment, so they don't expose HttpOnly cookies to JS.
|
||
*
|
||
* The format for HttpOnly cookies is
|
||
*
|
||
* #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie
|
||
*
|
||
*/
|
||
|
||
// We will likely be adding a bunch of cookies to the DB, so we use async
|
||
// batching with storage to make this super fast.
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
|
||
if (originalCookieCount == 0 && mDefaultDBState->dbConn) {
|
||
mDefaultDBState->stmtInsert->NewBindingParamsArray(
|
||
getter_AddRefs(paramsArray));
|
||
}
|
||
|
||
while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
|
||
if (StringBeginsWith(buffer, NS_LITERAL_CSTRING(HTTP_ONLY_PREFIX))) {
|
||
isHttpOnly = true;
|
||
hostIndex = sizeof(HTTP_ONLY_PREFIX) - 1;
|
||
} else if (buffer.IsEmpty() || buffer.First() == '#') {
|
||
continue;
|
||
} else {
|
||
isHttpOnly = false;
|
||
hostIndex = 0;
|
||
}
|
||
|
||
// this is a cheap, cheesy way of parsing a tab-delimited line into
|
||
// string indexes, which can be lopped off into substrings. just for
|
||
// purposes of obfuscation, it also checks that each token was found.
|
||
// todo: use iterators?
|
||
if ((isDomainIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 ||
|
||
(pathIndex = buffer.FindChar('\t', isDomainIndex) + 1) == 0 ||
|
||
(secureIndex = buffer.FindChar('\t', pathIndex) + 1) == 0 ||
|
||
(expiresIndex = buffer.FindChar('\t', secureIndex) + 1) == 0 ||
|
||
(nameIndex = buffer.FindChar('\t', expiresIndex) + 1) == 0 ||
|
||
(cookieIndex = buffer.FindChar('\t', nameIndex) + 1) == 0) {
|
||
continue;
|
||
}
|
||
|
||
// check the expirytime first - if it's expired, ignore
|
||
// nullstomp the trailing tab, to avoid copying the string
|
||
auto iter = buffer.BeginWriting() + nameIndex - 1;
|
||
*iter = char(0);
|
||
numInts = PR_sscanf(buffer.get() + expiresIndex, "%lld", &expires);
|
||
if (numInts != 1 || expires < currentTime) {
|
||
continue;
|
||
}
|
||
|
||
isDomain = Substring(buffer, isDomainIndex, pathIndex - isDomainIndex - 1)
|
||
.EqualsLiteral(kTrue);
|
||
const nsACString& host =
|
||
Substring(buffer, hostIndex, isDomainIndex - hostIndex - 1);
|
||
// check for bad legacy cookies (domain not starting with a dot, or
|
||
// containing a port), and discard
|
||
if ((isDomain && !host.IsEmpty() && host.First() != '.') ||
|
||
host.Contains(':')) {
|
||
continue;
|
||
}
|
||
|
||
// compute the baseDomain from the host
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
if (NS_FAILED(rv)) continue;
|
||
|
||
// pre-existing cookies have inIsolatedMozBrowser=false set by default
|
||
// constructor of OriginAttributes().
|
||
nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
|
||
|
||
// Create a new nsCookie and assign the data. We don't know the cookie
|
||
// creation time, so just use the current time to generate a unique one.
|
||
RefPtr<nsCookie> newCookie = nsCookie::Create(
|
||
Substring(buffer, nameIndex, cookieIndex - nameIndex - 1),
|
||
Substring(buffer, cookieIndex, buffer.Length() - cookieIndex), host,
|
||
Substring(buffer, pathIndex, secureIndex - pathIndex - 1), expires,
|
||
lastAccessedCounter,
|
||
nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), false,
|
||
Substring(buffer, secureIndex, expiresIndex - secureIndex - 1)
|
||
.EqualsLiteral(kTrue),
|
||
isHttpOnly, key.mOriginAttributes, nsICookie::SAMESITE_NONE,
|
||
nsICookie::SAMESITE_NONE);
|
||
if (!newCookie) {
|
||
return NS_ERROR_OUT_OF_MEMORY;
|
||
}
|
||
|
||
// trick: preserve the most-recently-used cookie ordering,
|
||
// by successively decrementing the lastAccessed time
|
||
lastAccessedCounter--;
|
||
|
||
if (originalCookieCount == 0) {
|
||
AddCookieToList(key, newCookie, mDefaultDBState, paramsArray);
|
||
} else {
|
||
AddInternal(key, newCookie, currentTimeInUsec, nullptr, VoidCString(),
|
||
true);
|
||
}
|
||
}
|
||
|
||
// If we need to write to disk, do so now.
|
||
if (paramsArray) {
|
||
uint32_t length;
|
||
paramsArray->GetLength(&length);
|
||
if (length) {
|
||
rv = mDefaultDBState->stmtInsert->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = mDefaultDBState->stmtInsert->ExecuteAsync(
|
||
mDefaultDBState->insertListener, getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
|
||
COOKIE_LOGSTRING(LogLevel::Debug,
|
||
("ImportCookies(): %" PRIu32 " cookies imported",
|
||
mDefaultDBState->cookieCount));
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* private GetCookie/SetCookie helpers
|
||
******************************************************************************/
|
||
|
||
bool nsCookieService::DomainMatches(nsCookie* aCookie,
|
||
const nsACString& aHost) {
|
||
// first, check for an exact host or domain cookie match, e.g. "google.com"
|
||
// or ".google.com"; second a subdomain match, e.g.
|
||
// host = "mail.google.com", cookie domain = ".google.com".
|
||
return aCookie->RawHost() == aHost ||
|
||
(aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host()));
|
||
}
|
||
|
||
bool nsCookieService::PathMatches(nsCookie* aCookie, const nsACString& aPath) {
|
||
nsCString cookiePath(aCookie->GetFilePath());
|
||
|
||
// if our cookie path is empty we can't really perform our prefix check, and
|
||
// also we can't check the last character of the cookie path, so we would
|
||
// never return a successful match.
|
||
if (cookiePath.IsEmpty()) return false;
|
||
|
||
// if the cookie path and the request path are identical, they match.
|
||
if (cookiePath.Equals(aPath)) return true;
|
||
|
||
// if the cookie path is a prefix of the request path, and the last character
|
||
// of the cookie path is %x2F ("/"), they match.
|
||
bool isPrefix = StringBeginsWith(aPath, cookiePath);
|
||
if (isPrefix && cookiePath.Last() == '/') return true;
|
||
|
||
// if the cookie path is a prefix of the request path, and the first character
|
||
// of the request path that is not included in the cookie path is a %x2F ("/")
|
||
// character, they match.
|
||
uint32_t cookiePathLen = cookiePath.Length();
|
||
if (isPrefix && aPath[cookiePathLen] == '/') return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
void nsCookieService::GetCookiesForURI(
|
||
nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign,
|
||
bool aIsTrackingResource, bool aFirstPartyStorageAccessGranted,
|
||
uint32_t aRejectedReason, bool aIsSafeTopLevelNav, bool aIsSameSiteForeign,
|
||
bool aHttpBound, const OriginAttributes& aOriginAttrs,
|
||
nsTArray<nsCookie*>& aCookieList) {
|
||
NS_ASSERTION(aHostURI, "null host!");
|
||
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState =
|
||
(aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
|
||
|
||
// get the base domain, host, and path from the URI.
|
||
// e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk".
|
||
// file:// URI's (i.e. with an empty host) are allowed, but any other
|
||
// scheme must have a non-empty host. A trailing dot in the host
|
||
// is acceptable.
|
||
bool requireHostMatch;
|
||
nsAutoCString baseDomain, hostFromURI, pathFromURI;
|
||
nsresult rv =
|
||
GetBaseDomain(mTLDService, aHostURI, baseDomain, requireHostMatch);
|
||
if (NS_SUCCEEDED(rv)) rv = aHostURI->GetAsciiHost(hostFromURI);
|
||
if (NS_SUCCEEDED(rv)) rv = aHostURI->GetFilePath(pathFromURI);
|
||
if (NS_FAILED(rv)) {
|
||
COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, VoidCString(),
|
||
"invalid host/path from URI");
|
||
return;
|
||
}
|
||
|
||
nsCOMPtr<nsICookieSettings> cookieSettings = GetCookieSettings(aChannel);
|
||
|
||
// check default prefs
|
||
uint32_t rejectedReason = aRejectedReason;
|
||
uint32_t priorCookieCount = 0;
|
||
CountCookiesFromHost(hostFromURI, &priorCookieCount);
|
||
CookieStatus cookieStatus =
|
||
CheckPrefs(cookieSettings, aHostURI, aIsForeign, aIsTrackingResource,
|
||
aFirstPartyStorageAccessGranted, VoidCString(),
|
||
priorCookieCount, aOriginAttrs, &rejectedReason);
|
||
|
||
MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED);
|
||
|
||
// for GetCookie(), we only fire acceptance/rejection notifications
|
||
// (but not if there was an error)
|
||
switch (cookieStatus) {
|
||
case STATUS_REJECTED:
|
||
// If we don't have any cookies from this host, fail silently.
|
||
if (priorCookieCount) {
|
||
NotifyRejected(aHostURI, aChannel, rejectedReason, OPERATION_READ);
|
||
}
|
||
return;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// Note: The following permissions logic is mirrored in
|
||
// extensions::MatchPattern::MatchesCookie.
|
||
// If it changes, please update that function, or file a bug for someone
|
||
// else to do so.
|
||
|
||
// check if aHostURI is using an https secure protocol.
|
||
// if it isn't, then we can't send a secure cookie over the connection.
|
||
// if SchemeIs fails, assume an insecure connection, to be on the safe side
|
||
bool isSecure = aHostURI->SchemeIs("https");
|
||
|
||
nsCookie* cookie;
|
||
int64_t currentTimeInUsec = PR_Now();
|
||
int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
|
||
bool stale = false;
|
||
|
||
nsCookieKey key(baseDomain, aOriginAttrs);
|
||
|
||
// perform the hash lookup
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(key);
|
||
if (!entry) return;
|
||
|
||
// iterate the cookies!
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
cookie = cookies[i];
|
||
|
||
// check the host, since the base domain lookup is conservative.
|
||
if (!DomainMatches(cookie, hostFromURI)) continue;
|
||
|
||
// if the cookie is secure and the host scheme isn't, we can't send it
|
||
if (cookie->IsSecure() && !isSecure) continue;
|
||
|
||
int32_t sameSiteAttr = 0;
|
||
cookie->GetSameSite(&sameSiteAttr);
|
||
if (aIsSameSiteForeign) {
|
||
// it if's a cross origin request and the cookie is same site only
|
||
// (strict) don't send it
|
||
if (sameSiteAttr == nsICookie::SAMESITE_STRICT) {
|
||
continue;
|
||
}
|
||
// if it's a cross origin request, the cookie is same site lax, but it's
|
||
// not a top-level navigation, don't send it
|
||
if (sameSiteAttr == nsICookie::SAMESITE_LAX && !aIsSafeTopLevelNav) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// if the cookie is httpOnly and it's not going directly to the HTTP
|
||
// connection, don't send it
|
||
if (cookie->IsHttpOnly() && !aHttpBound) continue;
|
||
|
||
// if the nsIURI path doesn't match the cookie path, don't send it back
|
||
if (!PathMatches(cookie, pathFromURI)) continue;
|
||
|
||
// check if the cookie has expired
|
||
if (cookie->Expiry() <= currentTime) {
|
||
continue;
|
||
}
|
||
|
||
// all checks passed - add to list and check if lastAccessed stamp needs
|
||
// updating
|
||
aCookieList.AppendElement(cookie);
|
||
if (cookie->IsStale()) {
|
||
stale = true;
|
||
}
|
||
}
|
||
|
||
int32_t count = aCookieList.Length();
|
||
if (count == 0) return;
|
||
|
||
// Send a notification about the acceptance of the cookies now that we found
|
||
// some.
|
||
NotifyAccepted(aChannel);
|
||
|
||
// update lastAccessed timestamps. we only do this if the timestamp is stale
|
||
// by a certain amount, to avoid thrashing the db during pageload.
|
||
if (stale) {
|
||
// Create an array of parameters to bind to our update statement. Batching
|
||
// is OK here since we're updating cookies with no interleaved operations.
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
|
||
mozIStorageAsyncStatement* stmt = mDBState->stmtUpdate;
|
||
if (mDBState->dbConn) {
|
||
stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
|
||
}
|
||
|
||
for (int32_t i = 0; i < count; ++i) {
|
||
cookie = aCookieList.ElementAt(i);
|
||
|
||
if (cookie->IsStale()) {
|
||
UpdateCookieInList(cookie, currentTimeInUsec, paramsArray);
|
||
}
|
||
}
|
||
// Update the database now if necessary.
|
||
if (paramsArray) {
|
||
uint32_t length;
|
||
paramsArray->GetLength(&length);
|
||
if (length) {
|
||
DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(mDBState->updateListener,
|
||
getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
}
|
||
|
||
// return cookies in order of path length; longest to shortest.
|
||
// this is required per RFC2109. if cookies match in length,
|
||
// then sort by creation time (see bug 236772).
|
||
aCookieList.Sort(CompareCookiesForSending());
|
||
}
|
||
|
||
void nsCookieService::GetCookieStringInternal(
|
||
nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign,
|
||
bool aIsTrackingResource, bool aFirstPartyStorageAccessGranted,
|
||
uint32_t aRejectedReason, bool aIsSafeTopLevelNav, bool aIsSameSiteForeign,
|
||
bool aHttpBound, const OriginAttributes& aOriginAttrs,
|
||
nsACString& aCookieString) {
|
||
AutoTArray<nsCookie*, 8> foundCookieList;
|
||
GetCookiesForURI(aHostURI, aChannel, aIsForeign, aIsTrackingResource,
|
||
aFirstPartyStorageAccessGranted, aRejectedReason,
|
||
aIsSafeTopLevelNav, aIsSameSiteForeign, aHttpBound,
|
||
aOriginAttrs, foundCookieList);
|
||
|
||
nsCookie* cookie;
|
||
for (uint32_t i = 0; i < foundCookieList.Length(); ++i) {
|
||
cookie = foundCookieList.ElementAt(i);
|
||
|
||
// check if we have anything to write
|
||
if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
|
||
// if we've already added a cookie to the return list, append a "; " so
|
||
// that subsequent cookies are delimited in the final list.
|
||
if (!aCookieString.IsEmpty()) {
|
||
aCookieString.AppendLiteral("; ");
|
||
}
|
||
|
||
if (!cookie->Name().IsEmpty()) {
|
||
// we have a name and value - write both
|
||
aCookieString +=
|
||
cookie->Name() + NS_LITERAL_CSTRING("=") + cookie->Value();
|
||
} else {
|
||
// just write value
|
||
aCookieString += cookie->Value();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!aCookieString.IsEmpty())
|
||
COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false);
|
||
}
|
||
|
||
// processes a single cookie, and returns true if there are more cookies
|
||
// to be processed
|
||
bool nsCookieService::CanSetCookie(nsIURI* aHostURI, const nsCookieKey& aKey,
|
||
CookieStruct& aCookieData,
|
||
bool aRequireHostMatch, CookieStatus aStatus,
|
||
nsCString& aCookieHeader,
|
||
int64_t aServerTime, bool aFromHttp,
|
||
nsIChannel* aChannel, bool& aSetCookie,
|
||
mozIThirdPartyUtil* aThirdPartyUtil) {
|
||
NS_ASSERTION(aHostURI, "null host!");
|
||
|
||
aSetCookie = false;
|
||
|
||
// init expiryTime such that session cookies won't prematurely expire
|
||
aCookieData.expiry() = INT64_MAX;
|
||
|
||
// aCookieHeader is an in/out param to point to the next cookie, if
|
||
// there is one. Save the present value for logging purposes
|
||
nsCString savedCookieHeader(aCookieHeader);
|
||
|
||
// newCookie says whether there are multiple cookies in the header;
|
||
// so we can handle them separately.
|
||
nsAutoCString expires;
|
||
nsAutoCString maxage;
|
||
bool acceptedByParser = false;
|
||
bool newCookie = ParseAttributes(aCookieHeader, aCookieData, expires, maxage,
|
||
acceptedByParser);
|
||
if (!acceptedByParser) {
|
||
return newCookie;
|
||
}
|
||
|
||
// Collect telemetry on how often secure cookies are set from non-secure
|
||
// origins, and vice-versa.
|
||
//
|
||
// 0 = nonsecure and "http:"
|
||
// 1 = nonsecure and "https:"
|
||
// 2 = secure and "http:"
|
||
// 3 = secure and "https:"
|
||
bool isHTTPS = aHostURI->SchemeIs("https");
|
||
Telemetry::Accumulate(
|
||
Telemetry::COOKIE_SCHEME_SECURITY,
|
||
((aCookieData.isSecure()) ? 0x02 : 0x00) | ((isHTTPS) ? 0x01 : 0x00));
|
||
|
||
// Collect telemetry on how often are first- and third-party cookies set
|
||
// from HTTPS origins:
|
||
//
|
||
// 0 (000) = first-party and "http:"
|
||
// 1 (001) = first-party and "http:" with bogus Secure cookie flag?!
|
||
// 2 (010) = first-party and "https:"
|
||
// 3 (011) = first-party and "https:" with Secure cookie flag
|
||
// 4 (100) = third-party and "http:"
|
||
// 5 (101) = third-party and "http:" with bogus Secure cookie flag?!
|
||
// 6 (110) = third-party and "https:"
|
||
// 7 (111) = third-party and "https:" with Secure cookie flag
|
||
if (aThirdPartyUtil) {
|
||
bool isThirdParty = true;
|
||
|
||
if (aChannel) {
|
||
aThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isThirdParty);
|
||
}
|
||
Telemetry::Accumulate(Telemetry::COOKIE_SCHEME_HTTPS,
|
||
(isThirdParty ? 0x04 : 0x00) |
|
||
(isHTTPS ? 0x02 : 0x00) |
|
||
(aCookieData.isSecure() ? 0x01 : 0x00));
|
||
}
|
||
|
||
int64_t currentTimeInUsec = PR_Now();
|
||
|
||
// calculate expiry time of cookie.
|
||
aCookieData.isSession() =
|
||
GetExpiry(aCookieData, expires, maxage, aServerTime,
|
||
currentTimeInUsec / PR_USEC_PER_SEC, aFromHttp);
|
||
if (aStatus == STATUS_ACCEPT_SESSION) {
|
||
// force lifetime to session. note that the expiration time, if set above,
|
||
// will still apply.
|
||
aCookieData.isSession() = true;
|
||
}
|
||
|
||
// reject cookie if it's over the size limit, per RFC2109
|
||
if ((aCookieData.name().Length() + aCookieData.value().Length()) >
|
||
kMaxBytesPerCookie) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"cookie too big (> 4kb)");
|
||
return newCookie;
|
||
}
|
||
|
||
const char illegalNameCharacters[] = {
|
||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
|
||
0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||
0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x00};
|
||
|
||
if (aCookieData.name().FindCharInSet(illegalNameCharacters, 0) != -1) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"invalid name character");
|
||
return newCookie;
|
||
}
|
||
|
||
// domain & path checks
|
||
if (!CheckDomain(aCookieData, aHostURI, aKey.mBaseDomain,
|
||
aRequireHostMatch)) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"failed the domain tests");
|
||
return newCookie;
|
||
}
|
||
|
||
if (!CheckPath(aCookieData, aHostURI)) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"failed the path tests");
|
||
return newCookie;
|
||
}
|
||
|
||
// magic prefix checks. MUST be run after CheckDomain() and CheckPath()
|
||
if (!CheckPrefixes(aCookieData, isHTTPS)) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"failed the prefix tests");
|
||
return newCookie;
|
||
}
|
||
|
||
// reject cookie if value contains an RFC 6265 disallowed character - see
|
||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1191423
|
||
// NOTE: this is not the full set of characters disallowed by 6265 - notably
|
||
// 0x09, 0x20, 0x22, 0x2C, 0x5C, and 0x7F are missing from this list. This is
|
||
// for parity with Chrome. This only applies to cookies set via the Set-Cookie
|
||
// header, as document.cookie is defined to be UTF-8. Hooray for
|
||
// symmetry!</sarcasm>
|
||
const char illegalCharacters[] = {
|
||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
|
||
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x00};
|
||
if (aFromHttp &&
|
||
(aCookieData.value().FindCharInSet(illegalCharacters, 0) != -1)) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"invalid value character");
|
||
return newCookie;
|
||
}
|
||
|
||
// if the new cookie is httponly, make sure we're not coming from script
|
||
if (!aFromHttp && aCookieData.isHttpOnly()) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"cookie is httponly; coming from script");
|
||
return newCookie;
|
||
}
|
||
|
||
// If the new cookie is non-https and wants to set secure flag,
|
||
// browser have to ignore this new cookie.
|
||
// (draft-ietf-httpbis-cookie-alone section 3.1)
|
||
if (aCookieData.isSecure() && !isHTTPS) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"non-https cookie can't set secure flag");
|
||
return newCookie;
|
||
}
|
||
|
||
// If the new cookie is same-site but in a cross site context,
|
||
// browser must ignore the cookie.
|
||
if ((aCookieData.sameSite() != nsICookie::SAMESITE_NONE) && aThirdPartyUtil) {
|
||
// Do not treat loads triggered by web extensions as foreign
|
||
bool addonAllowsLoad = false;
|
||
if (aChannel) {
|
||
nsCOMPtr<nsIURI> channelURI;
|
||
NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
|
||
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
|
||
addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
|
||
->AddonAllowsLoad(channelURI);
|
||
}
|
||
|
||
if (!addonAllowsLoad) {
|
||
bool isThirdParty = false;
|
||
nsresult rv = aThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI,
|
||
&isThirdParty);
|
||
if (NS_FAILED(rv) || isThirdParty) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"failed the samesite tests");
|
||
return newCookie;
|
||
}
|
||
}
|
||
}
|
||
|
||
aSetCookie = true;
|
||
return newCookie;
|
||
}
|
||
|
||
// processes a single cookie, and returns true if there are more cookies
|
||
// to be processed
|
||
bool nsCookieService::SetCookieInternal(
|
||
nsIURI* aHostURI, const mozilla::net::nsCookieKey& aKey,
|
||
bool aRequireHostMatch, CookieStatus aStatus, nsCString& aCookieHeader,
|
||
int64_t aServerTime, bool aFromHttp, nsIChannel* aChannel) {
|
||
NS_ASSERTION(aHostURI, "null host!");
|
||
bool canSetCookie = false;
|
||
nsCString savedCookieHeader(aCookieHeader);
|
||
CookieStruct cookieData;
|
||
bool newCookie = CanSetCookie(aHostURI, aKey, cookieData, aRequireHostMatch,
|
||
aStatus, aCookieHeader, aServerTime, aFromHttp,
|
||
aChannel, canSetCookie, mThirdPartyUtil);
|
||
|
||
if (!canSetCookie) {
|
||
return newCookie;
|
||
}
|
||
|
||
int64_t currentTimeInUsec = PR_Now();
|
||
// create a new nsCookie and copy attributes
|
||
RefPtr<nsCookie> cookie = nsCookie::Create(
|
||
cookieData.name(), cookieData.value(), cookieData.host(),
|
||
cookieData.path(), cookieData.expiry(), currentTimeInUsec,
|
||
nsCookie::GenerateUniqueCreationTime(currentTimeInUsec),
|
||
cookieData.isSession(), cookieData.isSecure(), cookieData.isHttpOnly(),
|
||
aKey.mOriginAttributes, cookieData.sameSite(), cookieData.rawSameSite());
|
||
if (!cookie) return newCookie;
|
||
|
||
// check permissions from site permission list, or ask the user,
|
||
// to determine if we can set the cookie
|
||
if (mPermissionService) {
|
||
bool permission;
|
||
mPermissionService->CanSetCookie(
|
||
aHostURI, aChannel,
|
||
static_cast<nsICookie*>(static_cast<nsCookie*>(cookie)),
|
||
&cookieData.isSession(), &cookieData.expiry(), &permission);
|
||
if (!permission) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
|
||
"cookie rejected by permission manager");
|
||
NotifyRejected(
|
||
aHostURI, aChannel,
|
||
nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION,
|
||
OPERATION_WRITE);
|
||
return newCookie;
|
||
}
|
||
|
||
// update isSession and expiry attributes, in case they changed
|
||
cookie->SetIsSession(cookieData.isSession());
|
||
cookie->SetExpiry(cookieData.expiry());
|
||
}
|
||
|
||
// add the cookie to the list. AddInternal() takes care of logging.
|
||
// we get the current time again here, since it may have changed during
|
||
// prompting
|
||
AddInternal(aKey, cookie, PR_Now(), aHostURI, savedCookieHeader, aFromHttp);
|
||
return newCookie;
|
||
}
|
||
|
||
// this is a backend function for adding a cookie to the list, via SetCookie.
|
||
// also used in the cookie manager, for profile migration from IE.
|
||
// it either replaces an existing cookie; or adds the cookie to the hashtable,
|
||
// and deletes a cookie (if maximum number of cookies has been
|
||
// reached). also performs list maintenance by removing expired cookies.
|
||
void nsCookieService::AddInternal(const nsCookieKey& aKey, nsCookie* aCookie,
|
||
int64_t aCurrentTimeInUsec, nsIURI* aHostURI,
|
||
const nsACString& aCookieHeader,
|
||
bool aFromHttp) {
|
||
MOZ_ASSERT(mInitializedDBStates);
|
||
MOZ_ASSERT(mInitializedDBConn);
|
||
|
||
int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
|
||
|
||
nsListIter exactIter;
|
||
bool foundCookie = false;
|
||
foundCookie = FindCookie(aKey, aCookie->Host(), aCookie->Name(),
|
||
aCookie->Path(), exactIter);
|
||
bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
|
||
bool isSecure = true;
|
||
if (aHostURI) {
|
||
isSecure = aHostURI->SchemeIs("https");
|
||
}
|
||
bool oldCookieIsSession = false;
|
||
// Step1, call FindSecureCookie(). FindSecureCookie() would
|
||
// find the existing cookie with the security flag and has
|
||
// the same name, host and path of the new cookie, if there is any.
|
||
// Step2, Confirm new cookie's security setting. If any targeted
|
||
// cookie had been found in Step1, then confirm whether the
|
||
// new cookie could modify it. If the new created cookie’s
|
||
// "secure-only-flag" is not set, and the "scheme" component
|
||
// of the "request-uri" does not denote a "secure" protocol,
|
||
// then ignore the new cookie.
|
||
// (draft-ietf-httpbis-cookie-alone section 3.2)
|
||
if (!aCookie->IsSecure() &&
|
||
(foundSecureExact || FindSecureCookie(aKey, aCookie)) && !isSecure) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"cookie can't save because older cookie is secure "
|
||
"cookie but newer cookie is non-secure cookie");
|
||
return;
|
||
}
|
||
|
||
RefPtr<nsCookie> oldCookie;
|
||
nsCOMPtr<nsIArray> purgedList;
|
||
if (foundCookie) {
|
||
oldCookie = exactIter.Cookie();
|
||
oldCookieIsSession = oldCookie->IsSession();
|
||
|
||
// Check if the old cookie is stale (i.e. has already expired). If so, we
|
||
// need to be careful about the semantics of removing it and adding the new
|
||
// cookie: we want the behavior wrt adding the new cookie to be the same as
|
||
// if it didn't exist, but we still want to fire a removal notification.
|
||
if (oldCookie->Expiry() <= currentTime) {
|
||
if (aCookie->Expiry() <= currentTime) {
|
||
// The new cookie has expired and the old one is stale. Nothing to do.
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"cookie has already expired");
|
||
return;
|
||
}
|
||
|
||
// Remove the stale cookie. We save notification for later, once all list
|
||
// modifications are complete.
|
||
RemoveCookieFromList(exactIter);
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"stale cookie was purged");
|
||
purgedList = CreatePurgeList(oldCookie);
|
||
|
||
// We've done all we need to wrt removing and notifying the stale cookie.
|
||
// From here on out, we pretend pretend it didn't exist, so that we
|
||
// preserve expected notification semantics when adding the new cookie.
|
||
foundCookie = false;
|
||
|
||
} else {
|
||
// If the old cookie is httponly, make sure we're not coming from script.
|
||
if (!aFromHttp && oldCookie->IsHttpOnly()) {
|
||
COOKIE_LOGFAILURE(
|
||
SET_COOKIE, aHostURI, aCookieHeader,
|
||
"previously stored cookie is httponly; coming from script");
|
||
return;
|
||
}
|
||
|
||
// If the new cookie has the same value, expiry date, isSecure, isSession,
|
||
// isHttpOnly and sameSite flags then we can just keep the old one.
|
||
// Only if any of these differ we would want to override the cookie.
|
||
if (oldCookie->Value().Equals(aCookie->Value()) &&
|
||
oldCookie->Expiry() == aCookie->Expiry() &&
|
||
oldCookie->IsSecure() == aCookie->IsSecure() &&
|
||
oldCookie->IsSession() == aCookie->IsSession() &&
|
||
oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
|
||
oldCookie->SameSite() == aCookie->SameSite() &&
|
||
// We don't want to perform this optimization if the cookie is
|
||
// considered stale, since in this case we would need to update the
|
||
// database.
|
||
!oldCookie->IsStale()) {
|
||
// Update the last access time on the old cookie.
|
||
oldCookie->SetLastAccessed(aCookie->LastAccessed());
|
||
UpdateCookieOldestTime(mDBState, oldCookie);
|
||
return;
|
||
}
|
||
|
||
// Remove the old cookie.
|
||
RemoveCookieFromList(exactIter);
|
||
|
||
// If the new cookie has expired -- i.e. the intent was simply to delete
|
||
// the old cookie -- then we're done.
|
||
if (aCookie->Expiry() <= currentTime) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"previously stored cookie was deleted");
|
||
NotifyChanged(oldCookie, u"deleted", oldCookieIsSession, aFromHttp);
|
||
return;
|
||
}
|
||
|
||
// Preserve creation time of cookie for ordering purposes.
|
||
aCookie->SetCreationTime(oldCookie->CreationTime());
|
||
}
|
||
|
||
} else {
|
||
// check if cookie has already expired
|
||
if (aCookie->Expiry() <= currentTime) {
|
||
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
|
||
"cookie has already expired");
|
||
return;
|
||
}
|
||
|
||
// check if we have to delete an old cookie.
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(aKey);
|
||
if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
|
||
nsTArray<nsListIter> removedIterList;
|
||
// Prioritize evicting insecure cookies.
|
||
// (draft-ietf-httpbis-cookie-alone section 3.3)
|
||
uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost;
|
||
FindStaleCookies(entry, currentTime, false, removedIterList, limit);
|
||
if (removedIterList.Length() == 0) {
|
||
if (aCookie->IsSecure()) {
|
||
// It's valid to evict a secure cookie for another secure cookie.
|
||
FindStaleCookies(entry, currentTime, true, removedIterList, limit);
|
||
} else {
|
||
COOKIE_LOGEVICTED(aCookie,
|
||
"Too many cookies for this domain and the new "
|
||
"cookie is not a secure cookie");
|
||
return;
|
||
}
|
||
}
|
||
|
||
MOZ_ASSERT(!removedIterList.IsEmpty());
|
||
// Sort |removedIterList| by index again, since we have to remove the
|
||
// cookie in the reverse order.
|
||
removedIterList.Sort(CompareCookiesByIndex());
|
||
for (auto it = removedIterList.rbegin(); it != removedIterList.rend();
|
||
it++) {
|
||
RefPtr<nsCookie> evictedCookie = (*it).Cookie();
|
||
COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain");
|
||
RemoveCookieFromList(*it);
|
||
CreateOrUpdatePurgeList(getter_AddRefs(purgedList), evictedCookie);
|
||
MOZ_ASSERT((*it).entry);
|
||
}
|
||
|
||
} else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
|
||
int64_t maxAge = aCurrentTimeInUsec - mDBState->cookieOldestTime;
|
||
int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
|
||
if (maxAge >= purgeAge) {
|
||
// we're over both size and age limits by 10%; time to purge the table!
|
||
// do this by:
|
||
// 1) removing expired cookies;
|
||
// 2) evicting the balance of old cookies until we reach the size limit.
|
||
// note that the cookieOldestTime indicator can be pessimistic - if it's
|
||
// older than the actual oldest cookie, we'll just purge more eagerly.
|
||
purgedList = PurgeCookies(aCurrentTimeInUsec);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add the cookie to the db. We do not supply a params array for batching
|
||
// because this might result in removals and additions being out of order.
|
||
AddCookieToList(aKey, aCookie, mDBState, nullptr);
|
||
COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
|
||
|
||
// Now that list mutations are complete, notify observers. We do it here
|
||
// because observers may themselves attempt to mutate the list.
|
||
if (purgedList) {
|
||
NotifyChanged(purgedList, u"batch-deleted");
|
||
}
|
||
|
||
NotifyChanged(aCookie, foundCookie ? u"changed" : u"added",
|
||
oldCookieIsSession, aFromHttp);
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* private cookie header parsing functions
|
||
******************************************************************************/
|
||
|
||
// clang-format off
|
||
// The following comment block elucidates the function of ParseAttributes.
|
||
/******************************************************************************
|
||
** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1
|
||
** please note: this BNF deviates from both specifications, and reflects this
|
||
** implementation. <bnf> indicates a reference to the defined grammar "bnf".
|
||
|
||
** Differences from RFC2109/2616 and explanations:
|
||
1. implied *LWS
|
||
The grammar described by this specification is word-based. Except
|
||
where noted otherwise, linear white space (<LWS>) can be included
|
||
between any two adjacent words (token or quoted-string), and
|
||
between adjacent words and separators, without changing the
|
||
interpretation of a field.
|
||
<LWS> according to spec is SP|HT|CR|LF, but here, we allow only SP | HT.
|
||
|
||
2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in
|
||
common use inside values.
|
||
|
||
3. tokens and values have looser restrictions on allowed characters than
|
||
spec. This is also due to certain characters being in common use inside
|
||
values. We allow only '=' to separate token/value pairs, and ';' to
|
||
terminate tokens or values. <LWS> is allowed within tokens and values
|
||
(see bug 206022).
|
||
|
||
4. where appropriate, full <OCTET>s are allowed, where the spec dictates to
|
||
reject control chars or non-ASCII chars. This is erring on the loose
|
||
side, since there's probably no good reason to enforce this strictness.
|
||
|
||
5. cookie <NAME> is optional, where spec requires it. This is a fairly
|
||
trivial case, but allows the flexibility of setting only a cookie <VALUE>
|
||
with a blank <NAME> and is required by some sites (see bug 169091).
|
||
|
||
6. Attribute "HttpOnly", not covered in the RFCs, is supported
|
||
(see bug 178993).
|
||
|
||
** Begin BNF:
|
||
token = 1*<any allowed-chars except separators>
|
||
value = 1*<any allowed-chars except value-sep>
|
||
separators = ";" | "="
|
||
value-sep = ";"
|
||
cookie-sep = CR | LF
|
||
allowed-chars = <any OCTET except NUL or cookie-sep>
|
||
OCTET = <any 8-bit sequence of data>
|
||
LWS = SP | HT
|
||
NUL = <US-ASCII NUL, null control character (0)>
|
||
CR = <US-ASCII CR, carriage return (13)>
|
||
LF = <US-ASCII LF, linefeed (10)>
|
||
SP = <US-ASCII SP, space (32)>
|
||
HT = <US-ASCII HT, horizontal-tab (9)>
|
||
|
||
set-cookie = "Set-Cookie:" cookies
|
||
cookies = cookie *( cookie-sep cookie )
|
||
cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first
|
||
NAME = token ; cookie name
|
||
VALUE = value ; cookie value
|
||
cookie-av = token ["=" value]
|
||
|
||
valid values for cookie-av (checked post-parsing) are:
|
||
cookie-av = "Path" "=" value
|
||
| "Domain" "=" value
|
||
| "Expires" "=" value
|
||
| "Max-Age" "=" value
|
||
| "Comment" "=" value
|
||
| "Version" "=" value
|
||
| "Secure"
|
||
| "HttpOnly"
|
||
|
||
******************************************************************************/
|
||
// clang-format on
|
||
|
||
// helper functions for GetTokenValue
|
||
static inline bool iswhitespace(char c) { return c == ' ' || c == '\t'; }
|
||
static inline bool isterminator(char c) { return c == '\n' || c == '\r'; }
|
||
static inline bool isvalueseparator(char c) {
|
||
return isterminator(c) || c == ';';
|
||
}
|
||
static inline bool istokenseparator(char c) {
|
||
return isvalueseparator(c) || c == '=';
|
||
}
|
||
|
||
// Parse a single token/value pair.
|
||
// Returns true if a cookie terminator is found, so caller can parse new cookie.
|
||
bool nsCookieService::GetTokenValue(nsACString::const_char_iterator& aIter,
|
||
nsACString::const_char_iterator& aEndIter,
|
||
nsDependentCSubstring& aTokenString,
|
||
nsDependentCSubstring& aTokenValue,
|
||
bool& aEqualsFound) {
|
||
nsACString::const_char_iterator start, lastSpace;
|
||
// initialize value string to clear garbage
|
||
aTokenValue.Rebind(aIter, aIter);
|
||
|
||
// find <token>, including any <LWS> between the end-of-token and the
|
||
// token separator. we'll remove trailing <LWS> next
|
||
while (aIter != aEndIter && iswhitespace(*aIter)) ++aIter;
|
||
start = aIter;
|
||
while (aIter != aEndIter && !istokenseparator(*aIter)) ++aIter;
|
||
|
||
// remove trailing <LWS>; first check we're not at the beginning
|
||
lastSpace = aIter;
|
||
if (lastSpace != start) {
|
||
while (--lastSpace != start && iswhitespace(*lastSpace)) continue;
|
||
++lastSpace;
|
||
}
|
||
aTokenString.Rebind(start, lastSpace);
|
||
|
||
aEqualsFound = (*aIter == '=');
|
||
if (aEqualsFound) {
|
||
// find <value>
|
||
while (++aIter != aEndIter && iswhitespace(*aIter)) continue;
|
||
|
||
start = aIter;
|
||
|
||
// process <token>
|
||
// just look for ';' to terminate ('=' allowed)
|
||
while (aIter != aEndIter && !isvalueseparator(*aIter)) ++aIter;
|
||
|
||
// remove trailing <LWS>; first check we're not at the beginning
|
||
if (aIter != start) {
|
||
lastSpace = aIter;
|
||
while (--lastSpace != start && iswhitespace(*lastSpace)) continue;
|
||
aTokenValue.Rebind(start, ++lastSpace);
|
||
}
|
||
}
|
||
|
||
// aIter is on ';', or terminator, or EOS
|
||
if (aIter != aEndIter) {
|
||
// if on terminator, increment past & return true to process new cookie
|
||
if (isterminator(*aIter)) {
|
||
++aIter;
|
||
return true;
|
||
}
|
||
// fall-through: aIter is on ';', increment and return false
|
||
++aIter;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Parses attributes from cookie header. expires/max-age attributes aren't
|
||
// folded into the cookie struct here, because we don't know which one to use
|
||
// until we've parsed the header.
|
||
bool nsCookieService::ParseAttributes(nsCString& aCookieHeader,
|
||
CookieStruct& aCookieData,
|
||
nsACString& aExpires, nsACString& aMaxage,
|
||
bool& aAcceptedByParser) {
|
||
aAcceptedByParser = false;
|
||
|
||
static const char kPath[] = "path";
|
||
static const char kDomain[] = "domain";
|
||
static const char kExpires[] = "expires";
|
||
static const char kMaxage[] = "max-age";
|
||
static const char kSecure[] = "secure";
|
||
static const char kHttpOnly[] = "httponly";
|
||
static const char kSameSite[] = "samesite";
|
||
static const char kSameSiteLax[] = "lax";
|
||
static const char kSameSiteNone[] = "none";
|
||
static const char kSameSiteStrict[] = "strict";
|
||
|
||
nsACString::const_char_iterator tempBegin, tempEnd;
|
||
nsACString::const_char_iterator cookieStart, cookieEnd;
|
||
aCookieHeader.BeginReading(cookieStart);
|
||
aCookieHeader.EndReading(cookieEnd);
|
||
|
||
aCookieData.isSecure() = false;
|
||
aCookieData.isHttpOnly() = false;
|
||
aCookieData.sameSite() = nsICookie::SAMESITE_NONE;
|
||
aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE;
|
||
|
||
if (StaticPrefs::network_cookie_sameSite_laxByDefault()) {
|
||
aCookieData.sameSite() = nsICookie::SAMESITE_LAX;
|
||
}
|
||
|
||
nsDependentCSubstring tokenString(cookieStart, cookieStart);
|
||
nsDependentCSubstring tokenValue(cookieStart, cookieStart);
|
||
bool newCookie, equalsFound;
|
||
|
||
// extract cookie <NAME> & <VALUE> (first attribute), and copy the strings.
|
||
// if we find multiple cookies, return for processing
|
||
// note: if there's no '=', we assume token is <VALUE>. this is required by
|
||
// some sites (see bug 169091).
|
||
// XXX fix the parser to parse according to <VALUE> grammar for this case
|
||
newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
|
||
equalsFound);
|
||
if (equalsFound) {
|
||
aCookieData.name() = tokenString;
|
||
aCookieData.value() = tokenValue;
|
||
} else {
|
||
aCookieData.value() = tokenString;
|
||
}
|
||
|
||
// extract remaining attributes
|
||
while (cookieStart != cookieEnd && !newCookie) {
|
||
newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
|
||
equalsFound);
|
||
|
||
if (!tokenValue.IsEmpty()) {
|
||
tokenValue.BeginReading(tempBegin);
|
||
tokenValue.EndReading(tempEnd);
|
||
}
|
||
|
||
// decide which attribute we have, and copy the string
|
||
if (tokenString.LowerCaseEqualsLiteral(kPath))
|
||
aCookieData.path() = tokenValue;
|
||
|
||
else if (tokenString.LowerCaseEqualsLiteral(kDomain))
|
||
aCookieData.host() = tokenValue;
|
||
|
||
else if (tokenString.LowerCaseEqualsLiteral(kExpires))
|
||
aExpires = tokenValue;
|
||
|
||
else if (tokenString.LowerCaseEqualsLiteral(kMaxage))
|
||
aMaxage = tokenValue;
|
||
|
||
// ignore any tokenValue for isSecure; just set the boolean
|
||
else if (tokenString.LowerCaseEqualsLiteral(kSecure))
|
||
aCookieData.isSecure() = true;
|
||
|
||
// ignore any tokenValue for isHttpOnly (see bug 178993);
|
||
// just set the boolean
|
||
else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly))
|
||
aCookieData.isHttpOnly() = true;
|
||
|
||
else if (tokenString.LowerCaseEqualsLiteral(kSameSite)) {
|
||
if (tokenValue.LowerCaseEqualsLiteral(kSameSiteLax)) {
|
||
aCookieData.sameSite() = nsICookie::SAMESITE_LAX;
|
||
aCookieData.rawSameSite() = nsICookie::SAMESITE_LAX;
|
||
} else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteStrict)) {
|
||
aCookieData.sameSite() = nsICookie::SAMESITE_STRICT;
|
||
aCookieData.rawSameSite() = nsICookie::SAMESITE_STRICT;
|
||
} else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteNone)) {
|
||
aCookieData.sameSite() = nsICookie::SAMESITE_NONE;
|
||
aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE;
|
||
}
|
||
}
|
||
}
|
||
|
||
// re-assign aCookieHeader, in case we need to process another cookie
|
||
aCookieHeader.Assign(Substring(cookieStart, cookieEnd));
|
||
|
||
// If same-site is set to 'none' but this is not a secure context, let's abort
|
||
// the parsing.
|
||
if (StaticPrefs::network_cookie_sameSite_laxByDefault() &&
|
||
StaticPrefs::network_cookie_sameSite_noneRequiresSecure() &&
|
||
!aCookieData.isSecure() &&
|
||
aCookieData.sameSite() == nsICookie::SAMESITE_NONE) {
|
||
return newCookie;
|
||
}
|
||
|
||
// Cookie accepted.
|
||
aAcceptedByParser = true;
|
||
|
||
MOZ_ASSERT(nsCookie::ValidateRawSame(aCookieData));
|
||
return newCookie;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* private domain & permission compliance enforcement functions
|
||
******************************************************************************/
|
||
|
||
// Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be
|
||
// "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing
|
||
// dot may be present. If aHostURI is an IP address, an alias such as
|
||
// 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will
|
||
// be the exact host, and aRequireHostMatch will be true to indicate that
|
||
// substring matches should not be performed.
|
||
nsresult nsCookieService::GetBaseDomain(nsIEffectiveTLDService* aTLDService,
|
||
nsIURI* aHostURI,
|
||
nsCString& aBaseDomain,
|
||
bool& aRequireHostMatch) {
|
||
// get the base domain. this will fail if the host contains a leading dot,
|
||
// more than one trailing dot, or is otherwise malformed.
|
||
nsresult rv = aTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain);
|
||
aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
|
||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
|
||
if (aRequireHostMatch) {
|
||
// aHostURI is either an IP address, an alias such as 'localhost', an eTLD
|
||
// such as 'co.uk', or the empty string. use the host as a key in such
|
||
// cases.
|
||
rv = aHostURI->GetAsciiHost(aBaseDomain);
|
||
}
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// aHost (and thus aBaseDomain) may be the string '.'. If so, fail.
|
||
if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.')
|
||
return NS_ERROR_INVALID_ARG;
|
||
|
||
// block any URIs without a host that aren't file:// URIs.
|
||
if (aBaseDomain.IsEmpty() && !aHostURI->SchemeIs("file")) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
// Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be
|
||
// "bbc.co.uk". This is done differently than GetBaseDomain(mTLDService, ): it
|
||
// is assumed that aHost is already normalized, and it may contain a leading dot
|
||
// (indicating that it represents a domain). A trailing dot may be present.
|
||
// If aHost is an IP address, an alias such as 'localhost', an eTLD such as
|
||
// 'co.uk', or the empty string, aBaseDomain will be the exact host, and a
|
||
// leading dot will be treated as an error.
|
||
nsresult nsCookieService::GetBaseDomainFromHost(
|
||
nsIEffectiveTLDService* aTLDService, const nsACString& aHost,
|
||
nsCString& aBaseDomain) {
|
||
// aHost must not be the string '.'.
|
||
if (aHost.Length() == 1 && aHost.Last() == '.') return NS_ERROR_INVALID_ARG;
|
||
|
||
// aHost may contain a leading dot; if so, strip it now.
|
||
bool domain = !aHost.IsEmpty() && aHost.First() == '.';
|
||
|
||
// get the base domain. this will fail if the host contains a leading dot,
|
||
// more than one trailing dot, or is otherwise malformed.
|
||
nsresult rv = aTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0,
|
||
aBaseDomain);
|
||
if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
|
||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
|
||
// aHost is either an IP address, an alias such as 'localhost', an eTLD
|
||
// such as 'co.uk', or the empty string. use the host as a key in such
|
||
// cases; however, we reject any such hosts with a leading dot, since it
|
||
// doesn't make sense for them to be domain cookies.
|
||
if (domain) return NS_ERROR_INVALID_ARG;
|
||
|
||
aBaseDomain = aHost;
|
||
return NS_OK;
|
||
}
|
||
return rv;
|
||
}
|
||
|
||
// Normalizes the given hostname, component by component. ASCII/ACE
|
||
// components are lower-cased, and UTF-8 components are normalized per
|
||
// RFC 3454 and converted to ACE.
|
||
nsresult nsCookieService::NormalizeHost(nsCString& aHost) {
|
||
if (!IsAscii(aHost)) {
|
||
nsAutoCString host;
|
||
nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host);
|
||
if (NS_FAILED(rv)) return rv;
|
||
|
||
aHost = host;
|
||
}
|
||
|
||
ToLowerCase(aHost);
|
||
return NS_OK;
|
||
}
|
||
|
||
// returns true if 'a' is equal to or a subdomain of 'b',
|
||
// assuming no leading dots are present.
|
||
static inline bool IsSubdomainOf(const nsCString& a, const nsCString& b) {
|
||
if (a == b) return true;
|
||
if (a.Length() > b.Length())
|
||
return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b);
|
||
return false;
|
||
}
|
||
|
||
CookieStatus nsCookieService::CheckPrefs(
|
||
nsICookieSettings* aCookieSettings, nsIURI* aHostURI, bool aIsForeign,
|
||
bool aIsTrackingResource, bool aFirstPartyStorageAccessGranted,
|
||
const nsACString& aCookieHeader, const int aNumOfCookies,
|
||
const OriginAttributes& aOriginAttrs, uint32_t* aRejectedReason) {
|
||
nsresult rv;
|
||
|
||
MOZ_ASSERT(aRejectedReason);
|
||
|
||
*aRejectedReason = 0;
|
||
|
||
// don't let ftp sites get/set cookies (could be a security issue)
|
||
if (aHostURI->SchemeIs("ftp")) {
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader, "ftp sites cannot read cookies");
|
||
return STATUS_REJECTED_WITH_ERROR;
|
||
}
|
||
|
||
nsCOMPtr<nsIPrincipal> principal =
|
||
BasePrincipal::CreateContentPrincipal(aHostURI, aOriginAttrs);
|
||
|
||
if (!principal) {
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader,
|
||
"non-content principals cannot get/set cookies");
|
||
return STATUS_REJECTED_WITH_ERROR;
|
||
}
|
||
|
||
// check the permission list first; if we find an entry, it overrides
|
||
// default prefs. see bug 184059.
|
||
uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT;
|
||
rv = aCookieSettings->CookiePermission(principal, &cookiePermission);
|
||
if (NS_SUCCEEDED(rv)) {
|
||
switch (cookiePermission) {
|
||
case nsICookiePermission::ACCESS_DENY:
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader,
|
||
"cookies are blocked for this site");
|
||
*aRejectedReason =
|
||
nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION;
|
||
return STATUS_REJECTED;
|
||
|
||
case nsICookiePermission::ACCESS_ALLOW:
|
||
return STATUS_ACCEPTED;
|
||
}
|
||
}
|
||
|
||
// No cookies allowed if this request comes from a tracker, in a 3rd party
|
||
// context, when anti-tracking protection is enabled and when we don't have
|
||
// access to the first-party cookie jar.
|
||
if (aIsForeign && aIsTrackingResource && !aFirstPartyStorageAccessGranted &&
|
||
aCookieSettings->GetRejectThirdPartyTrackers()) {
|
||
// Explicitly pass nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER
|
||
// here to ensure that we are testing the partitioning configuration only
|
||
// for the nsICookieService::BEHAVIOR_REJECT_TRACKER configuration.
|
||
// When partitioning for BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, we
|
||
// don't want to give a free pass to tracker cookies here!
|
||
if (StoragePartitioningEnabled(
|
||
nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER,
|
||
aCookieSettings)) {
|
||
MOZ_ASSERT(!aOriginAttrs.mFirstPartyDomain.IsEmpty(),
|
||
"We must have a StoragePrincipal here!");
|
||
return STATUS_ACCEPTED;
|
||
}
|
||
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader,
|
||
"cookies are disabled in trackers");
|
||
*aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER;
|
||
return STATUS_REJECTED;
|
||
}
|
||
|
||
// check default prefs.
|
||
// Check aFirstPartyStorageAccessGranted when checking aCookieBehavior
|
||
// so that we take things such as the content blocking allow list into
|
||
// account.
|
||
if (aCookieSettings->GetCookieBehavior() ==
|
||
nsICookieService::BEHAVIOR_REJECT &&
|
||
!aFirstPartyStorageAccessGranted) {
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader, "cookies are disabled");
|
||
*aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL;
|
||
return STATUS_REJECTED;
|
||
}
|
||
|
||
// check if cookie is foreign
|
||
if (aIsForeign) {
|
||
if (aCookieSettings->GetCookieBehavior() ==
|
||
nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
|
||
!aFirstPartyStorageAccessGranted) {
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader, "context is third party");
|
||
*aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
|
||
return STATUS_REJECTED;
|
||
}
|
||
|
||
if (aCookieSettings->GetCookieBehavior() ==
|
||
nsICookieService::BEHAVIOR_LIMIT_FOREIGN &&
|
||
!aFirstPartyStorageAccessGranted && aNumOfCookies == 0) {
|
||
COOKIE_LOGFAILURE(aCookieHeader.IsVoid() ? GET_COOKIE : SET_COOKIE,
|
||
aHostURI, aCookieHeader, "context is third party");
|
||
*aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
|
||
return STATUS_REJECTED;
|
||
}
|
||
|
||
if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) {
|
||
return STATUS_ACCEPT_SESSION;
|
||
}
|
||
|
||
if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly()) {
|
||
if (!aHostURI->SchemeIs("https")) {
|
||
return STATUS_ACCEPT_SESSION;
|
||
}
|
||
}
|
||
}
|
||
|
||
// if nothing has complained, accept cookie
|
||
return STATUS_ACCEPTED;
|
||
}
|
||
|
||
// processes domain attribute, and returns true if host has permission to set
|
||
// for this domain.
|
||
bool nsCookieService::CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI,
|
||
const nsCString& aBaseDomain,
|
||
bool aRequireHostMatch) {
|
||
// Note: The logic in this function is mirrored in
|
||
// toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions().
|
||
// If it changes, please update that function, or file a bug for someone
|
||
// else to do so.
|
||
|
||
// get host from aHostURI
|
||
nsAutoCString hostFromURI;
|
||
aHostURI->GetAsciiHost(hostFromURI);
|
||
|
||
// if a domain is given, check the host has permission
|
||
if (!aCookieData.host().IsEmpty()) {
|
||
// Tolerate leading '.' characters, but not if it's otherwise an empty host.
|
||
if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') {
|
||
aCookieData.host().Cut(0, 1);
|
||
}
|
||
|
||
// switch to lowercase now, to avoid case-insensitive compares everywhere
|
||
ToLowerCase(aCookieData.host());
|
||
|
||
// check whether the host is either an IP address, an alias such as
|
||
// 'localhost', an eTLD such as 'co.uk', or the empty string. in these
|
||
// cases, require an exact string match for the domain, and leave the cookie
|
||
// as a non-domain one. bug 105917 originally noted the requirement to deal
|
||
// with IP addresses.
|
||
if (aRequireHostMatch) return hostFromURI.Equals(aCookieData.host());
|
||
|
||
// ensure the proposed domain is derived from the base domain; and also
|
||
// that the host domain is derived from the proposed domain (per RFC2109).
|
||
if (IsSubdomainOf(aCookieData.host(), aBaseDomain) &&
|
||
IsSubdomainOf(hostFromURI, aCookieData.host())) {
|
||
// prepend a dot to indicate a domain cookie
|
||
aCookieData.host().InsertLiteral(".", 0);
|
||
return true;
|
||
}
|
||
|
||
/*
|
||
* note: RFC2109 section 4.3.2 requires that we check the following:
|
||
* that the portion of host not in domain does not contain a dot.
|
||
* this prevents hosts of the form x.y.co.nz from setting cookies in the
|
||
* entire .co.nz domain. however, it's only a only a partial solution and
|
||
* it breaks sites (IE doesn't enforce it), so we don't perform this check.
|
||
*/
|
||
return false;
|
||
}
|
||
|
||
// no domain specified, use hostFromURI
|
||
aCookieData.host() = hostFromURI;
|
||
return true;
|
||
}
|
||
|
||
nsAutoCString nsCookieService::GetPathFromURI(nsIURI* aHostURI) {
|
||
// strip down everything after the last slash to get the path,
|
||
// ignoring slashes in the query string part.
|
||
// if we can QI to nsIURL, that'll take care of the query string portion.
|
||
// otherwise, it's not an nsIURL and can't have a query string, so just find
|
||
// the last slash.
|
||
nsAutoCString path;
|
||
nsCOMPtr<nsIURL> hostURL = do_QueryInterface(aHostURI);
|
||
if (hostURL) {
|
||
hostURL->GetDirectory(path);
|
||
} else {
|
||
aHostURI->GetPathQueryRef(path);
|
||
int32_t slash = path.RFindChar('/');
|
||
if (slash != kNotFound) {
|
||
path.Truncate(slash + 1);
|
||
}
|
||
}
|
||
|
||
// strip the right-most %x2F ("/") if the path doesn't contain only 1 '/'.
|
||
int32_t lastSlash = path.RFindChar('/');
|
||
int32_t firstSlash = path.FindChar('/');
|
||
if (lastSlash != firstSlash && lastSlash != kNotFound &&
|
||
lastSlash == (int32_t)(path.Length() - 1)) {
|
||
path.Truncate(lastSlash);
|
||
}
|
||
|
||
return path;
|
||
}
|
||
|
||
bool nsCookieService::CheckPath(CookieStruct& aCookieData, nsIURI* aHostURI) {
|
||
// if a path is given, check the host has permission
|
||
if (aCookieData.path().IsEmpty() || aCookieData.path().First() != '/') {
|
||
aCookieData.path() = GetPathFromURI(aHostURI);
|
||
|
||
#if 0
|
||
} else {
|
||
/**
|
||
* The following test is part of the RFC2109 spec. Loosely speaking, it says that a site
|
||
* cannot set a cookie for a path that it is not on. See bug 155083. However this patch
|
||
* broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has
|
||
* been disabled, unless we can evangelize these sites.
|
||
*/
|
||
// get path from aHostURI
|
||
nsAutoCString pathFromURI;
|
||
if (NS_FAILED(aHostURI->GetPathQueryRef(pathFromURI)) ||
|
||
!StringBeginsWith(pathFromURI, aCookieData.path())) {
|
||
return false;
|
||
}
|
||
#endif
|
||
}
|
||
|
||
if (aCookieData.path().Length() > kMaxBytesPerPath ||
|
||
aCookieData.path().Contains('\t'))
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
// CheckPrefixes
|
||
//
|
||
// Reject cookies whose name starts with the magic prefixes from
|
||
// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00
|
||
// if they do not meet the criteria required by the prefix.
|
||
//
|
||
// Must not be called until after CheckDomain() and CheckPath() have
|
||
// regularized and validated the CookieStruct values!
|
||
bool nsCookieService::CheckPrefixes(CookieStruct& aCookieData,
|
||
bool aSecureRequest) {
|
||
static const char kSecure[] = "__Secure-";
|
||
static const char kHost[] = "__Host-";
|
||
static const int kSecureLen = sizeof(kSecure) - 1;
|
||
static const int kHostLen = sizeof(kHost) - 1;
|
||
|
||
bool isSecure = strncmp(aCookieData.name().get(), kSecure, kSecureLen) == 0;
|
||
bool isHost = strncmp(aCookieData.name().get(), kHost, kHostLen) == 0;
|
||
|
||
if (!isSecure && !isHost) {
|
||
// not one of the magic prefixes: carry on
|
||
return true;
|
||
}
|
||
|
||
if (!aSecureRequest || !aCookieData.isSecure()) {
|
||
// the magic prefixes may only be used from a secure request and
|
||
// the secure attribute must be set on the cookie
|
||
return false;
|
||
}
|
||
|
||
if (isHost) {
|
||
// The host prefix requires that the path is "/" and that the cookie
|
||
// had no domain attribute. CheckDomain() and CheckPath() MUST be run
|
||
// first to make sure invalid attributes are rejected and to regularlize
|
||
// them. In particular all explicit domain attributes result in a host
|
||
// that starts with a dot, and if the host doesn't start with a dot it
|
||
// correctly matches the true host.
|
||
if (aCookieData.host()[0] == '.' ||
|
||
!aCookieData.path().EqualsLiteral("/")) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool nsCookieService::GetExpiry(CookieStruct& aCookieData,
|
||
const nsACString& aExpires,
|
||
const nsACString& aMaxage, int64_t aServerTime,
|
||
int64_t aCurrentTime, bool aFromHttp) {
|
||
// maxageCap is in seconds.
|
||
// Disabled for HTTP cookies.
|
||
int64_t maxageCap =
|
||
aFromHttp ? 0 : StaticPrefs::privacy_documentCookies_maxage();
|
||
|
||
/* Determine when the cookie should expire. This is done by taking the
|
||
* difference between the server time and the time the server wants the cookie
|
||
* to expire, and adding that difference to the client time. This localizes
|
||
* the client time regardless of whether or not the TZ environment variable
|
||
* was set on the client.
|
||
*
|
||
* Note: We need to consider accounting for network lag here, per RFC.
|
||
*/
|
||
// check for max-age attribute first; this overrides expires attribute
|
||
if (!aMaxage.IsEmpty()) {
|
||
// obtain numeric value of maxageAttribute
|
||
int64_t maxage;
|
||
int32_t numInts = PR_sscanf(aMaxage.BeginReading(), "%lld", &maxage);
|
||
|
||
// default to session cookie if the conversion failed
|
||
if (numInts != 1) {
|
||
return true;
|
||
}
|
||
|
||
// if this addition overflows, expiryTime will be less than currentTime
|
||
// and the cookie will be expired - that's okay.
|
||
if (maxageCap) {
|
||
aCookieData.expiry() = aCurrentTime + std::min(maxage, maxageCap);
|
||
} else {
|
||
aCookieData.expiry() = aCurrentTime + maxage;
|
||
}
|
||
|
||
// check for expires attribute
|
||
} else if (!aExpires.IsEmpty()) {
|
||
PRTime expires;
|
||
|
||
// parse expiry time
|
||
if (PR_ParseTimeString(aExpires.BeginReading(), true, &expires) !=
|
||
PR_SUCCESS) {
|
||
return true;
|
||
}
|
||
|
||
// If set-cookie used absolute time to set expiration, and it can't use
|
||
// client time to set expiration.
|
||
// Because if current time be set in the future, but the cookie expire
|
||
// time be set less than current time and more than server time.
|
||
// The cookie item have to be used to the expired cookie.
|
||
if (maxageCap) {
|
||
aCookieData.expiry() = std::min(expires / int64_t(PR_USEC_PER_SEC),
|
||
aCurrentTime + maxageCap);
|
||
} else {
|
||
aCookieData.expiry() = expires / int64_t(PR_USEC_PER_SEC);
|
||
}
|
||
|
||
// default to session cookie if no attributes found. Here we don't need to
|
||
// enforce the maxage cap, because session cookies are short-lived by
|
||
// definition.
|
||
} else {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/******************************************************************************
|
||
* nsCookieService impl:
|
||
* private cookielist management functions
|
||
******************************************************************************/
|
||
|
||
void nsCookieService::RemoveAllFromMemory() {
|
||
// clearing the hashtable will call each nsCookieEntry's dtor,
|
||
// which releases all their respective children.
|
||
mDBState->hostTable.Clear();
|
||
mDBState->cookieCount = 0;
|
||
mDBState->cookieOldestTime = INT64_MAX;
|
||
}
|
||
|
||
// comparator class for lastaccessed times of cookies.
|
||
class CompareCookiesByAge {
|
||
public:
|
||
bool Equals(const nsListIter& a, const nsListIter& b) const {
|
||
return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
|
||
a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
|
||
}
|
||
|
||
bool LessThan(const nsListIter& a, const nsListIter& b) const {
|
||
// compare by lastAccessed time, and tiebreak by creationTime.
|
||
int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
|
||
if (result != 0) return result < 0;
|
||
|
||
return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
|
||
}
|
||
};
|
||
|
||
// purges expired and old cookies in a batch operation.
|
||
already_AddRefed<nsIArray> nsCookieService::PurgeCookies(
|
||
int64_t aCurrentTimeInUsec) {
|
||
NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty");
|
||
|
||
uint32_t initialCookieCount = mDBState->cookieCount;
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Debug,
|
||
("PurgeCookies(): beginning purge with %" PRIu32 " cookies and %" PRId64
|
||
" oldest age",
|
||
mDBState->cookieCount, aCurrentTimeInUsec - mDBState->cookieOldestTime));
|
||
|
||
typedef nsTArray<nsListIter> PurgeList;
|
||
PurgeList purgeList(kMaxNumberOfCookies);
|
||
|
||
nsCOMPtr<nsIMutableArray> removedList =
|
||
do_CreateInstance(NS_ARRAY_CONTRACTID);
|
||
|
||
// Create a params array to batch the removals. This is OK here because
|
||
// all the removals are in order, and there are no interleaved additions.
|
||
mozIStorageAsyncStatement* stmt = mDBState->stmtDelete;
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
|
||
if (mDBState->dbConn) {
|
||
stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
|
||
}
|
||
|
||
int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
|
||
int64_t purgeTime = aCurrentTimeInUsec - mCookiePurgeAge;
|
||
int64_t oldestTime = INT64_MAX;
|
||
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
nsCookieEntry* entry = iter.Get();
|
||
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
auto length = cookies.Length();
|
||
for (nsCookieEntry::IndexType i = 0; i < length;) {
|
||
nsListIter iter(entry, i);
|
||
nsCookie* cookie = cookies[i];
|
||
|
||
// check if the cookie has expired
|
||
if (cookie->Expiry() <= currentTime) {
|
||
removedList->AppendElement(cookie);
|
||
COOKIE_LOGEVICTED(cookie, "Cookie expired");
|
||
|
||
// remove from list; do not increment our iterator, but stop if we're
|
||
// done already.
|
||
gCookieService->RemoveCookieFromList(iter, paramsArray);
|
||
if (i == --length) {
|
||
break;
|
||
}
|
||
} else {
|
||
// check if the cookie is over the age limit
|
||
if (cookie->LastAccessed() <= purgeTime) {
|
||
purgeList.AppendElement(iter);
|
||
|
||
} else if (cookie->LastAccessed() < oldestTime) {
|
||
// reset our indicator
|
||
oldestTime = cookie->LastAccessed();
|
||
}
|
||
|
||
++i;
|
||
}
|
||
MOZ_ASSERT(length == cookies.Length());
|
||
}
|
||
}
|
||
|
||
uint32_t postExpiryCookieCount = mDBState->cookieCount;
|
||
|
||
// now we have a list of iterators for cookies over the age limit.
|
||
// sort them by age, and then we'll see how many to remove...
|
||
purgeList.Sort(CompareCookiesByAge());
|
||
|
||
// only remove old cookies until we reach the max cookie limit, no more.
|
||
uint32_t excess = mDBState->cookieCount > mMaxNumberOfCookies
|
||
? mDBState->cookieCount - mMaxNumberOfCookies
|
||
: 0;
|
||
if (purgeList.Length() > excess) {
|
||
// We're not purging everything in the list, so update our indicator.
|
||
oldestTime = purgeList[excess].Cookie()->LastAccessed();
|
||
|
||
purgeList.SetLength(excess);
|
||
}
|
||
|
||
// sort the list again, this time grouping cookies with a common entryclass
|
||
// together, and with ascending index. this allows us to iterate backwards
|
||
// over the list removing cookies, without having to adjust indexes as we go.
|
||
purgeList.Sort(CompareCookiesByIndex());
|
||
for (PurgeList::index_type i = purgeList.Length(); i--;) {
|
||
nsCookie* cookie = purgeList[i].Cookie();
|
||
removedList->AppendElement(cookie);
|
||
COOKIE_LOGEVICTED(cookie, "Cookie too old");
|
||
|
||
RemoveCookieFromList(purgeList[i], paramsArray);
|
||
}
|
||
|
||
// Update the database if we have entries to purge.
|
||
if (paramsArray) {
|
||
uint32_t length;
|
||
paramsArray->GetLength(&length);
|
||
if (length) {
|
||
DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
|
||
// reset the oldest time indicator
|
||
mDBState->cookieOldestTime = oldestTime;
|
||
|
||
COOKIE_LOGSTRING(
|
||
LogLevel::Debug,
|
||
("PurgeCookies(): %" PRIu32 " expired; %" PRIu32 " purged; %" PRIu32
|
||
" remain; %" PRId64 " oldest age",
|
||
initialCookieCount - postExpiryCookieCount,
|
||
postExpiryCookieCount - mDBState->cookieCount, mDBState->cookieCount,
|
||
aCurrentTimeInUsec - mDBState->cookieOldestTime));
|
||
|
||
return removedList.forget();
|
||
}
|
||
|
||
// find whether a given cookie has been previously set. this is provided by the
|
||
// nsICookieManager interface.
|
||
NS_IMETHODIMP
|
||
nsCookieService::CookieExists(const nsACString& aHost, const nsACString& aPath,
|
||
const nsACString& aName,
|
||
JS::HandleValue aOriginAttributes, JSContext* aCx,
|
||
bool* aFoundCookie) {
|
||
NS_ENSURE_ARG_POINTER(aCx);
|
||
NS_ENSURE_ARG_POINTER(aFoundCookie);
|
||
|
||
OriginAttributes attrs;
|
||
if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
return CookieExistsNative(aHost, aPath, aName, &attrs, aFoundCookie);
|
||
}
|
||
|
||
NS_IMETHODIMP_(nsresult)
|
||
nsCookieService::CookieExistsNative(const nsACString& aHost,
|
||
const nsACString& aPath,
|
||
const nsACString& aName,
|
||
OriginAttributes* aOriginAttributes,
|
||
bool* aFoundCookie) {
|
||
NS_ENSURE_ARG_POINTER(aOriginAttributes);
|
||
NS_ENSURE_ARG_POINTER(aFoundCookie);
|
||
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState
|
||
: mDefaultDBState;
|
||
|
||
nsAutoCString baseDomain;
|
||
nsresult rv = GetBaseDomainFromHost(mTLDService, aHost, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsListIter iter;
|
||
*aFoundCookie = FindCookie(
|
||
nsCookieKey(baseDomain, *aOriginAttributes), PromiseFlatCString(aHost),
|
||
PromiseFlatCString(aName), PromiseFlatCString(aPath), iter);
|
||
return NS_OK;
|
||
}
|
||
|
||
// Cookie comparator for the priority queue used in FindStaleCookies.
|
||
// Note that the expired cookie has the highest priority.
|
||
// Other non-expired cookies are sorted by their age.
|
||
class CookieIterComparator {
|
||
private:
|
||
CompareCookiesByAge mAgeComparator;
|
||
int64_t mCurrentTime;
|
||
|
||
public:
|
||
explicit CookieIterComparator(int64_t aTime) : mCurrentTime(aTime) {}
|
||
|
||
bool LessThan(const nsListIter& lhs, const nsListIter& rhs) {
|
||
bool lExpired = lhs.Cookie()->Expiry() <= mCurrentTime;
|
||
bool rExpired = rhs.Cookie()->Expiry() <= mCurrentTime;
|
||
if (lExpired && !rExpired) {
|
||
return true;
|
||
}
|
||
|
||
if (!lExpired && rExpired) {
|
||
return false;
|
||
}
|
||
|
||
return mAgeComparator.LessThan(lhs, rhs);
|
||
}
|
||
};
|
||
|
||
// Given the output iter array and the count limit, find cookies
|
||
// sort by expiry and lastAccessed time.
|
||
void nsCookieService::FindStaleCookies(nsCookieEntry* aEntry,
|
||
int64_t aCurrentTime, bool aIsSecure,
|
||
nsTArray<nsListIter>& aOutput,
|
||
uint32_t aLimit) {
|
||
MOZ_ASSERT(aLimit);
|
||
|
||
const nsCookieEntry::ArrayType& cookies = aEntry->GetCookies();
|
||
aOutput.Clear();
|
||
|
||
CookieIterComparator comp(aCurrentTime);
|
||
nsTPriorityQueue<nsListIter, CookieIterComparator> queue(comp);
|
||
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
nsCookie* cookie = cookies[i];
|
||
|
||
if (cookie->Expiry() <= aCurrentTime) {
|
||
queue.Push(nsListIter(aEntry, i));
|
||
continue;
|
||
}
|
||
|
||
if (!aIsSecure) {
|
||
// We want to look for the non-secure cookie first time through,
|
||
// then find the secure cookie the second time this function is called.
|
||
if (cookie->IsSecure()) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
queue.Push(nsListIter(aEntry, i));
|
||
}
|
||
|
||
uint32_t count = 0;
|
||
while (!queue.IsEmpty() && count < aLimit) {
|
||
aOutput.AppendElement(queue.Pop());
|
||
count++;
|
||
}
|
||
}
|
||
|
||
// count the number of cookies stored by a particular host. this is provided by
|
||
// the nsICookieManager interface.
|
||
NS_IMETHODIMP
|
||
nsCookieService::CountCookiesFromHost(const nsACString& aHost,
|
||
uint32_t* aCountFromHost) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
// first, normalize the hostname, and fail if it contains illegal characters.
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
|
||
|
||
// Return a count of all cookies, including expired.
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(key);
|
||
*aCountFromHost = entry ? entry->GetCookies().Length() : 0;
|
||
return NS_OK;
|
||
}
|
||
|
||
// get an enumerator of cookies stored by a particular host. this is provided by
|
||
// the nsICookieManager interface.
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetCookiesFromHost(const nsACString& aHost,
|
||
JS::HandleValue aOriginAttributes,
|
||
JSContext* aCx,
|
||
nsISimpleEnumerator** aEnumerator) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
// first, normalize the hostname, and fail if it contains illegal characters.
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
OriginAttributes attrs;
|
||
if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (attrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
|
||
|
||
nsCookieKey key = nsCookieKey(baseDomain, attrs);
|
||
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(key);
|
||
if (!entry) return NS_NewEmptyEnumerator(aEnumerator);
|
||
|
||
nsCOMArray<nsICookie> cookieList(mMaxCookiesPerHost);
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
cookieList.AppendObject(cookies[i]);
|
||
}
|
||
|
||
return NS_NewArrayEnumerator(aEnumerator, cookieList, NS_GET_IID(nsICookie));
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::GetCookiesWithOriginAttributes(
|
||
const nsAString& aPattern, const nsACString& aHost,
|
||
nsISimpleEnumerator** aEnumerator) {
|
||
mozilla::OriginAttributesPattern pattern;
|
||
if (!pattern.Init(aPattern)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
return GetCookiesWithOriginAttributes(pattern, baseDomain, aEnumerator);
|
||
}
|
||
|
||
nsresult nsCookieService::GetCookiesWithOriginAttributes(
|
||
const mozilla::OriginAttributesPattern& aPattern,
|
||
const nsCString& aBaseDomain, nsISimpleEnumerator** aEnumerator) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already closed?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (aPattern.mPrivateBrowsingId.WasPassed() &&
|
||
aPattern.mPrivateBrowsingId.Value() > 0)
|
||
? mPrivateDBState
|
||
: mDefaultDBState;
|
||
|
||
nsCOMArray<nsICookie> cookies;
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
nsCookieEntry* entry = iter.Get();
|
||
|
||
if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
|
||
continue;
|
||
}
|
||
|
||
if (!aPattern.Matches(entry->mOriginAttributes)) {
|
||
continue;
|
||
}
|
||
|
||
const nsCookieEntry::ArrayType& entryCookies = entry->GetCookies();
|
||
|
||
for (nsCookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
|
||
cookies.AppendObject(entryCookies[i]);
|
||
}
|
||
}
|
||
|
||
return NS_NewArrayEnumerator(aEnumerator, cookies, NS_GET_IID(nsICookie));
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::RemoveCookiesWithOriginAttributes(const nsAString& aPattern,
|
||
const nsACString& aHost) {
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
|
||
mozilla::OriginAttributesPattern pattern;
|
||
if (!pattern.Init(aPattern)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
return RemoveCookiesWithOriginAttributes(pattern, baseDomain);
|
||
}
|
||
|
||
nsresult nsCookieService::RemoveCookiesWithOriginAttributes(
|
||
const mozilla::OriginAttributesPattern& aPattern,
|
||
const nsCString& aBaseDomain) {
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already close?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (aPattern.mPrivateBrowsingId.WasPassed() &&
|
||
aPattern.mPrivateBrowsingId.Value() > 0)
|
||
? mPrivateDBState
|
||
: mDefaultDBState;
|
||
|
||
mozStorageTransaction transaction(mDBState->dbConn, false);
|
||
// Iterate the hash table of nsCookieEntry.
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
nsCookieEntry* entry = iter.Get();
|
||
|
||
if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
|
||
continue;
|
||
}
|
||
|
||
if (!aPattern.Matches(entry->mOriginAttributes)) {
|
||
continue;
|
||
}
|
||
|
||
// Pattern matches. Delete all cookies within this nsCookieEntry.
|
||
uint32_t cookiesCount = entry->GetCookies().Length();
|
||
|
||
for (nsCookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
|
||
// Remove the first cookie from the list.
|
||
nsListIter iter(entry, 0);
|
||
RefPtr<nsCookie> cookie = iter.Cookie();
|
||
|
||
// Remove the cookie.
|
||
RemoveCookieFromList(iter);
|
||
|
||
if (cookie) {
|
||
NotifyChanged(cookie, u"deleted");
|
||
}
|
||
}
|
||
}
|
||
DebugOnly<nsresult> rv = transaction.Commit();
|
||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::RemoveCookiesFromExactHost(const nsACString& aHost,
|
||
const nsAString& aPattern) {
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
|
||
mozilla::OriginAttributesPattern pattern;
|
||
if (!pattern.Init(aPattern)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
return RemoveCookiesFromExactHost(aHost, pattern);
|
||
}
|
||
|
||
nsresult nsCookieService::RemoveCookiesFromExactHost(
|
||
const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) {
|
||
nsAutoCString host(aHost);
|
||
nsresult rv = NormalizeHost(host);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoCString baseDomain;
|
||
rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
if (!mDBState) {
|
||
NS_WARNING("No DBState! Profile already close?");
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
EnsureReadComplete(true);
|
||
|
||
AutoRestore<DBState*> savePrevDBState(mDBState);
|
||
mDBState = (aPattern.mPrivateBrowsingId.WasPassed() &&
|
||
aPattern.mPrivateBrowsingId.Value() > 0)
|
||
? mPrivateDBState
|
||
: mDefaultDBState;
|
||
|
||
mozStorageTransaction transaction(mDBState->dbConn, false);
|
||
// Iterate the hash table of nsCookieEntry.
|
||
for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
|
||
nsCookieEntry* entry = iter.Get();
|
||
|
||
if (!baseDomain.Equals(entry->mBaseDomain)) {
|
||
continue;
|
||
}
|
||
|
||
if (!aPattern.Matches(entry->mOriginAttributes)) {
|
||
continue;
|
||
}
|
||
|
||
uint32_t cookiesCount = entry->GetCookies().Length();
|
||
for (nsCookieEntry::IndexType i = cookiesCount; i != 0; --i) {
|
||
nsListIter iter(entry, i - 1);
|
||
RefPtr<nsCookie> cookie = iter.Cookie();
|
||
|
||
if (!aHost.Equals(cookie->RawHost())) {
|
||
continue;
|
||
}
|
||
|
||
// Remove the cookie.
|
||
RemoveCookieFromList(iter);
|
||
|
||
if (cookie) {
|
||
NotifyChanged(cookie, u"deleted");
|
||
}
|
||
}
|
||
}
|
||
rv = transaction.Commit();
|
||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
// find an secure cookie specified by host and name
|
||
bool nsCookieService::FindSecureCookie(const nsCookieKey& aKey,
|
||
nsCookie* aCookie) {
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(aKey);
|
||
if (!entry) return false;
|
||
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
nsCookie* cookie = cookies[i];
|
||
// isn't a match if insecure or a different name
|
||
if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name()))
|
||
continue;
|
||
|
||
// The host must "domain-match" an existing cookie or vice-versa
|
||
if (DomainMatches(cookie, aCookie->Host()) ||
|
||
DomainMatches(aCookie, cookie->Host())) {
|
||
// If the path of new cookie and the path of existing cookie
|
||
// aren't "/", then this situation needs to compare paths to
|
||
// ensure only that a newly-created non-secure cookie does not
|
||
// overlay an existing secure cookie.
|
||
if (PathMatches(cookie, aCookie->GetFilePath())) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// find an exact cookie specified by host, name, and path that hasn't expired.
|
||
bool nsCookieService::FindCookie(const nsCookieKey& aKey,
|
||
const nsCString& aHost, const nsCString& aName,
|
||
const nsCString& aPath, nsListIter& aIter) {
|
||
// Should |EnsureReadComplete| before.
|
||
MOZ_ASSERT(mInitializedDBStates);
|
||
MOZ_ASSERT(mInitializedDBConn);
|
||
|
||
nsCookieEntry* entry = mDBState->hostTable.GetEntry(aKey);
|
||
if (!entry) return false;
|
||
|
||
const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
|
||
for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
|
||
nsCookie* cookie = cookies[i];
|
||
|
||
if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
|
||
aName.Equals(cookie->Name())) {
|
||
aIter = nsListIter(entry, i);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// remove a cookie from the hashtable, and update the iterator state.
|
||
void nsCookieService::RemoveCookieFromList(
|
||
const nsListIter& aIter, mozIStorageBindingParamsArray* aParamsArray) {
|
||
// if it's a non-session cookie, remove it from the db
|
||
if (!aIter.Cookie()->IsSession() && mDBState->dbConn) {
|
||
// Use the asynchronous binding methods to ensure that we do not acquire
|
||
// the database lock.
|
||
mozIStorageAsyncStatement* stmt = mDBState->stmtDelete;
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
|
||
if (!paramsArray) {
|
||
stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
|
||
}
|
||
|
||
nsCOMPtr<mozIStorageBindingParams> params;
|
||
paramsArray->NewBindingParams(getter_AddRefs(params));
|
||
|
||
DebugOnly<nsresult> rv = params->BindUTF8StringByName(
|
||
NS_LITERAL_CSTRING("name"), aIter.Cookie()->Name());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
|
||
aIter.Cookie()->Host());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
|
||
aIter.Cookie()->Path());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
nsAutoCString suffix;
|
||
aIter.Cookie()->OriginAttributesRef().CreateSuffix(suffix);
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
|
||
suffix);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = paramsArray->AddParams(params);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
// If we weren't given a params array, we'll need to remove it ourselves.
|
||
if (!aParamsArray) {
|
||
rv = stmt->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
|
||
if (aIter.entry->GetCookies().Length() == 1) {
|
||
// we're removing the last element in the array - so just remove the entry
|
||
// from the hash. note that the entryclass' dtor will take care of
|
||
// releasing this last element for us!
|
||
mDBState->hostTable.RawRemoveEntry(aIter.entry);
|
||
|
||
} else {
|
||
// just remove the element from the list
|
||
aIter.entry->GetCookies().RemoveElementAt(aIter.index);
|
||
}
|
||
|
||
--mDBState->cookieCount;
|
||
}
|
||
|
||
void bindCookieParameters(mozIStorageBindingParamsArray* aParamsArray,
|
||
const nsCookieKey& aKey, const nsCookie* aCookie) {
|
||
NS_ASSERTION(aParamsArray,
|
||
"Null params array passed to bindCookieParameters!");
|
||
NS_ASSERTION(aCookie, "Null cookie passed to bindCookieParameters!");
|
||
|
||
// Use the asynchronous binding methods to ensure that we do not acquire the
|
||
// database lock.
|
||
nsCOMPtr<mozIStorageBindingParams> params;
|
||
DebugOnly<nsresult> rv =
|
||
aParamsArray->NewBindingParams(getter_AddRefs(params));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
// Bind our values to params
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
|
||
aKey.mBaseDomain);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
nsAutoCString suffix;
|
||
aKey.mOriginAttributes.CreateSuffix(suffix);
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
|
||
suffix);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv =
|
||
params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), aCookie->Name());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("value"),
|
||
aCookie->Value());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv =
|
||
params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), aCookie->Host());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv =
|
||
params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), aCookie->Path());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt64ByName(NS_LITERAL_CSTRING("expiry"), aCookie->Expiry());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"),
|
||
aCookie->LastAccessed());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt64ByName(NS_LITERAL_CSTRING("creationTime"),
|
||
aCookie->CreationTime());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isSecure"),
|
||
aCookie->IsSecure());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isHttpOnly"),
|
||
aCookie->IsHttpOnly());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt32ByName(NS_LITERAL_CSTRING("sameSite"),
|
||
aCookie->SameSite());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindInt32ByName(NS_LITERAL_CSTRING("rawSameSite"),
|
||
aCookie->RawSameSite());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
// Bind the params to the array.
|
||
rv = aParamsArray->AddParams(params);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
|
||
void nsCookieService::UpdateCookieOldestTime(DBState* aDBState,
|
||
nsCookie* aCookie) {
|
||
if (aCookie->LastAccessed() < aDBState->cookieOldestTime) {
|
||
aDBState->cookieOldestTime = aCookie->LastAccessed();
|
||
}
|
||
}
|
||
|
||
void nsCookieService::AddCookieToList(
|
||
const nsCookieKey& aKey, nsCookie* aCookie, DBState* aDBState,
|
||
mozIStorageBindingParamsArray* aParamsArray, bool aWriteToDB) {
|
||
NS_ASSERTION(!(aDBState->dbConn && !aWriteToDB && aParamsArray),
|
||
"Not writing to the DB but have a params array?");
|
||
NS_ASSERTION(!(!aDBState->dbConn && aParamsArray),
|
||
"Do not have a DB connection but have a params array?");
|
||
|
||
if (!aCookie) {
|
||
NS_WARNING("Attempting to AddCookieToList with null cookie");
|
||
return;
|
||
}
|
||
|
||
nsCookieEntry* entry = aDBState->hostTable.PutEntry(aKey);
|
||
NS_ASSERTION(entry, "can't insert element into a null entry!");
|
||
|
||
entry->GetCookies().AppendElement(aCookie);
|
||
++aDBState->cookieCount;
|
||
|
||
// keep track of the oldest cookie, for when it comes time to purge
|
||
UpdateCookieOldestTime(aDBState, aCookie);
|
||
|
||
// if it's a non-session cookie and hasn't just been read from the db, write
|
||
// it out.
|
||
if (aWriteToDB && !aCookie->IsSession() && aDBState->dbConn) {
|
||
mozIStorageAsyncStatement* stmt = aDBState->stmtInsert;
|
||
nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
|
||
if (!paramsArray) {
|
||
stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
|
||
}
|
||
bindCookieParameters(paramsArray, aKey, aCookie);
|
||
|
||
// If we were supplied an array to store parameters, we shouldn't call
|
||
// executeAsync - someone up the stack will do this for us.
|
||
if (!aParamsArray) {
|
||
DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
nsCOMPtr<mozIStoragePendingStatement> handle;
|
||
rv = stmt->ExecuteAsync(mDBState->insertListener, getter_AddRefs(handle));
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
}
|
||
|
||
void nsCookieService::UpdateCookieInList(
|
||
nsCookie* aCookie, int64_t aLastAccessed,
|
||
mozIStorageBindingParamsArray* aParamsArray) {
|
||
NS_ASSERTION(aCookie, "Passing a null cookie to UpdateCookieInList!");
|
||
|
||
// udpate the lastAccessed timestamp
|
||
aCookie->SetLastAccessed(aLastAccessed);
|
||
|
||
// if it's a non-session cookie, update it in the db too
|
||
if (!aCookie->IsSession() && aParamsArray) {
|
||
// Create our params holder.
|
||
nsCOMPtr<mozIStorageBindingParams> params;
|
||
aParamsArray->NewBindingParams(getter_AddRefs(params));
|
||
|
||
// Bind our parameters.
|
||
DebugOnly<nsresult> rv = params->BindInt64ByName(
|
||
NS_LITERAL_CSTRING("lastAccessed"), aLastAccessed);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
|
||
aCookie->Name());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
|
||
aCookie->Host());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
|
||
aCookie->Path());
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
nsAutoCString suffix;
|
||
aCookie->OriginAttributesRef().CreateSuffix(suffix);
|
||
rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
|
||
suffix);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
|
||
// Add our bound parameters to the array.
|
||
rv = aParamsArray->AddParams(params);
|
||
NS_ASSERT_SUCCESS(rv);
|
||
}
|
||
}
|
||
|
||
size_t nsCookieService::SizeOfIncludingThis(
|
||
mozilla::MallocSizeOf aMallocSizeOf) const {
|
||
size_t n = aMallocSizeOf(this);
|
||
|
||
if (mDefaultDBState) {
|
||
n += mDefaultDBState->SizeOfIncludingThis(aMallocSizeOf);
|
||
}
|
||
if (mPrivateDBState) {
|
||
n += mPrivateDBState->SizeOfIncludingThis(aMallocSizeOf);
|
||
}
|
||
|
||
return n;
|
||
}
|
||
|
||
MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf)
|
||
|
||
NS_IMETHODIMP
|
||
nsCookieService::CollectReports(nsIHandleReportCallback* aHandleReport,
|
||
nsISupports* aData, bool aAnonymize) {
|
||
MOZ_COLLECT_REPORT("explicit/cookie-service", KIND_HEAP, UNITS_BYTES,
|
||
SizeOfIncludingThis(CookieServiceMallocSizeOf),
|
||
"Memory used by the cookie service.");
|
||
|
||
return NS_OK;
|
||
}
|