Bug 1869043 assert that graph set access is main thread only r=padenot
[gecko.git] / netwerk / cookie / CookieStorage.cpp
blobcc827a237271d6b65f7429a41c29e7fd467e92f6
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "Cookie.h"
7 #include "CookieCommons.h"
8 #include "CookieLogging.h"
9 #include "CookieNotification.h"
10 #include "nsCOMPtr.h"
11 #include "nsICookieNotification.h"
12 #include "CookieStorage.h"
13 #include "mozilla/dom/nsMixedContentBlocker.h"
14 #include "mozilla/glean/GleanMetrics.h"
15 #include "nsIMutableArray.h"
16 #include "nsTPriorityQueue.h"
17 #include "nsIScriptError.h"
18 #include "nsIUserIdleService.h"
19 #include "nsServiceManagerUtils.h"
20 #include "nsComponentManagerUtils.h"
21 #include "prprf.h"
22 #include "nsIPrefService.h"
24 #undef ADD_TEN_PERCENT
25 #define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
27 #undef LIMIT
28 #define LIMIT(x, low, high, default) \
29 ((x) >= (low) && (x) <= (high) ? (x) : (default))
31 namespace mozilla {
32 namespace net {
34 namespace {
36 // comparator class for lastaccessed times of cookies.
37 class CompareCookiesByAge {
38 public:
39 static bool Equals(const CookieListIter& a, const CookieListIter& b) {
40 return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
41 a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
44 static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
45 // compare by lastAccessed time, and tiebreak by creationTime.
46 int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
47 if (result != 0) {
48 return result < 0;
51 return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
55 // Cookie comparator for the priority queue used in FindStaleCookies.
56 // Note that the expired cookie has the highest priority.
57 // Other non-expired cookies are sorted by their age.
58 class CookieIterComparator {
59 private:
60 int64_t mCurrentTime;
62 public:
63 explicit CookieIterComparator(int64_t aTime) : mCurrentTime(aTime) {}
65 bool LessThan(const CookieListIter& lhs, const CookieListIter& rhs) {
66 bool lExpired = lhs.Cookie()->Expiry() <= mCurrentTime;
67 bool rExpired = rhs.Cookie()->Expiry() <= mCurrentTime;
68 if (lExpired && !rExpired) {
69 return true;
72 if (!lExpired && rExpired) {
73 return false;
76 return mozilla::net::CompareCookiesByAge::LessThan(lhs, rhs);
80 // comparator class for sorting cookies by entry and index.
81 class CompareCookiesByIndex {
82 public:
83 static bool Equals(const CookieListIter& a, const CookieListIter& b) {
84 NS_ASSERTION(a.entry != b.entry || a.index != b.index,
85 "cookie indexes should never be equal");
86 return false;
89 static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
90 // compare by entryclass pointer, then by index.
91 if (a.entry != b.entry) {
92 return a.entry < b.entry;
95 return a.index < b.index;
99 } // namespace
101 // ---------------------------------------------------------------------------
102 // CookieEntry
104 size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
105 size_t amount = CookieKey::SizeOfExcludingThis(aMallocSizeOf);
107 amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
108 for (uint32_t i = 0; i < mCookies.Length(); ++i) {
109 amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
112 return amount;
115 bool CookieEntry::IsPartitioned() const {
116 return !mOriginAttributes.mPartitionKey.IsEmpty();
119 // ---------------------------------------------------------------------------
120 // CookieStorage
122 NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference)
124 void CookieStorage::Init() {
125 // init our pref and observer
126 nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
127 if (prefBranch) {
128 prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
129 prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
130 prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
131 PrefChanged(prefBranch);
134 nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
135 NS_ENSURE_TRUE_VOID(observerService);
137 nsresult rv =
138 observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, true);
139 NS_ENSURE_SUCCESS_VOID(rv);
142 size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
143 size_t amount = 0;
145 amount += aMallocSizeOf(this);
146 amount += mHostTable.SizeOfExcludingThis(aMallocSizeOf);
148 return amount;
151 void CookieStorage::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const {
152 aCookies.SetCapacity(mCookieCount);
153 for (const auto& entry : mHostTable) {
154 const CookieEntry::ArrayType& cookies = entry.GetCookies();
155 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
156 aCookies.AppendElement(cookies[i]);
161 void CookieStorage::GetSessionCookies(
162 nsTArray<RefPtr<nsICookie>>& aCookies) const {
163 aCookies.SetCapacity(mCookieCount);
164 for (const auto& entry : mHostTable) {
165 const CookieEntry::ArrayType& cookies = entry.GetCookies();
166 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
167 Cookie* cookie = cookies[i];
168 // Filter out non-session cookies.
169 if (cookie->IsSession()) {
170 aCookies.AppendElement(cookie);
176 // find an exact cookie specified by host, name, and path that hasn't expired.
177 bool CookieStorage::FindCookie(const nsACString& aBaseDomain,
178 const OriginAttributes& aOriginAttributes,
179 const nsACString& aHost, const nsACString& aName,
180 const nsACString& aPath, CookieListIter& aIter) {
181 CookieEntry* entry =
182 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
183 if (!entry) {
184 return false;
187 const CookieEntry::ArrayType& cookies = entry->GetCookies();
188 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
189 Cookie* cookie = cookies[i];
191 if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
192 aName.Equals(cookie->Name())) {
193 aIter = CookieListIter(entry, i);
194 return true;
198 return false;
201 // find an secure cookie specified by host and name
202 bool CookieStorage::FindSecureCookie(const nsACString& aBaseDomain,
203 const OriginAttributes& aOriginAttributes,
204 Cookie* aCookie) {
205 CookieEntry* entry =
206 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
207 if (!entry) {
208 return false;
211 const CookieEntry::ArrayType& cookies = entry->GetCookies();
212 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
213 Cookie* cookie = cookies[i];
214 // isn't a match if insecure or a different name
215 if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name())) {
216 continue;
219 // The host must "domain-match" an existing cookie or vice-versa
220 if (CookieCommons::DomainMatches(cookie, aCookie->Host()) ||
221 CookieCommons::DomainMatches(aCookie, cookie->Host())) {
222 // If the path of new cookie and the path of existing cookie
223 // aren't "/", then this situation needs to compare paths to
224 // ensure only that a newly-created non-secure cookie does not
225 // overlay an existing secure cookie.
226 if (CookieCommons::PathMatches(cookie, aCookie->GetFilePath())) {
227 return true;
232 return false;
235 uint32_t CookieStorage::CountCookiesFromHost(const nsACString& aBaseDomain,
236 uint32_t aPrivateBrowsingId) {
237 OriginAttributes attrs;
238 attrs.mPrivateBrowsingId = aPrivateBrowsingId;
240 // Return a count of all cookies, including expired.
241 CookieEntry* entry = mHostTable.GetEntry(CookieKey(aBaseDomain, attrs));
242 return entry ? entry->GetCookies().Length() : 0;
245 void CookieStorage::GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const {
246 aResult.SetCapacity(mCookieCount);
248 for (const auto& entry : mHostTable) {
249 const CookieEntry::ArrayType& cookies = entry.GetCookies();
250 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
251 aResult.AppendElement(cookies[i]);
256 const nsTArray<RefPtr<Cookie>>* CookieStorage::GetCookiesFromHost(
257 const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes) {
258 CookieEntry* entry =
259 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
260 return entry ? &entry->GetCookies() : nullptr;
263 void CookieStorage::GetCookiesWithOriginAttributes(
264 const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain,
265 nsTArray<RefPtr<nsICookie>>& aResult) {
266 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
267 CookieEntry* entry = iter.Get();
269 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
270 continue;
273 if (!aPattern.Matches(entry->mOriginAttributes)) {
274 continue;
277 const CookieEntry::ArrayType& entryCookies = entry->GetCookies();
279 for (CookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
280 aResult.AppendElement(entryCookies[i]);
285 void CookieStorage::RemoveCookie(const nsACString& aBaseDomain,
286 const OriginAttributes& aOriginAttributes,
287 const nsACString& aHost,
288 const nsACString& aName,
289 const nsACString& aPath) {
290 CookieListIter matchIter{};
291 RefPtr<Cookie> cookie;
292 if (FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath,
293 matchIter)) {
294 cookie = matchIter.Cookie();
295 RemoveCookieFromList(matchIter);
298 if (cookie) {
299 // Everything's done. Notify observers.
300 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, aBaseDomain);
304 void CookieStorage::RemoveCookiesWithOriginAttributes(
305 const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) {
306 // Iterate the hash table of CookieEntry.
307 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
308 CookieEntry* entry = iter.Get();
310 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
311 continue;
314 if (!aPattern.Matches(entry->mOriginAttributes)) {
315 continue;
318 // Pattern matches. Delete all cookies within this CookieEntry.
319 uint32_t cookiesCount = entry->GetCookies().Length();
321 for (CookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
322 // Remove the first cookie from the list.
323 CookieListIter iter(entry, 0);
324 RefPtr<Cookie> cookie = iter.Cookie();
326 // Remove the cookie.
327 RemoveCookieFromList(iter);
329 if (cookie) {
330 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
331 aBaseDomain);
337 void CookieStorage::RemoveCookiesFromExactHost(
338 const nsACString& aHost, const nsACString& aBaseDomain,
339 const OriginAttributesPattern& aPattern) {
340 // Iterate the hash table of CookieEntry.
341 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
342 CookieEntry* entry = iter.Get();
344 if (!aBaseDomain.Equals(entry->mBaseDomain)) {
345 continue;
348 if (!aPattern.Matches(entry->mOriginAttributes)) {
349 continue;
352 uint32_t cookiesCount = entry->GetCookies().Length();
353 for (CookieEntry::IndexType i = cookiesCount; i != 0; --i) {
354 CookieListIter iter(entry, i - 1);
355 RefPtr<Cookie> cookie = iter.Cookie();
357 if (!aHost.Equals(cookie->RawHost())) {
358 continue;
361 // Remove the cookie.
362 RemoveCookieFromList(iter);
364 if (cookie) {
365 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
366 aBaseDomain);
372 void CookieStorage::RemoveAll() {
373 // clearing the hashtable will call each CookieEntry's dtor,
374 // which releases all their respective children.
375 mHostTable.Clear();
376 mCookieCount = 0;
377 mCookieOldestTime = INT64_MAX;
379 RemoveAllInternal();
381 NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED, ""_ns);
384 // notify observers that the cookie list changed.
385 void CookieStorage::NotifyChanged(nsISupports* aSubject,
386 nsICookieNotification::Action aAction,
387 const nsACString& aBaseDomain,
388 dom::BrowsingContext* aBrowsingContext,
389 bool aOldCookieIsSession) {
390 nsCOMPtr<nsIObserverService> os = services::GetObserverService();
391 if (!os) {
392 return;
395 nsCOMPtr<nsICookie> cookie;
396 nsCOMPtr<nsIArray> batchDeletedCookies;
398 if (aAction == nsICookieNotification::COOKIES_BATCH_DELETED) {
399 batchDeletedCookies = do_QueryInterface(aSubject);
400 } else {
401 cookie = do_QueryInterface(aSubject);
404 uint64_t browsingContextId = 0;
405 if (aBrowsingContext) {
406 browsingContextId = aBrowsingContext->Id();
409 nsCOMPtr<nsICookieNotification> notification = new CookieNotification(
410 aAction, cookie, aBaseDomain, batchDeletedCookies, browsingContextId);
411 // Notify for topic "private-cookie-changed" or "cookie-changed"
412 os->NotifyObservers(notification, NotificationTopic(), u"");
414 NotifyChangedInternal(notification, aOldCookieIsSession);
417 // this is a backend function for adding a cookie to the list, via SetCookie.
418 // also used in the cookie manager, for profile migration from IE. it either
419 // replaces an existing cookie; or adds the cookie to the hashtable, and
420 // deletes a cookie (if maximum number of cookies has been reached). also
421 // performs list maintenance by removing expired cookies.
422 void CookieStorage::AddCookie(nsIConsoleReportCollector* aCRC,
423 const nsACString& aBaseDomain,
424 const OriginAttributes& aOriginAttributes,
425 Cookie* aCookie, int64_t aCurrentTimeInUsec,
426 nsIURI* aHostURI, const nsACString& aCookieHeader,
427 bool aFromHttp,
428 dom::BrowsingContext* aBrowsingContext) {
429 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
431 CookieListIter exactIter{};
432 bool foundCookie = false;
433 foundCookie = FindCookie(aBaseDomain, aOriginAttributes, aCookie->Host(),
434 aCookie->Name(), aCookie->Path(), exactIter);
435 bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
436 bool potentiallyTrustworthy = true;
437 if (aHostURI) {
438 potentiallyTrustworthy =
439 nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
441 constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns;
442 bool oldCookieIsSession = false;
443 // Step1, call FindSecureCookie(). FindSecureCookie() would
444 // find the existing cookie with the security flag and has
445 // the same name, host and path of the new cookie, if there is any.
446 // Step2, Confirm new cookie's security setting. If any targeted
447 // cookie had been found in Step1, then confirm whether the
448 // new cookie could modify it. If the new created cookie’s
449 // "secure-only-flag" is not set, and the "scheme" component
450 // of the "request-uri" does not denote a "secure" protocol,
451 // then ignore the new cookie.
452 // (draft-ietf-httpbis-cookie-alone section 3.2)
453 if (!aCookie->IsSecure() &&
454 (foundSecureExact ||
455 FindSecureCookie(aBaseDomain, aOriginAttributes, aCookie)) &&
456 !potentiallyTrustworthy) {
457 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
458 "cookie can't save because older cookie is secure "
459 "cookie but newer cookie is non-secure cookie");
460 CookieLogging::LogMessageToConsole(
461 aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
462 "CookieRejectedNonsecureOverSecure"_ns,
463 AutoTArray<nsString, 1>{
464 NS_ConvertUTF8toUTF16(aCookie->Name()),
466 return;
469 RefPtr<Cookie> oldCookie;
470 nsCOMPtr<nsIArray> purgedList;
471 if (foundCookie) {
472 oldCookie = exactIter.Cookie();
473 oldCookieIsSession = oldCookie->IsSession();
475 // Check if the old cookie is stale (i.e. has already expired). If so, we
476 // need to be careful about the semantics of removing it and adding the new
477 // cookie: we want the behavior wrt adding the new cookie to be the same as
478 // if it didn't exist, but we still want to fire a removal notification.
479 if (oldCookie->Expiry() <= currentTime) {
480 if (aCookie->Expiry() <= currentTime) {
481 // The new cookie has expired and the old one is stale. Nothing to do.
482 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
483 "cookie has already expired");
484 return;
487 // Remove the stale cookie. We save notification for later, once all list
488 // modifications are complete.
489 RemoveCookieFromList(exactIter);
490 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
491 "stale cookie was purged");
492 purgedList = CreatePurgeList(oldCookie);
494 // We've done all we need to wrt removing and notifying the stale cookie.
495 // From here on out, we pretend pretend it didn't exist, so that we
496 // preserve expected notification semantics when adding the new cookie.
497 foundCookie = false;
499 } else {
500 // If the old cookie is httponly, make sure we're not coming from script.
501 if (!aFromHttp && oldCookie->IsHttpOnly()) {
502 COOKIE_LOGFAILURE(
503 SET_COOKIE, aHostURI, aCookieHeader,
504 "previously stored cookie is httponly; coming from script");
505 CookieLogging::LogMessageToConsole(
506 aCRC, aHostURI, nsIScriptError::warningFlag,
507 CONSOLE_REJECTION_CATEGORY,
508 "CookieRejectedHttpOnlyButFromScript"_ns,
509 AutoTArray<nsString, 1>{
510 NS_ConvertUTF8toUTF16(aCookie->Name()),
512 return;
515 // If the new cookie has the same value, expiry date, isSecure, isSession,
516 // isHttpOnly and SameSite flags then we can just keep the old one.
517 // Only if any of these differ we would want to override the cookie.
518 if (oldCookie->Value().Equals(aCookie->Value()) &&
519 oldCookie->Expiry() == aCookie->Expiry() &&
520 oldCookie->IsSecure() == aCookie->IsSecure() &&
521 oldCookie->IsSession() == aCookie->IsSession() &&
522 oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
523 oldCookie->SameSite() == aCookie->SameSite() &&
524 oldCookie->RawSameSite() == aCookie->RawSameSite() &&
525 oldCookie->SchemeMap() == aCookie->SchemeMap() &&
526 // We don't want to perform this optimization if the cookie is
527 // considered stale, since in this case we would need to update the
528 // database.
529 !oldCookie->IsStale()) {
530 // Update the last access time on the old cookie.
531 oldCookie->SetLastAccessed(aCookie->LastAccessed());
532 UpdateCookieOldestTime(oldCookie);
533 return;
536 // Merge the scheme map in case the old cookie and the new cookie are
537 // used with different schemes.
538 MergeCookieSchemeMap(oldCookie, aCookie);
540 // Remove the old cookie.
541 RemoveCookieFromList(exactIter);
543 // If the new cookie has expired -- i.e. the intent was simply to delete
544 // the old cookie -- then we're done.
545 if (aCookie->Expiry() <= currentTime) {
546 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
547 "previously stored cookie was deleted");
548 NotifyChanged(oldCookie, nsICookieNotification::COOKIE_DELETED,
549 aBaseDomain, aBrowsingContext, oldCookieIsSession);
550 return;
553 // Preserve creation time of cookie for ordering purposes.
554 aCookie->SetCreationTime(oldCookie->CreationTime());
557 } else {
558 // check if cookie has already expired
559 if (aCookie->Expiry() <= currentTime) {
560 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
561 "cookie has already expired");
562 return;
565 // check if we have to delete an old cookie.
566 CookieEntry* entry =
567 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
568 if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
569 nsTArray<CookieListIter> removedIterList;
570 // Prioritize evicting insecure cookies.
571 // (draft-ietf-httpbis-cookie-alone section 3.3)
572 uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost;
573 FindStaleCookies(entry, currentTime, false, removedIterList, limit);
574 if (removedIterList.Length() == 0) {
575 if (aCookie->IsSecure()) {
576 // It's valid to evict a secure cookie for another secure cookie.
577 FindStaleCookies(entry, currentTime, true, removedIterList, limit);
578 } else {
579 COOKIE_LOGEVICTED(aCookie,
580 "Too many cookies for this domain and the new "
581 "cookie is not a secure cookie");
582 return;
586 MOZ_ASSERT(!removedIterList.IsEmpty());
587 // Sort |removedIterList| by index again, since we have to remove the
588 // cookie in the reverse order.
589 removedIterList.Sort(CompareCookiesByIndex());
590 for (auto it = removedIterList.rbegin(); it != removedIterList.rend();
591 it++) {
592 RefPtr<Cookie> evictedCookie = (*it).Cookie();
593 COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain");
594 RemoveCookieFromList(*it);
595 CreateOrUpdatePurgeList(purgedList, evictedCookie);
596 MOZ_ASSERT((*it).entry);
598 uint32_t purgedLength = 0;
599 purgedList->GetLength(&purgedLength);
600 mozilla::glean::networking::cookie_purge_entry_max.AccumulateSamples(
601 {purgedLength});
603 } else if (mCookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
604 int64_t maxAge = aCurrentTimeInUsec - mCookieOldestTime;
605 int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
606 if (maxAge >= purgeAge) {
607 // we're over both size and age limits by 10%; time to purge the table!
608 // do this by:
609 // 1) removing expired cookies;
610 // 2) evicting the balance of old cookies until we reach the size limit.
611 // note that the mCookieOldestTime indicator can be pessimistic - if
612 // it's older than the actual oldest cookie, we'll just purge more
613 // eagerly.
614 purgedList = PurgeCookies(aCurrentTimeInUsec, mMaxNumberOfCookies,
615 mCookiePurgeAge);
616 uint32_t purgedLength = 0;
617 purgedList->GetLength(&purgedLength);
618 mozilla::glean::networking::cookie_purge_max.AccumulateSamples(
619 {purgedLength});
624 // Add the cookie to the db. We do not supply a params array for batching
625 // because this might result in removals and additions being out of order.
626 AddCookieToList(aBaseDomain, aOriginAttributes, aCookie);
627 StoreCookie(aBaseDomain, aOriginAttributes, aCookie);
629 COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
631 // Now that list mutations are complete, notify observers. We do it here
632 // because observers may themselves attempt to mutate the list.
633 if (purgedList) {
634 NotifyChanged(purgedList, nsICookieNotification::COOKIES_BATCH_DELETED,
635 ""_ns);
638 // Notify for topic "private-cookie-changed" or "cookie-changed"
639 NotifyChanged(aCookie,
640 foundCookie ? nsICookieNotification::COOKIE_CHANGED
641 : nsICookieNotification::COOKIE_ADDED,
642 aBaseDomain, aBrowsingContext, oldCookieIsSession);
645 void CookieStorage::UpdateCookieOldestTime(Cookie* aCookie) {
646 if (aCookie->LastAccessed() < mCookieOldestTime) {
647 mCookieOldestTime = aCookie->LastAccessed();
651 void CookieStorage::MergeCookieSchemeMap(Cookie* aOldCookie,
652 Cookie* aNewCookie) {
653 aNewCookie->SetSchemeMap(aOldCookie->SchemeMap() | aNewCookie->SchemeMap());
656 void CookieStorage::AddCookieToList(const nsACString& aBaseDomain,
657 const OriginAttributes& aOriginAttributes,
658 Cookie* aCookie) {
659 if (!aCookie) {
660 NS_WARNING("Attempting to AddCookieToList with null cookie");
661 return;
664 CookieKey key(aBaseDomain, aOriginAttributes);
666 CookieEntry* entry = mHostTable.PutEntry(key);
667 NS_ASSERTION(entry, "can't insert element into a null entry!");
669 entry->GetCookies().AppendElement(aCookie);
670 ++mCookieCount;
672 // keep track of the oldest cookie, for when it comes time to purge
673 UpdateCookieOldestTime(aCookie);
676 // static
677 already_AddRefed<nsIArray> CookieStorage::CreatePurgeList(nsICookie* aCookie) {
678 nsCOMPtr<nsIMutableArray> removedList =
679 do_CreateInstance(NS_ARRAY_CONTRACTID);
680 removedList->AppendElement(aCookie);
681 return removedList.forget();
684 // Given the output iter array and the count limit, find cookies
685 // sort by expiry and lastAccessed time.
686 // static
687 void CookieStorage::FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime,
688 bool aIsSecure,
689 nsTArray<CookieListIter>& aOutput,
690 uint32_t aLimit) {
691 MOZ_ASSERT(aLimit);
693 const CookieEntry::ArrayType& cookies = aEntry->GetCookies();
694 aOutput.Clear();
696 CookieIterComparator comp(aCurrentTime);
697 nsTPriorityQueue<CookieListIter, CookieIterComparator> queue(comp);
699 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
700 Cookie* cookie = cookies[i];
702 if (cookie->Expiry() <= aCurrentTime) {
703 queue.Push(CookieListIter(aEntry, i));
704 continue;
707 if (!aIsSecure) {
708 // We want to look for the non-secure cookie first time through,
709 // then find the secure cookie the second time this function is called.
710 if (cookie->IsSecure()) {
711 continue;
715 queue.Push(CookieListIter(aEntry, i));
718 uint32_t count = 0;
719 while (!queue.IsEmpty() && count < aLimit) {
720 aOutput.AppendElement(queue.Pop());
721 count++;
725 // static
726 void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList,
727 nsICookie* aCookie) {
728 if (!aPurgedList) {
729 COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list"));
730 aPurgedList = CreatePurgeList(aCookie);
731 return;
734 nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(aPurgedList);
735 if (purgedList) {
736 COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list"));
737 purgedList->AppendElement(aCookie);
738 } else {
739 COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!"));
743 // purges expired and old cookies in a batch operation.
744 already_AddRefed<nsIArray> CookieStorage::PurgeCookiesWithCallbacks(
745 int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
746 int64_t aCookiePurgeAge,
747 std::function<void(const CookieListIter&)>&& aRemoveCookieCallback,
748 std::function<void()>&& aFinalizeCallback) {
749 NS_ASSERTION(mHostTable.Count() > 0, "table is empty");
751 uint32_t initialCookieCount = mCookieCount;
752 COOKIE_LOGSTRING(LogLevel::Debug,
753 ("PurgeCookies(): beginning purge with %" PRIu32
754 " cookies and %" PRId64 " oldest age",
755 mCookieCount, aCurrentTimeInUsec - mCookieOldestTime));
757 using PurgeList = nsTArray<CookieListIter>;
758 PurgeList purgeList(kMaxNumberOfCookies);
760 nsCOMPtr<nsIMutableArray> removedList =
761 do_CreateInstance(NS_ARRAY_CONTRACTID);
763 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
764 int64_t purgeTime = aCurrentTimeInUsec - aCookiePurgeAge;
765 int64_t oldestTime = INT64_MAX;
767 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
768 CookieEntry* entry = iter.Get();
770 const CookieEntry::ArrayType& cookies = entry->GetCookies();
771 auto length = cookies.Length();
772 for (CookieEntry::IndexType i = 0; i < length;) {
773 CookieListIter iter(entry, i);
774 Cookie* cookie = cookies[i];
776 // check if the cookie has expired
777 if (cookie->Expiry() <= currentTime) {
778 removedList->AppendElement(cookie);
779 COOKIE_LOGEVICTED(cookie, "Cookie expired");
781 // remove from list; do not increment our iterator, but stop if we're
782 // done already.
783 aRemoveCookieCallback(iter);
784 if (i == --length) {
785 break;
787 } else {
788 // check if the cookie is over the age limit
789 if (cookie->LastAccessed() <= purgeTime) {
790 purgeList.AppendElement(iter);
792 } else if (cookie->LastAccessed() < oldestTime) {
793 // reset our indicator
794 oldestTime = cookie->LastAccessed();
797 ++i;
799 MOZ_ASSERT(length == cookies.Length());
803 uint32_t postExpiryCookieCount = mCookieCount;
805 // now we have a list of iterators for cookies over the age limit.
806 // sort them by age, and then we'll see how many to remove...
807 purgeList.Sort(CompareCookiesByAge());
809 // only remove old cookies until we reach the max cookie limit, no more.
810 uint32_t excess = mCookieCount > aMaxNumberOfCookies
811 ? mCookieCount - aMaxNumberOfCookies
812 : 0;
813 if (purgeList.Length() > excess) {
814 // We're not purging everything in the list, so update our indicator.
815 oldestTime = purgeList[excess].Cookie()->LastAccessed();
817 purgeList.SetLength(excess);
820 // sort the list again, this time grouping cookies with a common entryclass
821 // together, and with ascending index. this allows us to iterate backwards
822 // over the list removing cookies, without having to adjust indexes as we go.
823 purgeList.Sort(CompareCookiesByIndex());
824 for (PurgeList::index_type i = purgeList.Length(); i--;) {
825 Cookie* cookie = purgeList[i].Cookie();
826 removedList->AppendElement(cookie);
827 COOKIE_LOGEVICTED(cookie, "Cookie too old");
829 aRemoveCookieCallback(purgeList[i]);
832 // Update the database if we have entries to purge.
833 if (aFinalizeCallback) {
834 aFinalizeCallback();
837 // reset the oldest time indicator
838 mCookieOldestTime = oldestTime;
840 COOKIE_LOGSTRING(LogLevel::Debug,
841 ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32
842 " purged; %" PRIu32 " remain; %" PRId64 " oldest age",
843 initialCookieCount - postExpiryCookieCount,
844 postExpiryCookieCount - mCookieCount, mCookieCount,
845 aCurrentTimeInUsec - mCookieOldestTime));
847 return removedList.forget();
850 // remove a cookie from the hashtable, and update the iterator state.
851 void CookieStorage::RemoveCookieFromList(const CookieListIter& aIter) {
852 RemoveCookieFromDB(*aIter.Cookie());
853 RemoveCookieFromListInternal(aIter);
856 void CookieStorage::RemoveCookieFromListInternal(const CookieListIter& aIter) {
857 if (aIter.entry->GetCookies().Length() == 1) {
858 // we're removing the last element in the array - so just remove the entry
859 // from the hash. note that the entryclass' dtor will take care of
860 // releasing this last element for us!
861 mHostTable.RawRemoveEntry(aIter.entry);
863 } else {
864 // just remove the element from the list
865 aIter.entry->GetCookies().RemoveElementAt(aIter.index);
868 --mCookieCount;
871 void CookieStorage::PrefChanged(nsIPrefBranch* aPrefBranch) {
872 int32_t val;
873 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) {
874 mMaxNumberOfCookies =
875 static_cast<uint16_t> LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
878 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) {
879 mCookieQuotaPerHost = static_cast<uint16_t> LIMIT(
880 val, 1, mMaxCookiesPerHost - 1, kCookieQuotaPerHost);
883 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) {
884 mMaxCookiesPerHost = static_cast<uint16_t> LIMIT(
885 val, mCookieQuotaPerHost + 1, 0xFFFF, kMaxCookiesPerHost);
888 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
889 mCookiePurgeAge =
890 int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
894 NS_IMETHODIMP
895 CookieStorage::Observe(nsISupports* aSubject, const char* aTopic,
896 const char16_t* /*aData*/) {
897 if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
898 nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
899 if (prefBranch) {
900 PrefChanged(prefBranch);
902 } else if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
903 CollectCookieJarSizeData();
906 return NS_OK;
909 } // namespace net
910 } // namespace mozilla