Bug 1807268 - Re-enable verifyShowClipboardSuggestionsToggleTest UI test r=jajohnson
[gecko.git] / netwerk / cookie / CookieStorage.cpp
blobef4af82b6422e7e3f7186fc890912de3585676c5
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 "mozilla/net/MozURL_ffi.h"
11 #include "nsCOMPtr.h"
12 #include "nsICookieNotification.h"
13 #include "CookieStorage.h"
14 #include "mozilla/dom/nsMixedContentBlocker.h"
15 #include "mozilla/glean/GleanMetrics.h"
16 #include "nsIMutableArray.h"
17 #include "nsTPriorityQueue.h"
18 #include "nsIScriptError.h"
19 #include "nsIUserIdleService.h"
20 #include "nsServiceManagerUtils.h"
21 #include "nsComponentManagerUtils.h"
22 #include "prprf.h"
23 #include "nsIPrefService.h"
25 #undef ADD_TEN_PERCENT
26 #define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
28 #undef LIMIT
29 #define LIMIT(x, low, high, default) \
30 ((x) >= (low) && (x) <= (high) ? (x) : (default))
32 namespace mozilla {
33 namespace net {
35 namespace {
37 // comparator class for lastaccessed times of cookies.
38 class CompareCookiesByAge {
39 public:
40 static bool Equals(const CookieListIter& a, const CookieListIter& b) {
41 return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
42 a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
45 static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
46 // compare by lastAccessed time, and tiebreak by creationTime.
47 int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
48 if (result != 0) {
49 return result < 0;
52 return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
56 // Cookie comparator for the priority queue used in FindStaleCookies.
57 // Note that the expired cookie has the highest priority.
58 // Other non-expired cookies are sorted by their age.
59 class CookieIterComparator {
60 private:
61 int64_t mCurrentTime;
63 public:
64 explicit CookieIterComparator(int64_t aTime) : mCurrentTime(aTime) {}
66 bool LessThan(const CookieListIter& lhs, const CookieListIter& rhs) {
67 bool lExpired = lhs.Cookie()->Expiry() <= mCurrentTime;
68 bool rExpired = rhs.Cookie()->Expiry() <= mCurrentTime;
69 if (lExpired && !rExpired) {
70 return true;
73 if (!lExpired && rExpired) {
74 return false;
77 return mozilla::net::CompareCookiesByAge::LessThan(lhs, rhs);
81 // comparator class for sorting cookies by entry and index.
82 class CompareCookiesByIndex {
83 public:
84 static bool Equals(const CookieListIter& a, const CookieListIter& b) {
85 NS_ASSERTION(a.entry != b.entry || a.index != b.index,
86 "cookie indexes should never be equal");
87 return false;
90 static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
91 // compare by entryclass pointer, then by index.
92 if (a.entry != b.entry) {
93 return a.entry < b.entry;
96 return a.index < b.index;
100 } // namespace
102 // ---------------------------------------------------------------------------
103 // CookieEntry
105 size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
106 size_t amount = CookieKey::SizeOfExcludingThis(aMallocSizeOf);
108 amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
109 for (uint32_t i = 0; i < mCookies.Length(); ++i) {
110 amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
113 return amount;
116 bool CookieEntry::IsPartitioned() const {
117 return !mOriginAttributes.mPartitionKey.IsEmpty();
120 // ---------------------------------------------------------------------------
121 // CookieStorage
123 NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference)
125 void CookieStorage::Init() {
126 // init our pref and observer
127 nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
128 if (prefBranch) {
129 prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
130 prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
131 prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
132 PrefChanged(prefBranch);
135 nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
136 NS_ENSURE_TRUE_VOID(observerService);
138 nsresult rv =
139 observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, true);
140 NS_ENSURE_SUCCESS_VOID(rv);
143 size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
144 size_t amount = 0;
146 amount += aMallocSizeOf(this);
147 amount += mHostTable.SizeOfExcludingThis(aMallocSizeOf);
149 return amount;
152 void CookieStorage::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const {
153 aCookies.SetCapacity(mCookieCount);
154 for (const auto& entry : mHostTable) {
155 const CookieEntry::ArrayType& cookies = entry.GetCookies();
156 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
157 aCookies.AppendElement(cookies[i]);
162 void CookieStorage::GetSessionCookies(
163 nsTArray<RefPtr<nsICookie>>& aCookies) const {
164 aCookies.SetCapacity(mCookieCount);
165 for (const auto& entry : mHostTable) {
166 const CookieEntry::ArrayType& cookies = entry.GetCookies();
167 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
168 Cookie* cookie = cookies[i];
169 // Filter out non-session cookies.
170 if (cookie->IsSession()) {
171 aCookies.AppendElement(cookie);
177 // find an exact cookie specified by host, name, and path that hasn't expired.
178 bool CookieStorage::FindCookie(const nsACString& aBaseDomain,
179 const OriginAttributes& aOriginAttributes,
180 const nsACString& aHost, const nsACString& aName,
181 const nsACString& aPath, CookieListIter& aIter) {
182 CookieEntry* entry =
183 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
184 if (!entry) {
185 return false;
188 const CookieEntry::ArrayType& cookies = entry->GetCookies();
189 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
190 Cookie* cookie = cookies[i];
192 if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
193 aName.Equals(cookie->Name())) {
194 aIter = CookieListIter(entry, i);
195 return true;
199 return false;
202 // find an secure cookie specified by host and name
203 bool CookieStorage::FindSecureCookie(const nsACString& aBaseDomain,
204 const OriginAttributes& aOriginAttributes,
205 Cookie* aCookie) {
206 CookieEntry* entry =
207 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
208 if (!entry) {
209 return false;
212 const CookieEntry::ArrayType& cookies = entry->GetCookies();
213 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
214 Cookie* cookie = cookies[i];
215 // isn't a match if insecure or a different name
216 if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name())) {
217 continue;
220 // The host must "domain-match" an existing cookie or vice-versa
221 if (CookieCommons::DomainMatches(cookie, aCookie->Host()) ||
222 CookieCommons::DomainMatches(aCookie, cookie->Host())) {
223 // If the path of new cookie and the path of existing cookie
224 // aren't "/", then this situation needs to compare paths to
225 // ensure only that a newly-created non-secure cookie does not
226 // overlay an existing secure cookie.
227 if (CookieCommons::PathMatches(cookie, aCookie->GetFilePath())) {
228 return true;
233 return false;
236 uint32_t CookieStorage::CountCookiesFromHost(const nsACString& aBaseDomain,
237 uint32_t aPrivateBrowsingId) {
238 OriginAttributes attrs;
239 attrs.mPrivateBrowsingId = aPrivateBrowsingId;
241 // Return a count of all cookies, including expired.
242 CookieEntry* entry = mHostTable.GetEntry(CookieKey(aBaseDomain, attrs));
243 return entry ? entry->GetCookies().Length() : 0;
246 void CookieStorage::GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const {
247 aResult.SetCapacity(mCookieCount);
249 for (const auto& entry : mHostTable) {
250 const CookieEntry::ArrayType& cookies = entry.GetCookies();
251 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
252 aResult.AppendElement(cookies[i]);
257 const nsTArray<RefPtr<Cookie>>* CookieStorage::GetCookiesFromHost(
258 const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes) {
259 CookieEntry* entry =
260 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
261 return entry ? &entry->GetCookies() : nullptr;
264 void CookieStorage::GetCookiesWithOriginAttributes(
265 const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain,
266 nsTArray<RefPtr<nsICookie>>& aResult) {
267 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
268 CookieEntry* entry = iter.Get();
270 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
271 continue;
274 if (!aPattern.Matches(entry->mOriginAttributes)) {
275 continue;
278 const CookieEntry::ArrayType& entryCookies = entry->GetCookies();
280 for (CookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
281 aResult.AppendElement(entryCookies[i]);
286 void CookieStorage::RemoveCookie(const nsACString& aBaseDomain,
287 const OriginAttributes& aOriginAttributes,
288 const nsACString& aHost,
289 const nsACString& aName,
290 const nsACString& aPath) {
291 CookieListIter matchIter{};
292 RefPtr<Cookie> cookie;
293 if (FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath,
294 matchIter)) {
295 cookie = matchIter.Cookie();
296 RemoveCookieFromList(matchIter);
299 if (cookie) {
300 // Everything's done. Notify observers.
301 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, aBaseDomain);
305 void CookieStorage::RemoveCookiesWithOriginAttributes(
306 const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) {
307 // Iterate the hash table of CookieEntry.
308 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
309 CookieEntry* entry = iter.Get();
311 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
312 continue;
315 if (!aPattern.Matches(entry->mOriginAttributes)) {
316 continue;
319 // Pattern matches. Delete all cookies within this CookieEntry.
320 uint32_t cookiesCount = entry->GetCookies().Length();
322 for (CookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
323 // Remove the first cookie from the list.
324 CookieListIter iter(entry, 0);
325 RefPtr<Cookie> cookie = iter.Cookie();
327 // Remove the cookie.
328 RemoveCookieFromList(iter);
330 if (cookie) {
331 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
332 aBaseDomain);
338 /* static */ bool CookieStorage::isIPv6BaseDomain(
339 const nsACString& aBaseDomain) {
340 return aBaseDomain.Contains(':');
343 /* static */ bool CookieStorage::SerializeIPv6BaseDomain(
344 nsACString& aBaseDomain) {
345 bool hasStartBracket = aBaseDomain.First() == '[';
346 bool hasEndBracket = aBaseDomain.Last() == ']';
348 // If only start or end bracket exists host is malformed.
349 if (hasStartBracket != hasEndBracket) {
350 return false;
353 // If the base domain is not in URL format (e.g. [::1]) add brackets so we
354 // can use rusturl_parse_ipv6addr().
355 if (!hasStartBracket) {
356 aBaseDomain.Insert('[', 0);
357 aBaseDomain.Append(']');
360 // Serialize base domain to "zero abbreviation" and lower-case hex
361 // representation.
362 nsAutoCString baseDomain;
363 nsresult rv = (nsresult)rusturl_parse_ipv6addr(&aBaseDomain, &baseDomain);
364 NS_ENSURE_SUCCESS(rv, false);
366 // Strip brackets to match principal representation.
367 aBaseDomain = Substring(baseDomain, 1, baseDomain.Length() - 2);
369 return true;
372 void CookieStorage::RemoveCookiesFromExactHost(
373 const nsACString& aHost, const nsACString& aBaseDomain,
374 const OriginAttributesPattern& aPattern) {
375 // Intermediate fix until Bug 1882259 is resolved.
376 // Bug 1860033 - Cookies do not serialize IPv6 host / base domain in contrast
377 // to principals. To allow deletion by principal serialize before comparison.
378 // We check the base domain since it is used as the CookieList key and equals
379 // the normalized (ASCII) host for IP addresses
380 // (it is equal to the CookieService::NormalizeHost() output).
381 nsAutoCString removeBaseDomain;
382 bool isIPv6 = isIPv6BaseDomain(aBaseDomain);
383 if (isIPv6) {
384 MOZ_ASSERT(!aBaseDomain.IsEmpty());
385 // Copy base domain since argument is immutable.
386 removeBaseDomain = aBaseDomain;
387 if (NS_WARN_IF(!SerializeIPv6BaseDomain(removeBaseDomain))) {
388 // Return on malformed base domains.
389 return;
393 // Iterate the hash table of CookieEntry.
394 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
395 CookieEntry* entry = iter.Get();
397 // IPv6 host / base domain cookies
398 if (isIPv6) {
399 // If we look for a IPv6 cookie skip non-IPv6 cookie entries.
400 if (!isIPv6BaseDomain(entry->mBaseDomain)) {
401 continue;
403 // Serialize IPv6 base domains before comparison.
404 // Copy base domain since argument is immutable.
405 nsAutoCString entryBaseDomain;
406 entryBaseDomain = entry->mBaseDomain;
407 if (NS_WARN_IF(!SerializeIPv6BaseDomain(entryBaseDomain))) {
408 continue;
410 if (!removeBaseDomain.Equals(entryBaseDomain)) {
411 continue;
413 // Non-IPv6 cookies
414 } else if (!aBaseDomain.Equals(entry->mBaseDomain)) {
415 continue;
418 if (!aPattern.Matches(entry->mOriginAttributes)) {
419 continue;
422 uint32_t cookiesCount = entry->GetCookies().Length();
423 for (CookieEntry::IndexType i = cookiesCount; i != 0; --i) {
424 CookieListIter iter(entry, i - 1);
425 RefPtr<Cookie> cookie = iter.Cookie();
427 // For IP addresses (ASCII normalized) host == baseDomain, we checked
428 // equality already.
429 if (!isIPv6 && !aHost.Equals(cookie->RawHost())) {
430 continue;
433 // Remove the cookie.
434 RemoveCookieFromList(iter);
436 if (cookie) {
437 NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
438 aBaseDomain);
444 void CookieStorage::RemoveAll() {
445 // clearing the hashtable will call each CookieEntry's dtor,
446 // which releases all their respective children.
447 mHostTable.Clear();
448 mCookieCount = 0;
449 mCookieOldestTime = INT64_MAX;
451 RemoveAllInternal();
453 NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED, ""_ns);
456 // notify observers that the cookie list changed.
457 void CookieStorage::NotifyChanged(nsISupports* aSubject,
458 nsICookieNotification::Action aAction,
459 const nsACString& aBaseDomain,
460 dom::BrowsingContext* aBrowsingContext,
461 bool aOldCookieIsSession) {
462 nsCOMPtr<nsIObserverService> os = services::GetObserverService();
463 if (!os) {
464 return;
467 nsCOMPtr<nsICookie> cookie;
468 nsCOMPtr<nsIArray> batchDeletedCookies;
470 if (aAction == nsICookieNotification::COOKIES_BATCH_DELETED) {
471 batchDeletedCookies = do_QueryInterface(aSubject);
472 } else {
473 cookie = do_QueryInterface(aSubject);
476 uint64_t browsingContextId = 0;
477 if (aBrowsingContext) {
478 browsingContextId = aBrowsingContext->Id();
481 nsCOMPtr<nsICookieNotification> notification = new CookieNotification(
482 aAction, cookie, aBaseDomain, batchDeletedCookies, browsingContextId);
483 // Notify for topic "private-cookie-changed" or "cookie-changed"
484 os->NotifyObservers(notification, NotificationTopic(), u"");
486 NotifyChangedInternal(notification, aOldCookieIsSession);
489 // this is a backend function for adding a cookie to the list, via SetCookie.
490 // also used in the cookie manager, for profile migration from IE. it either
491 // replaces an existing cookie; or adds the cookie to the hashtable, and
492 // deletes a cookie (if maximum number of cookies has been reached). also
493 // performs list maintenance by removing expired cookies.
494 void CookieStorage::AddCookie(nsIConsoleReportCollector* aCRC,
495 const nsACString& aBaseDomain,
496 const OriginAttributes& aOriginAttributes,
497 Cookie* aCookie, int64_t aCurrentTimeInUsec,
498 nsIURI* aHostURI, const nsACString& aCookieHeader,
499 bool aFromHttp,
500 dom::BrowsingContext* aBrowsingContext) {
501 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
503 CookieListIter exactIter{};
504 bool foundCookie = false;
505 foundCookie = FindCookie(aBaseDomain, aOriginAttributes, aCookie->Host(),
506 aCookie->Name(), aCookie->Path(), exactIter);
507 bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
508 bool potentiallyTrustworthy = true;
509 if (aHostURI) {
510 potentiallyTrustworthy =
511 nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
513 constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns;
514 bool oldCookieIsSession = false;
515 // Step1, call FindSecureCookie(). FindSecureCookie() would
516 // find the existing cookie with the security flag and has
517 // the same name, host and path of the new cookie, if there is any.
518 // Step2, Confirm new cookie's security setting. If any targeted
519 // cookie had been found in Step1, then confirm whether the
520 // new cookie could modify it. If the new created cookie’s
521 // "secure-only-flag" is not set, and the "scheme" component
522 // of the "request-uri" does not denote a "secure" protocol,
523 // then ignore the new cookie.
524 // (draft-ietf-httpbis-cookie-alone section 3.2)
525 if (!aCookie->IsSecure() &&
526 (foundSecureExact ||
527 FindSecureCookie(aBaseDomain, aOriginAttributes, aCookie)) &&
528 !potentiallyTrustworthy) {
529 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
530 "cookie can't save because older cookie is secure "
531 "cookie but newer cookie is non-secure cookie");
532 CookieLogging::LogMessageToConsole(
533 aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
534 "CookieRejectedNonsecureOverSecure"_ns,
535 AutoTArray<nsString, 1>{
536 NS_ConvertUTF8toUTF16(aCookie->Name()),
538 return;
541 RefPtr<Cookie> oldCookie;
542 nsCOMPtr<nsIArray> purgedList;
543 if (foundCookie) {
544 oldCookie = exactIter.Cookie();
545 oldCookieIsSession = oldCookie->IsSession();
547 // Check if the old cookie is stale (i.e. has already expired). If so, we
548 // need to be careful about the semantics of removing it and adding the new
549 // cookie: we want the behavior wrt adding the new cookie to be the same as
550 // if it didn't exist, but we still want to fire a removal notification.
551 if (oldCookie->Expiry() <= currentTime) {
552 if (aCookie->Expiry() <= currentTime) {
553 // The new cookie has expired and the old one is stale. Nothing to do.
554 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
555 "cookie has already expired");
556 return;
559 // Remove the stale cookie. We save notification for later, once all list
560 // modifications are complete.
561 RemoveCookieFromList(exactIter);
562 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
563 "stale cookie was purged");
564 purgedList = CreatePurgeList(oldCookie);
566 // We've done all we need to wrt removing and notifying the stale cookie.
567 // From here on out, we pretend pretend it didn't exist, so that we
568 // preserve expected notification semantics when adding the new cookie.
569 foundCookie = false;
571 } else {
572 // If the old cookie is httponly, make sure we're not coming from script.
573 if (!aFromHttp && oldCookie->IsHttpOnly()) {
574 COOKIE_LOGFAILURE(
575 SET_COOKIE, aHostURI, aCookieHeader,
576 "previously stored cookie is httponly; coming from script");
577 CookieLogging::LogMessageToConsole(
578 aCRC, aHostURI, nsIScriptError::warningFlag,
579 CONSOLE_REJECTION_CATEGORY,
580 "CookieRejectedHttpOnlyButFromScript"_ns,
581 AutoTArray<nsString, 1>{
582 NS_ConvertUTF8toUTF16(aCookie->Name()),
584 return;
587 // If the new cookie has the same value, expiry date, isSecure, isSession,
588 // isHttpOnly and SameSite flags then we can just keep the old one.
589 // Only if any of these differ we would want to override the cookie.
590 if (oldCookie->Value().Equals(aCookie->Value()) &&
591 oldCookie->Expiry() == aCookie->Expiry() &&
592 oldCookie->IsSecure() == aCookie->IsSecure() &&
593 oldCookie->IsSession() == aCookie->IsSession() &&
594 oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
595 oldCookie->SameSite() == aCookie->SameSite() &&
596 oldCookie->RawSameSite() == aCookie->RawSameSite() &&
597 oldCookie->SchemeMap() == aCookie->SchemeMap() &&
598 // We don't want to perform this optimization if the cookie is
599 // considered stale, since in this case we would need to update the
600 // database.
601 !oldCookie->IsStale()) {
602 // Update the last access time on the old cookie.
603 oldCookie->SetLastAccessed(aCookie->LastAccessed());
604 UpdateCookieOldestTime(oldCookie);
605 return;
608 // Merge the scheme map in case the old cookie and the new cookie are
609 // used with different schemes.
610 MergeCookieSchemeMap(oldCookie, aCookie);
612 // Remove the old cookie.
613 RemoveCookieFromList(exactIter);
615 // If the new cookie has expired -- i.e. the intent was simply to delete
616 // the old cookie -- then we're done.
617 if (aCookie->Expiry() <= currentTime) {
618 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
619 "previously stored cookie was deleted");
620 NotifyChanged(oldCookie, nsICookieNotification::COOKIE_DELETED,
621 aBaseDomain, aBrowsingContext, oldCookieIsSession);
622 return;
625 // Preserve creation time of cookie for ordering purposes.
626 aCookie->SetCreationTime(oldCookie->CreationTime());
629 } else {
630 // check if cookie has already expired
631 if (aCookie->Expiry() <= currentTime) {
632 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
633 "cookie has already expired");
634 return;
637 // check if we have to delete an old cookie.
638 CookieEntry* entry =
639 mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
640 if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
641 nsTArray<CookieListIter> removedIterList;
642 // Prioritize evicting insecure cookies.
643 // (draft-ietf-httpbis-cookie-alone section 3.3)
644 uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost;
645 FindStaleCookies(entry, currentTime, false, removedIterList, limit);
646 if (removedIterList.Length() == 0) {
647 if (aCookie->IsSecure()) {
648 // It's valid to evict a secure cookie for another secure cookie.
649 FindStaleCookies(entry, currentTime, true, removedIterList, limit);
650 } else {
651 COOKIE_LOGEVICTED(aCookie,
652 "Too many cookies for this domain and the new "
653 "cookie is not a secure cookie");
654 return;
658 MOZ_ASSERT(!removedIterList.IsEmpty());
659 // Sort |removedIterList| by index again, since we have to remove the
660 // cookie in the reverse order.
661 removedIterList.Sort(CompareCookiesByIndex());
662 for (auto it = removedIterList.rbegin(); it != removedIterList.rend();
663 it++) {
664 RefPtr<Cookie> evictedCookie = (*it).Cookie();
665 COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain");
666 RemoveCookieFromList(*it);
667 CreateOrUpdatePurgeList(purgedList, evictedCookie);
668 MOZ_ASSERT((*it).entry);
670 uint32_t purgedLength = 0;
671 purgedList->GetLength(&purgedLength);
672 mozilla::glean::networking::cookie_purge_entry_max.AccumulateSingleSample(
673 purgedLength);
675 } else if (mCookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
676 int64_t maxAge = aCurrentTimeInUsec - mCookieOldestTime;
677 int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
678 if (maxAge >= purgeAge) {
679 // we're over both size and age limits by 10%; time to purge the table!
680 // do this by:
681 // 1) removing expired cookies;
682 // 2) evicting the balance of old cookies until we reach the size limit.
683 // note that the mCookieOldestTime indicator can be pessimistic - if
684 // it's older than the actual oldest cookie, we'll just purge more
685 // eagerly.
686 purgedList = PurgeCookies(aCurrentTimeInUsec, mMaxNumberOfCookies,
687 mCookiePurgeAge);
688 uint32_t purgedLength = 0;
689 purgedList->GetLength(&purgedLength);
690 mozilla::glean::networking::cookie_purge_max.AccumulateSingleSample(
691 purgedLength);
696 // Add the cookie to the db. We do not supply a params array for batching
697 // because this might result in removals and additions being out of order.
698 AddCookieToList(aBaseDomain, aOriginAttributes, aCookie);
699 StoreCookie(aBaseDomain, aOriginAttributes, aCookie);
701 COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
703 // Now that list mutations are complete, notify observers. We do it here
704 // because observers may themselves attempt to mutate the list.
705 if (purgedList) {
706 NotifyChanged(purgedList, nsICookieNotification::COOKIES_BATCH_DELETED,
707 ""_ns);
710 // Notify for topic "private-cookie-changed" or "cookie-changed"
711 NotifyChanged(aCookie,
712 foundCookie ? nsICookieNotification::COOKIE_CHANGED
713 : nsICookieNotification::COOKIE_ADDED,
714 aBaseDomain, aBrowsingContext, oldCookieIsSession);
717 void CookieStorage::UpdateCookieOldestTime(Cookie* aCookie) {
718 if (aCookie->LastAccessed() < mCookieOldestTime) {
719 mCookieOldestTime = aCookie->LastAccessed();
723 void CookieStorage::MergeCookieSchemeMap(Cookie* aOldCookie,
724 Cookie* aNewCookie) {
725 aNewCookie->SetSchemeMap(aOldCookie->SchemeMap() | aNewCookie->SchemeMap());
728 void CookieStorage::AddCookieToList(const nsACString& aBaseDomain,
729 const OriginAttributes& aOriginAttributes,
730 Cookie* aCookie) {
731 if (!aCookie) {
732 NS_WARNING("Attempting to AddCookieToList with null cookie");
733 return;
736 CookieKey key(aBaseDomain, aOriginAttributes);
738 CookieEntry* entry = mHostTable.PutEntry(key);
739 NS_ASSERTION(entry, "can't insert element into a null entry!");
741 entry->GetCookies().AppendElement(aCookie);
742 ++mCookieCount;
744 // keep track of the oldest cookie, for when it comes time to purge
745 UpdateCookieOldestTime(aCookie);
748 // static
749 already_AddRefed<nsIArray> CookieStorage::CreatePurgeList(nsICookie* aCookie) {
750 nsCOMPtr<nsIMutableArray> removedList =
751 do_CreateInstance(NS_ARRAY_CONTRACTID);
752 removedList->AppendElement(aCookie);
753 return removedList.forget();
756 // Given the output iter array and the count limit, find cookies
757 // sort by expiry and lastAccessed time.
758 // static
759 void CookieStorage::FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime,
760 bool aIsSecure,
761 nsTArray<CookieListIter>& aOutput,
762 uint32_t aLimit) {
763 MOZ_ASSERT(aLimit);
765 const CookieEntry::ArrayType& cookies = aEntry->GetCookies();
766 aOutput.Clear();
768 CookieIterComparator comp(aCurrentTime);
769 nsTPriorityQueue<CookieListIter, CookieIterComparator> queue(comp);
771 for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
772 Cookie* cookie = cookies[i];
774 if (cookie->Expiry() <= aCurrentTime) {
775 queue.Push(CookieListIter(aEntry, i));
776 continue;
779 if (!aIsSecure) {
780 // We want to look for the non-secure cookie first time through,
781 // then find the secure cookie the second time this function is called.
782 if (cookie->IsSecure()) {
783 continue;
787 queue.Push(CookieListIter(aEntry, i));
790 uint32_t count = 0;
791 while (!queue.IsEmpty() && count < aLimit) {
792 aOutput.AppendElement(queue.Pop());
793 count++;
797 // static
798 void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList,
799 nsICookie* aCookie) {
800 if (!aPurgedList) {
801 COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list"));
802 aPurgedList = CreatePurgeList(aCookie);
803 return;
806 nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(aPurgedList);
807 if (purgedList) {
808 COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list"));
809 purgedList->AppendElement(aCookie);
810 } else {
811 COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!"));
815 // purges expired and old cookies in a batch operation.
816 already_AddRefed<nsIArray> CookieStorage::PurgeCookiesWithCallbacks(
817 int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
818 int64_t aCookiePurgeAge,
819 std::function<void(const CookieListIter&)>&& aRemoveCookieCallback,
820 std::function<void()>&& aFinalizeCallback) {
821 NS_ASSERTION(mHostTable.Count() > 0, "table is empty");
823 uint32_t initialCookieCount = mCookieCount;
824 COOKIE_LOGSTRING(LogLevel::Debug,
825 ("PurgeCookies(): beginning purge with %" PRIu32
826 " cookies and %" PRId64 " oldest age",
827 mCookieCount, aCurrentTimeInUsec - mCookieOldestTime));
829 using PurgeList = nsTArray<CookieListIter>;
830 PurgeList purgeList(kMaxNumberOfCookies);
832 nsCOMPtr<nsIMutableArray> removedList =
833 do_CreateInstance(NS_ARRAY_CONTRACTID);
835 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
836 int64_t purgeTime = aCurrentTimeInUsec - aCookiePurgeAge;
837 int64_t oldestTime = INT64_MAX;
839 for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
840 CookieEntry* entry = iter.Get();
842 const CookieEntry::ArrayType& cookies = entry->GetCookies();
843 auto length = cookies.Length();
844 for (CookieEntry::IndexType i = 0; i < length;) {
845 CookieListIter iter(entry, i);
846 Cookie* cookie = cookies[i];
848 // check if the cookie has expired
849 if (cookie->Expiry() <= currentTime) {
850 removedList->AppendElement(cookie);
851 COOKIE_LOGEVICTED(cookie, "Cookie expired");
853 // remove from list; do not increment our iterator, but stop if we're
854 // done already.
855 aRemoveCookieCallback(iter);
856 if (i == --length) {
857 break;
859 } else {
860 // check if the cookie is over the age limit
861 if (cookie->LastAccessed() <= purgeTime) {
862 purgeList.AppendElement(iter);
864 } else if (cookie->LastAccessed() < oldestTime) {
865 // reset our indicator
866 oldestTime = cookie->LastAccessed();
869 ++i;
871 MOZ_ASSERT(length == cookies.Length());
875 uint32_t postExpiryCookieCount = mCookieCount;
877 // now we have a list of iterators for cookies over the age limit.
878 // sort them by age, and then we'll see how many to remove...
879 purgeList.Sort(CompareCookiesByAge());
881 // only remove old cookies until we reach the max cookie limit, no more.
882 uint32_t excess = mCookieCount > aMaxNumberOfCookies
883 ? mCookieCount - aMaxNumberOfCookies
884 : 0;
885 if (purgeList.Length() > excess) {
886 // We're not purging everything in the list, so update our indicator.
887 oldestTime = purgeList[excess].Cookie()->LastAccessed();
889 purgeList.SetLength(excess);
892 // sort the list again, this time grouping cookies with a common entryclass
893 // together, and with ascending index. this allows us to iterate backwards
894 // over the list removing cookies, without having to adjust indexes as we go.
895 purgeList.Sort(CompareCookiesByIndex());
896 for (PurgeList::index_type i = purgeList.Length(); i--;) {
897 Cookie* cookie = purgeList[i].Cookie();
898 removedList->AppendElement(cookie);
899 COOKIE_LOGEVICTED(cookie, "Cookie too old");
901 aRemoveCookieCallback(purgeList[i]);
904 // Update the database if we have entries to purge.
905 if (aFinalizeCallback) {
906 aFinalizeCallback();
909 // reset the oldest time indicator
910 mCookieOldestTime = oldestTime;
912 COOKIE_LOGSTRING(LogLevel::Debug,
913 ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32
914 " purged; %" PRIu32 " remain; %" PRId64 " oldest age",
915 initialCookieCount - postExpiryCookieCount,
916 postExpiryCookieCount - mCookieCount, mCookieCount,
917 aCurrentTimeInUsec - mCookieOldestTime));
919 return removedList.forget();
922 // remove a cookie from the hashtable, and update the iterator state.
923 void CookieStorage::RemoveCookieFromList(const CookieListIter& aIter) {
924 RemoveCookieFromDB(*aIter.Cookie());
925 RemoveCookieFromListInternal(aIter);
928 void CookieStorage::RemoveCookieFromListInternal(const CookieListIter& aIter) {
929 if (aIter.entry->GetCookies().Length() == 1) {
930 // we're removing the last element in the array - so just remove the entry
931 // from the hash. note that the entryclass' dtor will take care of
932 // releasing this last element for us!
933 mHostTable.RawRemoveEntry(aIter.entry);
935 } else {
936 // just remove the element from the list
937 aIter.entry->GetCookies().RemoveElementAt(aIter.index);
940 --mCookieCount;
943 void CookieStorage::PrefChanged(nsIPrefBranch* aPrefBranch) {
944 int32_t val;
945 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) {
946 mMaxNumberOfCookies =
947 static_cast<uint16_t> LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
950 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) {
951 mCookieQuotaPerHost = static_cast<uint16_t> LIMIT(
952 val, 1, mMaxCookiesPerHost - 1, kCookieQuotaPerHost);
955 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) {
956 mMaxCookiesPerHost = static_cast<uint16_t> LIMIT(
957 val, mCookieQuotaPerHost + 1, 0xFFFF, kMaxCookiesPerHost);
960 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
961 mCookiePurgeAge =
962 int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
966 NS_IMETHODIMP
967 CookieStorage::Observe(nsISupports* aSubject, const char* aTopic,
968 const char16_t* /*aData*/) {
969 if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
970 nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
971 if (prefBranch) {
972 PrefChanged(prefBranch);
974 } else if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
975 CollectCookieJarSizeData();
978 return NS_OK;
981 } // namespace net
982 } // namespace mozilla