tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

CookieStorage.cpp (44350B)


      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/. */
      5 
      6 #include "Cookie.h"
      7 #include "CookieCommons.h"
      8 #include "CookieLogging.h"
      9 #include "CookieParser.h"
     10 #include "CookieNotification.h"
     11 #include "mozilla/net/MozURL_ffi.h"
     12 #include "CookieService.h"
     13 #include "nsCOMPtr.h"
     14 #include "nsICookieNotification.h"
     15 #include "CookieStorage.h"
     16 #include "mozilla/dom/nsMixedContentBlocker.h"
     17 #include "mozilla/glean/NetwerkMetrics.h"
     18 #include "mozilla/StaticPrefs_network.h"
     19 #include "nsIMutableArray.h"
     20 #include "nsTPriorityQueue.h"
     21 #include "nsIScriptError.h"
     22 #include "nsIUserIdleService.h"
     23 #include "nsServiceManagerUtils.h"
     24 #include "nsComponentManagerUtils.h"
     25 #include "prprf.h"
     26 #include "nsIPrefService.h"
     27 
     28 #undef ADD_TEN_PERCENT
     29 #define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
     30 
     31 #undef LIMIT
     32 #define LIMIT(x, low, high, default) \
     33  ((x) >= (low) && (x) <= (high) ? (x) : (default))
     34 
     35 // in order to keep our metrics consistent
     36 // we only send metrics when the pref hasn't been manipulated from the default
     37 static const uint32_t kChipsPartitionByteCapacityDefault = 10240;
     38 static const double kChipsHardLimitFactor = 1.2;
     39 
     40 namespace mozilla {
     41 namespace net {
     42 
     43 // comparator class for lastaccessed times of cookies.
     44 class CookieStorage::CompareCookiesByAge {
     45 public:
     46  static bool Equals(const CookieListIter& a, const CookieListIter& b) {
     47    return a.Cookie()->LastAccessedInUSec() ==
     48               b.Cookie()->LastAccessedInUSec() &&
     49           a.Cookie()->CreationTimeInUSec() == b.Cookie()->CreationTimeInUSec();
     50  }
     51 
     52  static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
     53    // compare by lastAccessedInUSec time, and tiebreak by creationTimeInUSec.
     54    int64_t result =
     55        a.Cookie()->LastAccessedInUSec() - b.Cookie()->LastAccessedInUSec();
     56    if (result != 0) {
     57      return result < 0;
     58    }
     59 
     60    return a.Cookie()->CreationTimeInUSec() < b.Cookie()->CreationTimeInUSec();
     61  }
     62 };
     63 
     64 // Cookie comparator for the priority queue used in FindStaleCookies.
     65 // Note that the expired cookie has the highest priority.
     66 // Other non-expired cookies are sorted by their age.
     67 class CookieStorage::CookieIterComparator {
     68 private:
     69  int64_t mCurrentTimeInMSec;
     70 
     71 public:
     72  explicit CookieIterComparator(int64_t aTimeInMSec)
     73      : mCurrentTimeInMSec(aTimeInMSec) {}
     74 
     75  bool LessThan(const CookieListIter& lhs, const CookieListIter& rhs) {
     76    bool lExpired = lhs.Cookie()->ExpiryInMSec() <= mCurrentTimeInMSec;
     77    bool rExpired = rhs.Cookie()->ExpiryInMSec() <= mCurrentTimeInMSec;
     78    if (lExpired && !rExpired) {
     79      return true;
     80    }
     81 
     82    if (!lExpired && rExpired) {
     83      return false;
     84    }
     85 
     86    return CompareCookiesByAge::LessThan(lhs, rhs);
     87  }
     88 };
     89 
     90 // comparator class for sorting cookies by entry and index.
     91 class CookieStorage::CompareCookiesByIndex {
     92 public:
     93  static bool Equals(const CookieListIter& a, const CookieListIter& b) {
     94    NS_ASSERTION(a.entry != b.entry || a.index != b.index,
     95                 "cookie indexes should never be equal");
     96    return false;
     97  }
     98 
     99  static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
    100    // compare by entryclass pointer, then by index.
    101    if (a.entry != b.entry) {
    102      return a.entry < b.entry;
    103    }
    104 
    105    return a.index < b.index;
    106  }
    107 };
    108 
    109 // ---------------------------------------------------------------------------
    110 // CookieEntry
    111 
    112 size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
    113  size_t amount = CookieKey::SizeOfExcludingThis(aMallocSizeOf);
    114 
    115  amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
    116  for (uint32_t i = 0; i < mCookies.Length(); ++i) {
    117    amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
    118  }
    119 
    120  return amount;
    121 }
    122 
    123 bool CookieEntry::IsPartitioned() const {
    124  return !mOriginAttributes.mPartitionKey.IsEmpty();
    125 }
    126 
    127 // ---------------------------------------------------------------------------
    128 // CookieStorage
    129 
    130 NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference)
    131 
    132 void CookieStorage::Init() {
    133  // init our pref and observer
    134  nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
    135  if (prefBranch) {
    136    prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
    137    prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
    138    prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
    139    PrefChanged(prefBranch);
    140  }
    141 
    142  nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
    143  NS_ENSURE_TRUE_VOID(observerService);
    144 
    145  nsresult rv =
    146      observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, true);
    147  NS_ENSURE_SUCCESS_VOID(rv);
    148 }
    149 
    150 size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
    151  size_t amount = 0;
    152 
    153  amount += aMallocSizeOf(this);
    154  amount += mHostTable.SizeOfExcludingThis(aMallocSizeOf);
    155 
    156  return amount;
    157 }
    158 
    159 void CookieStorage::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const {
    160  aCookies.SetCapacity(mCookieCount);
    161  for (const auto& entry : mHostTable) {
    162    const CookieEntry::ArrayType& cookies = entry.GetCookies();
    163    for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    164      aCookies.AppendElement(cookies[i]);
    165    }
    166  }
    167 }
    168 
    169 void CookieStorage::GetSessionCookies(
    170    nsTArray<RefPtr<nsICookie>>& aCookies) const {
    171  aCookies.SetCapacity(mCookieCount);
    172  for (const auto& entry : mHostTable) {
    173    const CookieEntry::ArrayType& cookies = entry.GetCookies();
    174    for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    175      Cookie* cookie = cookies[i];
    176      // Filter out non-session cookies.
    177      if (cookie->IsSession()) {
    178        aCookies.AppendElement(cookie);
    179      }
    180    }
    181  }
    182 }
    183 
    184 // find an exact cookie specified by host, name, and path that hasn't expired.
    185 already_AddRefed<Cookie> CookieStorage::FindCookie(
    186    const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes,
    187    const nsACString& aHost, const nsACString& aName, const nsACString& aPath) {
    188  CookieListIter iter{};
    189 
    190  if (!FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath, iter)) {
    191    return nullptr;
    192  }
    193 
    194  RefPtr<Cookie> cookie = iter.Cookie();
    195  return cookie.forget();
    196 }
    197 
    198 // find an exact cookie specified by host, name, and path that hasn't expired.
    199 bool CookieStorage::FindCookie(const nsACString& aBaseDomain,
    200                               const OriginAttributes& aOriginAttributes,
    201                               const nsACString& aHost, const nsACString& aName,
    202                               const nsACString& aPath, CookieListIter& aIter) {
    203  CookieEntry* entry =
    204      mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
    205  if (!entry) {
    206    return false;
    207  }
    208 
    209  const CookieEntry::ArrayType& cookies = entry->GetCookies();
    210  for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    211    Cookie* cookie = cookies[i];
    212 
    213    if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
    214        aName.Equals(cookie->Name())) {
    215      aIter = CookieListIter(entry, i);
    216      return true;
    217    }
    218  }
    219 
    220  return false;
    221 }
    222 
    223 // find an secure cookie specified by host and name
    224 bool CookieStorage::FindSecureCookie(const nsACString& aBaseDomain,
    225                                     const OriginAttributes& aOriginAttributes,
    226                                     Cookie* aCookie) {
    227  CookieEntry* entry =
    228      mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
    229  if (!entry) {
    230    return false;
    231  }
    232 
    233  const CookieEntry::ArrayType& cookies = entry->GetCookies();
    234  for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    235    Cookie* cookie = cookies[i];
    236    // isn't a match if insecure or a different name
    237    if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name())) {
    238      continue;
    239    }
    240 
    241    // The host must "domain-match" an existing cookie or vice-versa
    242    if (CookieCommons::DomainMatches(cookie, aCookie->Host()) ||
    243        CookieCommons::DomainMatches(aCookie, cookie->Host())) {
    244      // If the path of new cookie and the path of existing cookie
    245      // aren't "/", then this situation needs to compare paths to
    246      // ensure only that a newly-created non-secure cookie does not
    247      // overlay an existing secure cookie.
    248      if (CookieCommons::PathMatches(cookie, aCookie->Path())) {
    249        return true;
    250      }
    251    }
    252  }
    253 
    254  return false;
    255 }
    256 
    257 uint32_t CookieStorage::CountCookiesFromHost(const nsACString& aBaseDomain,
    258                                             uint32_t aPrivateBrowsingId) {
    259  OriginAttributes attrs;
    260  attrs.mPrivateBrowsingId = aPrivateBrowsingId;
    261 
    262  // Return a count of all cookies, including expired.
    263  CookieEntry* entry = mHostTable.GetEntry(CookieKey(aBaseDomain, attrs));
    264  return entry ? entry->GetCookies().Length() : 0;
    265 }
    266 
    267 uint32_t CookieStorage::CountCookieBytesNotMatchingCookie(
    268    const Cookie& cookie, const nsACString& baseDomain) {
    269  nsTArray<RefPtr<Cookie>> cookies;
    270  GetCookiesFromHost(baseDomain, cookie.OriginAttributesRef(), cookies);
    271 
    272  // count cookies with different name to the cookie being added
    273  uint32_t cookieBytes = 0;
    274  for (Cookie* c : cookies) {
    275    nsAutoCString name;
    276    nsAutoCString value;
    277    c->GetName(name);
    278    c->GetValue(value);
    279    if (!cookie.Name().Equals(name)) {
    280      cookieBytes += name.Length() + value.Length();
    281    }
    282  }
    283  return cookieBytes;
    284 }
    285 
    286 void CookieStorage::GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const {
    287  aResult.SetCapacity(mCookieCount);
    288 
    289  for (const auto& entry : mHostTable) {
    290    const CookieEntry::ArrayType& cookies = entry.GetCookies();
    291    for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    292      aResult.AppendElement(cookies[i]);
    293    }
    294  }
    295 }
    296 
    297 void CookieStorage::GetCookiesFromHost(
    298    const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes,
    299    nsTArray<RefPtr<Cookie>>& aCookies) {
    300  CookieEntry* entry =
    301      mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
    302  if (!entry) {
    303    return;
    304  }
    305 
    306  aCookies = entry->GetCookies().Clone();
    307 }
    308 
    309 void CookieStorage::GetCookiesWithOriginAttributes(
    310    const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain,
    311    bool aSorted, nsTArray<RefPtr<nsICookie>>& aResult) {
    312  for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
    313    CookieEntry* entry = iter.Get();
    314 
    315    if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
    316      continue;
    317    }
    318 
    319    if (!aPattern.Matches(entry->mOriginAttributes)) {
    320      continue;
    321    }
    322 
    323    const CookieEntry::ArrayType& entryCookies = entry->GetCookies();
    324 
    325    for (CookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
    326      aResult.AppendElement(entryCookies[i]);
    327    }
    328  }
    329 
    330  if (aSorted) {
    331    aResult.Sort(CompareCookiesForSending());
    332  }
    333 }
    334 
    335 void CookieStorage::RemoveCookie(const nsACString& aBaseDomain,
    336                                 const OriginAttributes& aOriginAttributes,
    337                                 const nsACString& aHost,
    338                                 const nsACString& aName,
    339                                 const nsACString& aPath, bool aFromHttp,
    340                                 const nsID* aOperationID) {
    341  CookieListIter matchIter{};
    342  RefPtr<Cookie> cookie;
    343  if (FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath,
    344                 matchIter)) {
    345    cookie = matchIter.Cookie();
    346 
    347    // If the old cookie is httponly, make sure we're not coming from script.
    348    if (cookie && !aFromHttp && cookie->IsHttpOnly()) {
    349      return;
    350    }
    351 
    352    RemoveCookieFromList(matchIter);
    353  }
    354 
    355  if (cookie) {
    356    // Everything's done. Notify observers.
    357    NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, aBaseDomain,
    358                  aOperationID);
    359  }
    360 }
    361 
    362 void CookieStorage::RemoveCookiesWithOriginAttributes(
    363    const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) {
    364  // Iterate the hash table of CookieEntry.
    365  for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
    366    CookieEntry* entry = iter.Get();
    367 
    368    if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
    369      continue;
    370    }
    371 
    372    if (!aPattern.Matches(entry->mOriginAttributes)) {
    373      continue;
    374    }
    375 
    376    // Pattern matches. Delete all cookies within this CookieEntry.
    377    uint32_t cookiesCount = entry->GetCookies().Length();
    378 
    379    for (CookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
    380      // Remove the first cookie from the list.
    381      CookieListIter iter(entry, 0);
    382      RefPtr<Cookie> cookie = iter.Cookie();
    383 
    384      // Remove the cookie.
    385      RemoveCookieFromList(iter);
    386 
    387      if (cookie) {
    388        NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
    389                      aBaseDomain);
    390      }
    391    }
    392  }
    393 }
    394 
    395 /* static */ bool CookieStorage::SerializeIPv6BaseDomain(
    396    nsACString& aBaseDomain) {
    397  bool hasStartBracket = aBaseDomain.First() == '[';
    398  bool hasEndBracket = aBaseDomain.Last() == ']';
    399 
    400  // If only start or end bracket exists host is malformed.
    401  if (hasStartBracket != hasEndBracket) {
    402    return false;
    403  }
    404 
    405  // If the base domain is not in URL format (e.g. [::1]) add brackets so we
    406  // can use rusturl_parse_ipv6addr().
    407  if (!hasStartBracket) {
    408    aBaseDomain.Insert('[', 0);
    409    aBaseDomain.Append(']');
    410  }
    411 
    412  // Serialize base domain to "zero abbreviation" and lower-case hex
    413  // representation.
    414  nsAutoCString baseDomain;
    415  nsresult rv = (nsresult)rusturl_parse_ipv6addr(&aBaseDomain, &baseDomain);
    416  NS_ENSURE_SUCCESS(rv, false);
    417 
    418  // Strip brackets to match principal representation.
    419  aBaseDomain = Substring(baseDomain, 1, baseDomain.Length() - 2);
    420 
    421  return true;
    422 }
    423 
    424 void CookieStorage::RemoveCookiesFromExactHost(
    425    const nsACString& aHost, const nsACString& aBaseDomain,
    426    const OriginAttributesPattern& aPattern) {
    427  // Intermediate fix until Bug 1882259 is resolved.
    428  // Bug 1860033 - Cookies do not serialize IPv6 host / base domain in contrast
    429  // to principals. To allow deletion by principal serialize before comparison.
    430  // We check the base domain since it is used as the CookieList key and equals
    431  // the normalized (ASCII) host for IP addresses
    432  // (it is equal to the CookieService::NormalizeHost() output).
    433  nsAutoCString removeBaseDomain;
    434  bool isIPv6 = CookieCommons::IsIPv6BaseDomain(aBaseDomain);
    435  if (isIPv6) {
    436    MOZ_ASSERT(!aBaseDomain.IsEmpty());
    437    // Copy base domain since argument is immutable.
    438    removeBaseDomain = aBaseDomain;
    439    if (NS_WARN_IF(!SerializeIPv6BaseDomain(removeBaseDomain))) {
    440      // Return on malformed base domains.
    441      return;
    442    }
    443  }
    444 
    445  // Iterate the hash table of CookieEntry.
    446  for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
    447    CookieEntry* entry = iter.Get();
    448 
    449    // IPv6 host / base domain cookies
    450    if (isIPv6) {
    451      // If we look for a IPv6 cookie skip non-IPv6 cookie entries.
    452      if (!CookieCommons::IsIPv6BaseDomain(entry->mBaseDomain)) {
    453        continue;
    454      }
    455      // Serialize IPv6 base domains before comparison.
    456      // Copy base domain since argument is immutable.
    457      nsAutoCString entryBaseDomain;
    458      entryBaseDomain = entry->mBaseDomain;
    459      if (NS_WARN_IF(!SerializeIPv6BaseDomain(entryBaseDomain))) {
    460        continue;
    461      }
    462      if (!removeBaseDomain.Equals(entryBaseDomain)) {
    463        continue;
    464      }
    465      // Non-IPv6 cookies
    466    } else if (!aBaseDomain.Equals(entry->mBaseDomain)) {
    467      continue;
    468    }
    469 
    470    if (!aPattern.Matches(entry->mOriginAttributes)) {
    471      continue;
    472    }
    473 
    474    uint32_t cookiesCount = entry->GetCookies().Length();
    475    for (CookieEntry::IndexType i = cookiesCount; i != 0; --i) {
    476      CookieListIter iter(entry, i - 1);
    477      RefPtr<Cookie> cookie = iter.Cookie();
    478 
    479      // For IP addresses (ASCII normalized) host == baseDomain, we checked
    480      // equality already.
    481      if (!isIPv6 && !aHost.Equals(cookie->RawHost())) {
    482        continue;
    483      }
    484 
    485      // Remove the cookie.
    486      RemoveCookieFromList(iter);
    487 
    488      if (cookie) {
    489        NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
    490                      aBaseDomain);
    491      }
    492    }
    493  }
    494 }
    495 
    496 void CookieStorage::RemoveAll() {
    497  // clearing the hashtable will call each CookieEntry's dtor,
    498  // which releases all their respective children.
    499  mHostTable.Clear();
    500  mCookieCount = 0;
    501  mCookieOldestTime = INT64_MAX;
    502 
    503  RemoveAllInternal();
    504 
    505  NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED, ""_ns);
    506 }
    507 
    508 // notify observers that the cookie list changed.
    509 void CookieStorage::NotifyChanged(nsISupports* aSubject,
    510                                  nsICookieNotification::Action aAction,
    511                                  const nsACString& aBaseDomain,
    512                                  bool aIsThirdParty,
    513                                  dom::BrowsingContext* aBrowsingContext,
    514                                  bool aOldCookieIsSession,
    515                                  const nsID* aOperationID) {
    516  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
    517  if (!os) {
    518    return;
    519  }
    520 
    521  nsCOMPtr<nsICookie> cookie;
    522  nsCOMPtr<nsIArray> batchDeletedCookies;
    523 
    524  if (aAction == nsICookieNotification::COOKIES_BATCH_DELETED) {
    525    batchDeletedCookies = do_QueryInterface(aSubject);
    526  } else {
    527    cookie = do_QueryInterface(aSubject);
    528  }
    529 
    530  uint64_t browsingContextId = 0;
    531  if (aBrowsingContext) {
    532    browsingContextId = aBrowsingContext->Id();
    533  }
    534 
    535  nsCOMPtr<nsICookieNotification> notification = new CookieNotification(
    536      aAction, cookie, aBaseDomain, aIsThirdParty, batchDeletedCookies,
    537      browsingContextId, aOperationID);
    538  // Notify for topic "private-cookie-changed" or "cookie-changed"
    539  os->NotifyObservers(notification, NotificationTopic(), u"");
    540 
    541  NotifyChangedInternal(notification, aOldCookieIsSession);
    542 }
    543 
    544 void CookieStorage::RemoveCookiesFromBack(
    545    nsTArray<CookieListIter>& aCookieIters, nsCOMPtr<nsIArray>& aPurgedList) {
    546  for (auto it = aCookieIters.rbegin(); it != aCookieIters.rend(); ++it) {
    547    RefPtr<Cookie> cookie = (*it).Cookie();
    548    MOZ_ASSERT(cookie);
    549    COOKIE_LOGEVICTED(cookie, "Too many cookie bytes for this partition");
    550    RemoveCookieFromList(*it);
    551    CreateOrUpdatePurgeList(aPurgedList, cookie);
    552 
    553    // if a sole cookie is ever removed, we would remove the entire entry
    554    // but practically speaking,
    555    // we should never be in a scenario when we remove the final cookie
    556    // unless a single cookie puts us over the limit
    557    // for that to occur a user would have to adjust the CHIPS limit to be < 4
    558    // KB
    559    MOZ_ASSERT((*it).entry);
    560  }
    561 }
    562 
    563 uint32_t CookieStorage::RemoveOldestCookies(CookieEntry* aEntry, bool aSecure,
    564                                            uint32_t aBytesToRemove,
    565                                            nsCOMPtr<nsIArray>& aPurgedList) {
    566  const CookieEntry::ArrayType& cookies = aEntry->GetCookies();
    567  using MaybePurgeList = nsTArray<CookieListIter>;
    568 
    569  // note that because the maybePurgeList is populated exclusively by
    570  // pre-existing cookie list, we will never remove the cookie that is currently
    571  // being added
    572  MaybePurgeList maybePurgeList(aEntry->GetCookies().Length());
    573  for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    574    CookieListIter iter(aEntry, i);
    575    // assumes that secure cookie removal will always happen after insecure
    576    // cookie removal
    577    // Ie. this function will will only be called with aSecure == true
    578    // after it has already been called with aSecure == false
    579    if (aSecure || !iter.Cookie()->IsSecure()) {
    580      maybePurgeList.AppendElement(iter);
    581    }
    582  }
    583 
    584  // sort by age to prep oldest cookies first
    585  // since the underlying cookie list doesn't guarantee age-order because age
    586  // is primarily determined by lastAccessed, not creation time
    587  // todo: write test to assert oldest first
    588  maybePurgeList.Sort(CompareCookiesByAge());
    589 
    590  // truncate the list if we don't need to remove all cookies
    591  uint32_t bytesRemoved = 0;
    592  uint32_t count = 0;
    593  for (auto iter : maybePurgeList) {
    594    bytesRemoved += iter.Cookie()->NameAndValueBytes();
    595    count++;
    596    if (bytesRemoved >= static_cast<uint32_t>(aBytesToRemove)) {
    597      maybePurgeList.SetLength(count);
    598      break;
    599    }
    600  }
    601  // sort for safe, orderly removal (by index)
    602  // because CookieIters effectively are just pointers to the underlying cookie
    603  // list we must remove them from the back (largest index first)
    604  maybePurgeList.Sort(CompareCookiesByIndex());
    605  RemoveCookiesFromBack(maybePurgeList, aPurgedList);
    606  return bytesRemoved;
    607 }
    608 
    609 void CookieStorage::RemoveOlderCookiesByBytes(CookieEntry* aEntry,
    610                                              uint32_t removeBytes,
    611                                              nsCOMPtr<nsIArray>& aPurgedList) {
    612  MOZ_ASSERT(aEntry);
    613 
    614  // remove insecure older cookies until we are within the byte limit
    615  // (CHIPS cookies will not be detected here since they must be secure)
    616  uint32_t bytesRemoved =
    617      RemoveOldestCookies(aEntry, false, removeBytes, aPurgedList);
    618 
    619  // remove secure cookies if we still have cookies to remove
    620  if (bytesRemoved <= removeBytes) {
    621    // remove secure older cookies until we are within the byte limit
    622    MOZ_LOG(gCookieLog, LogLevel::Debug,
    623            ("Still too many cookies for partition, purging secure\n"));
    624    uint32_t bytesStillToRemove = removeBytes - bytesRemoved;
    625    RemoveOldestCookies(aEntry, true, bytesStillToRemove, aPurgedList);
    626  }
    627 }
    628 
    629 CookieStorage::ChipsLimitExcess CookieStorage::PartitionLimitExceededBytes(
    630    Cookie* aCookie, const nsACString& aBaseDomain) {
    631  uint32_t newByteCount =
    632      CountCookieBytesNotMatchingCookie(*aCookie, aBaseDomain) +
    633      aCookie->NameAndValueBytes();
    634  ChipsLimitExcess res{.hard = 0, .soft = 0};
    635  uint32_t softLimit =
    636      StaticPrefs::network_cookie_chips_partitionLimitByteCapacity();
    637  // shouldn't expect more the 4000 cookies * 4000 bytes/cookie -> 16MB
    638  uint32_t hardLimit = static_cast<uint32_t>(softLimit * kChipsHardLimitFactor);
    639  if (newByteCount > hardLimit) {
    640    res.hard = newByteCount - hardLimit;
    641    res.soft = newByteCount - softLimit;
    642  }
    643  return res;
    644 }
    645 
    646 // this is a backend function for adding a cookie to the list, via SetCookie.
    647 // also used in the cookie manager, for profile migration from IE.  it either
    648 // replaces an existing cookie; or adds the cookie to the hashtable, and
    649 // deletes a cookie (if maximum number of cookies has been reached). also
    650 // performs list maintenance by removing expired cookies.
    651 void CookieStorage::AddCookie(CookieParser* aCookieParser,
    652                              const nsACString& aBaseDomain,
    653                              const OriginAttributes& aOriginAttributes,
    654                              Cookie* aCookie, int64_t aCurrentTimeInUsec,
    655                              nsIURI* aHostURI, const nsACString& aCookieHeader,
    656                              bool aFromHttp, bool aIsThirdParty,
    657                              dom::BrowsingContext* aBrowsingContext,
    658                              const nsID* aOperationID) {
    659  if (CookieCommons::IsFirstPartyPartitionedCookieWithoutCHIPS(
    660          aCookie, aBaseDomain, aOriginAttributes)) {
    661    COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    662                      "Invalid first-party partitioned cookie without "
    663                      "partitioned cookie attribution.");
    664    MOZ_ASSERT(false);
    665    return;
    666  }
    667 
    668  int64_t currentTimeInMSec = aCurrentTimeInUsec / PR_USEC_PER_MSEC;
    669 
    670  CookieListIter exactIter{};
    671  bool foundCookie = false;
    672  foundCookie = FindCookie(aBaseDomain, aOriginAttributes, aCookie->Host(),
    673                           aCookie->Name(), aCookie->Path(), exactIter);
    674  bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
    675  bool potentiallyTrustworthy = true;
    676  if (aHostURI) {
    677    potentiallyTrustworthy =
    678        nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
    679  }
    680  bool oldCookieIsSession = false;
    681  // Step1, call FindSecureCookie(). FindSecureCookie() would
    682  // find the existing cookie with the security flag and has
    683  // the same name, host and path of the new cookie, if there is any.
    684  // Step2, Confirm new cookie's security setting. If any targeted
    685  // cookie had been found in Step1, then confirm whether the
    686  // new cookie could modify it. If the new created cookie’s
    687  // "secure-only-flag" is not set, and the "scheme" component
    688  // of the "request-uri" does not denote a "secure" protocol,
    689  // then ignore the new cookie.
    690  // (draft-ietf-httpbis-cookie-alone section 3.2)
    691  if (!aCookie->IsSecure() &&
    692      (foundSecureExact ||
    693       FindSecureCookie(aBaseDomain, aOriginAttributes, aCookie)) &&
    694      !potentiallyTrustworthy) {
    695    COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    696                      "cookie can't save because older cookie is secure "
    697                      "cookie but newer cookie is non-secure cookie");
    698    if (aCookieParser) {
    699      aCookieParser->RejectCookie(CookieParser::RejectedNonsecureOverSecure);
    700    }
    701    return;
    702  }
    703 
    704  RefPtr<Cookie> oldCookie;
    705  nsCOMPtr<nsIArray> purgedList;
    706  if (foundCookie) {
    707    oldCookie = exactIter.Cookie();
    708    oldCookieIsSession = oldCookie->IsSession();
    709 
    710    // Check if the old cookie is stale (i.e. has already expired). If so, we
    711    // need to be careful about the semantics of removing it and adding the new
    712    // cookie: we want the behavior wrt adding the new cookie to be the same as
    713    // if it didn't exist, but we still want to fire a removal notification.
    714    if (oldCookie->ExpiryInMSec() <= currentTimeInMSec) {
    715      if (aCookie->ExpiryInMSec() <= currentTimeInMSec) {
    716        // The new cookie has expired and the old one is stale. Nothing to do.
    717        COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    718                          "cookie has already expired");
    719        return;
    720      }
    721 
    722      // Remove the stale cookie. We save notification for later, once all list
    723      // modifications are complete.
    724      RemoveCookieFromList(exactIter);
    725      COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    726                        "stale cookie was purged");
    727      purgedList = CreatePurgeList(oldCookie);
    728 
    729      // We've done all we need to wrt removing and notifying the stale cookie.
    730      // From here on out, we pretend pretend it didn't exist, so that we
    731      // preserve expected notification semantics when adding the new cookie.
    732      foundCookie = false;
    733 
    734    } else {
    735      // If the old cookie is httponly, make sure we're not coming from script.
    736      if (!aFromHttp && oldCookie->IsHttpOnly()) {
    737        COOKIE_LOGFAILURE(
    738            SET_COOKIE, aHostURI, aCookieHeader,
    739            "previously stored cookie is httponly; coming from script");
    740        if (aCookieParser) {
    741          aCookieParser->RejectCookie(
    742              CookieParser::RejectedHttpOnlyButFromScript);
    743        }
    744        return;
    745      }
    746 
    747      // If the new cookie has the same value, expiry date, isSecure, isSession,
    748      // isHttpOnly and SameSite flags then we can just keep the old one.
    749      // Only if any of these differ we would want to override the cookie.
    750      if (oldCookie->Value().Equals(aCookie->Value()) &&
    751          oldCookie->ExpiryInMSec() == aCookie->ExpiryInMSec() &&
    752          oldCookie->IsSecure() == aCookie->IsSecure() &&
    753          oldCookie->IsSession() == aCookie->IsSession() &&
    754          oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
    755          oldCookie->SameSite() == aCookie->SameSite() &&
    756          oldCookie->SchemeMap() == aCookie->SchemeMap() &&
    757          // We don't want to perform this optimization if the cookie is
    758          // considered stale, since in this case we would need to update the
    759          // database.
    760          !oldCookie->IsStale()) {
    761        // Update the last access time on the old cookie.
    762        oldCookie->SetLastAccessedInUSec(aCookie->LastAccessedInUSec());
    763        UpdateCookieOldestTime(oldCookie);
    764        return;
    765      }
    766 
    767      // Merge the scheme map in case the old cookie and the new cookie are
    768      // used with different schemes.
    769      MergeCookieSchemeMap(oldCookie, aCookie);
    770 
    771      // Remove the old cookie.
    772      RemoveCookieFromList(exactIter);
    773 
    774      // If the new cookie has expired -- i.e. the intent was simply to delete
    775      // the old cookie -- then we're done.
    776      if (aCookie->ExpiryInMSec() <= currentTimeInMSec) {
    777        COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    778                          "previously stored cookie was deleted");
    779        NotifyChanged(oldCookie, nsICookieNotification::COOKIE_DELETED,
    780                      aBaseDomain, false, aBrowsingContext, oldCookieIsSession,
    781                      aOperationID);
    782        return;
    783      }
    784 
    785      // Preserve creation time of cookie for ordering purposes.
    786      aCookie->SetCreationTimeInUSec(oldCookie->CreationTimeInUSec());
    787    }
    788 
    789    // check for CHIPS-partitioned exceeding byte limit
    790    // when we overwrite a cookie with a cookie)
    791    if (CookieCommons::ChipsLimitEnabledAndChipsCookie(*aCookie,
    792                                                       aBrowsingContext)) {
    793      CookieEntry* entry =
    794          mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
    795      if (entry) {
    796        ChipsLimitExcess exceededBytes =
    797            PartitionLimitExceededBytes(aCookie, aBaseDomain);
    798        if (exceededBytes.hard > 0) {
    799          MOZ_LOG(gCookieLog, LogLevel::Debug,
    800                  ("Partition byte limit exceeded on cookie overwrite\n"));
    801          if (!StaticPrefs::network_cookie_chips_partitionLimitDryRun()) {
    802            RemoveOlderCookiesByBytes(entry, exceededBytes.soft, purgedList);
    803          }
    804          if (StaticPrefs::network_cookie_chips_partitionLimitByteCapacity() ==
    805              kChipsPartitionByteCapacityDefault) {
    806            mozilla::glean::networking::cookie_chips_partition_limit_overflow
    807                .AccumulateSingleSample(exceededBytes.hard);
    808          }
    809        }
    810      }
    811    }
    812  } else {
    813    // check if cookie has already expired
    814    if (aCookie->ExpiryInMSec() <= currentTimeInMSec) {
    815      COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
    816                        "cookie has already expired");
    817      return;
    818    }
    819 
    820    // check if we have to delete an old cookie.
    821    CookieEntry* entry =
    822        mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
    823    ChipsLimitExcess partitionLimitExceededBytes{};
    824    // we haven't yet added the new cookie so we compare cookie list with >=
    825    if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
    826      nsTArray<CookieListIter> removedIterList;
    827      // +1 to account for the cookie that we are adding,
    828      // to ensure that we end up with mCookieQuotaPerHost cookies.
    829      // "excess" should only be > 1 when prefs have been manipulated
    830      uint32_t excess = entry->GetCookies().Length() - mMaxCookiesPerHost + 1;
    831      uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost + excess;
    832      // Prioritize evicting insecure cookies.
    833      // (draft-ietf-httpbis-cookie-alone section 3.3)
    834      FindStaleCookies(entry, currentTimeInMSec, false, removedIterList, limit);
    835      if (removedIterList.Length() == 0) {
    836        if (aCookie->IsSecure()) {
    837          // It's valid to evict a secure cookie for another secure cookie.
    838          FindStaleCookies(entry, currentTimeInMSec, true, removedIterList,
    839                           limit);
    840        } else {
    841          COOKIE_LOGEVICTED(aCookie,
    842                            "Too many cookies for this domain and the new "
    843                            "cookie is not a secure cookie");
    844          return;
    845        }
    846      }
    847 
    848      MOZ_ASSERT(!removedIterList.IsEmpty());
    849      // Sort |removedIterList| by index again, since we have to remove the
    850      // cookie in the reverse order.
    851      removedIterList.Sort(CompareCookiesByIndex());
    852      for (auto it = removedIterList.rbegin(); it != removedIterList.rend();
    853           it++) {
    854        RefPtr<Cookie> evictedCookie = (*it).Cookie();
    855        COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain");
    856        RemoveCookieFromList(*it);
    857        CreateOrUpdatePurgeList(purgedList, evictedCookie);
    858        MOZ_ASSERT((*it).entry);
    859      }
    860      uint32_t purgedLength = 0;
    861      purgedList->GetLength(&purgedLength);
    862      mozilla::glean::networking::cookie_purge_entry_max.AccumulateSingleSample(
    863          purgedLength);
    864 
    865    } else if (CookieCommons::ChipsLimitEnabledAndChipsCookie(
    866                   *aCookie, aBrowsingContext) &&
    867               entry &&
    868               (partitionLimitExceededBytes =
    869                    PartitionLimitExceededBytes(aCookie, aBaseDomain))
    870                       .hard > 0) {
    871      MOZ_LOG(gCookieLog, LogLevel::Debug,
    872              ("Partition byte limit exceeded on cookie add\n"));
    873 
    874      if (!StaticPrefs::network_cookie_chips_partitionLimitDryRun()) {
    875        RemoveOlderCookiesByBytes(entry, partitionLimitExceededBytes.soft,
    876                                  purgedList);
    877      }
    878      if (StaticPrefs::network_cookie_chips_partitionLimitByteCapacity() ==
    879          kChipsPartitionByteCapacityDefault) {
    880        mozilla::glean::networking::cookie_chips_partition_limit_overflow
    881            .AccumulateSingleSample(partitionLimitExceededBytes.hard);
    882      }
    883    } else if (mCookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
    884      int64_t maxAge = aCurrentTimeInUsec - mCookieOldestTime;
    885      int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
    886      if (maxAge >= purgeAge) {
    887        // we're over both size and age limits by 10%; time to purge the table!
    888        // do this by:
    889        // 1) removing expired cookies;
    890        // 2) evicting the balance of old cookies until we reach the size limit.
    891        // note that the mCookieOldestTime indicator can be pessimistic - if
    892        // it's older than the actual oldest cookie, we'll just purge more
    893        // eagerly.
    894        purgedList = PurgeCookies(aCurrentTimeInUsec, mMaxNumberOfCookies,
    895                                  mCookiePurgeAge);
    896        uint32_t purgedLength = 0;
    897        purgedList->GetLength(&purgedLength);
    898        mozilla::glean::networking::cookie_purge_max.AccumulateSingleSample(
    899            purgedLength);
    900      }
    901    }
    902  }
    903 
    904  // Add the cookie to the db. We do not supply a params array for batching
    905  // because this might result in removals and additions being out of order.
    906  AddCookieToList(aBaseDomain, aOriginAttributes, aCookie);
    907  StoreCookie(aBaseDomain, aOriginAttributes, aCookie);
    908 
    909  COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
    910 
    911  // Now that list mutations are complete, notify observers. We do it here
    912  // because observers may themselves attempt to mutate the list.
    913  if (purgedList) {
    914    NotifyChanged(purgedList, nsICookieNotification::COOKIES_BATCH_DELETED,
    915                  ""_ns, false, nullptr, false, aOperationID);
    916  }
    917 
    918  // Notify for topic "private-cookie-changed" or "cookie-changed"
    919  NotifyChanged(aCookie,
    920                foundCookie ? nsICookieNotification::COOKIE_CHANGED
    921                            : nsICookieNotification::COOKIE_ADDED,
    922                aBaseDomain, aIsThirdParty, aBrowsingContext,
    923                oldCookieIsSession, aOperationID);
    924 }
    925 
    926 void CookieStorage::UpdateCookieOldestTime(Cookie* aCookie) {
    927  if (aCookie->LastAccessedInUSec() < mCookieOldestTime) {
    928    mCookieOldestTime = aCookie->LastAccessedInUSec();
    929  }
    930 }
    931 
    932 void CookieStorage::MergeCookieSchemeMap(Cookie* aOldCookie,
    933                                         Cookie* aNewCookie) {
    934  aNewCookie->SetSchemeMap(aOldCookie->SchemeMap() | aNewCookie->SchemeMap());
    935 }
    936 
    937 void CookieStorage::AddCookieToList(const nsACString& aBaseDomain,
    938                                    const OriginAttributes& aOriginAttributes,
    939                                    Cookie* aCookie) {
    940  if (!aCookie) {
    941    NS_WARNING("Attempting to AddCookieToList with null cookie");
    942    return;
    943  }
    944 
    945  CookieKey key(aBaseDomain, aOriginAttributes);
    946 
    947  CookieEntry* entry = mHostTable.PutEntry(key);
    948  NS_ASSERTION(entry, "can't insert element into a null entry!");
    949 
    950  entry->GetCookies().AppendElement(aCookie);
    951  ++mCookieCount;
    952 
    953  // keep track of the oldest cookie, for when it comes time to purge
    954  UpdateCookieOldestTime(aCookie);
    955 }
    956 
    957 // static
    958 already_AddRefed<nsIArray> CookieStorage::CreatePurgeList(nsICookie* aCookie) {
    959  nsCOMPtr<nsIMutableArray> removedList =
    960      do_CreateInstance(NS_ARRAY_CONTRACTID);
    961  removedList->AppendElement(aCookie);
    962  return removedList.forget();
    963 }
    964 
    965 // Given the output iter array and the count limit, find cookies
    966 // sort by expiry and lastAccessedInUSec time.
    967 // static
    968 void CookieStorage::FindStaleCookies(CookieEntry* aEntry,
    969                                     int64_t aCurrentTimeInMSec, bool aIsSecure,
    970                                     nsTArray<CookieListIter>& aOutput,
    971                                     uint32_t aLimit) {
    972  MOZ_ASSERT(aLimit);
    973 
    974  const CookieEntry::ArrayType& cookies = aEntry->GetCookies();
    975  aOutput.Clear();
    976 
    977  CookieIterComparator comp(aCurrentTimeInMSec);
    978  nsTPriorityQueue<CookieListIter, CookieIterComparator> queue(comp);
    979 
    980  for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
    981    Cookie* cookie = cookies[i];
    982 
    983    if (cookie->ExpiryInMSec() <= aCurrentTimeInMSec) {
    984      queue.Push(CookieListIter(aEntry, i));
    985      continue;
    986    }
    987 
    988    if (!aIsSecure) {
    989      // We want to look for the non-secure cookie first time through,
    990      // then find the secure cookie the second time this function is called.
    991      if (cookie->IsSecure()) {
    992        continue;
    993      }
    994    }
    995 
    996    queue.Push(CookieListIter(aEntry, i));
    997  }
    998 
    999  uint32_t count = 0;
   1000  while (!queue.IsEmpty() && count < aLimit) {
   1001    aOutput.AppendElement(queue.Pop());
   1002    count++;
   1003  }
   1004 }
   1005 
   1006 // static
   1007 void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList,
   1008                                            nsICookie* aCookie) {
   1009  if (!aPurgedList) {
   1010    COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list"));
   1011    aPurgedList = CreatePurgeList(aCookie);
   1012    return;
   1013  }
   1014 
   1015  nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(aPurgedList);
   1016  if (purgedList) {
   1017    COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list"));
   1018    purgedList->AppendElement(aCookie);
   1019  } else {
   1020    COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!"));
   1021  }
   1022 }
   1023 
   1024 // purges expired and old cookies in a batch operation.
   1025 already_AddRefed<nsIArray> CookieStorage::PurgeCookiesWithCallbacks(
   1026    int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
   1027    int64_t aCookiePurgeAge,
   1028    std::function<void(const CookieListIter&)>&& aRemoveCookieCallback,
   1029    std::function<void()>&& aFinalizeCallback) {
   1030  NS_ASSERTION(mHostTable.Count() > 0, "table is empty");
   1031 
   1032  uint32_t initialCookieCount = mCookieCount;
   1033  COOKIE_LOGSTRING(LogLevel::Debug,
   1034                   ("PurgeCookies(): beginning purge with %" PRIu32
   1035                    " cookies and %" PRId64 " oldest age",
   1036                    mCookieCount, aCurrentTimeInUsec - mCookieOldestTime));
   1037 
   1038  using PurgeList = nsTArray<CookieListIter>;
   1039  PurgeList purgeList(kMaxNumberOfCookies);
   1040 
   1041  nsCOMPtr<nsIMutableArray> removedList =
   1042      do_CreateInstance(NS_ARRAY_CONTRACTID);
   1043 
   1044  int64_t currentTimeInMSec = aCurrentTimeInUsec / PR_USEC_PER_MSEC;
   1045  int64_t purgeTime = aCurrentTimeInUsec - aCookiePurgeAge;
   1046  int64_t oldestTime = INT64_MAX;
   1047 
   1048  for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
   1049    CookieEntry* entry = iter.Get();
   1050 
   1051    const CookieEntry::ArrayType& cookies = entry->GetCookies();
   1052    auto length = cookies.Length();
   1053    for (CookieEntry::IndexType i = 0; i < length;) {
   1054      CookieListIter iter(entry, i);
   1055      Cookie* cookie = cookies[i];
   1056 
   1057      // check if the cookie has expired
   1058      if (cookie->ExpiryInMSec() <= currentTimeInMSec) {
   1059        removedList->AppendElement(cookie);
   1060        COOKIE_LOGEVICTED(cookie, "Cookie expired");
   1061 
   1062        // remove from list; do not increment our iterator, but stop if we're
   1063        // done already.
   1064        aRemoveCookieCallback(iter);
   1065        if (i == --length) {
   1066          break;
   1067        }
   1068      } else {
   1069        // check if the cookie is over the age limit
   1070        if (cookie->LastAccessedInUSec() <= purgeTime) {
   1071          purgeList.AppendElement(iter);
   1072 
   1073        } else if (cookie->LastAccessedInUSec() < oldestTime) {
   1074          // reset our indicator
   1075          oldestTime = cookie->LastAccessedInUSec();
   1076        }
   1077 
   1078        ++i;
   1079      }
   1080      MOZ_ASSERT(length == cookies.Length());
   1081    }
   1082  }
   1083 
   1084  uint32_t postExpiryCookieCount = mCookieCount;
   1085 
   1086  // now we have a list of iterators for cookies over the age limit.
   1087  // sort them by age, and then we'll see how many to remove...
   1088  purgeList.Sort(CompareCookiesByAge());
   1089 
   1090  // only remove old cookies until we reach the max cookie limit, no more.
   1091  uint32_t excess = mCookieCount > aMaxNumberOfCookies
   1092                        ? mCookieCount - aMaxNumberOfCookies
   1093                        : 0;
   1094  if (purgeList.Length() > excess) {
   1095    // We're not purging everything in the list, so update our indicator.
   1096    oldestTime = purgeList[excess].Cookie()->LastAccessedInUSec();
   1097 
   1098    purgeList.SetLength(excess);
   1099  }
   1100 
   1101  // sort the list again, this time grouping cookies with a common entryclass
   1102  // together, and with ascending index. this allows us to iterate backwards
   1103  // over the list removing cookies, without having to adjust indexes as we go.
   1104  purgeList.Sort(CompareCookiesByIndex());
   1105  for (PurgeList::index_type i = purgeList.Length(); i--;) {
   1106    Cookie* cookie = purgeList[i].Cookie();
   1107    removedList->AppendElement(cookie);
   1108    COOKIE_LOGEVICTED(cookie, "Cookie too old");
   1109 
   1110    aRemoveCookieCallback(purgeList[i]);
   1111  }
   1112 
   1113  // Update the database if we have entries to purge.
   1114  if (aFinalizeCallback) {
   1115    aFinalizeCallback();
   1116  }
   1117 
   1118  // reset the oldest time indicator
   1119  mCookieOldestTime = oldestTime;
   1120 
   1121  COOKIE_LOGSTRING(LogLevel::Debug,
   1122                   ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32
   1123                    " purged; %" PRIu32 " remain; %" PRId64 " oldest age",
   1124                    initialCookieCount - postExpiryCookieCount,
   1125                    postExpiryCookieCount - mCookieCount, mCookieCount,
   1126                    aCurrentTimeInUsec - mCookieOldestTime));
   1127 
   1128  return removedList.forget();
   1129 }
   1130 
   1131 // remove a cookie from the hashtable, and update the iterator state.
   1132 void CookieStorage::RemoveCookieFromList(const CookieListIter& aIter) {
   1133  RemoveCookieFromDB(*aIter.Cookie());
   1134  RemoveCookieFromListInternal(aIter);
   1135 }
   1136 
   1137 void CookieStorage::RemoveCookieFromListInternal(const CookieListIter& aIter) {
   1138  if (aIter.entry->GetCookies().Length() == 1) {
   1139    // we're removing the last element in the array - so just remove the entry
   1140    // from the hash. note that the entryclass' dtor will take care of
   1141    // releasing this last element for us!
   1142    mHostTable.RawRemoveEntry(aIter.entry);
   1143 
   1144  } else {
   1145    // just remove the element from the list
   1146    aIter.entry->GetCookies().RemoveElementAt(aIter.index);
   1147  }
   1148 
   1149  --mCookieCount;
   1150 }
   1151 
   1152 void CookieStorage::PrefChanged(nsIPrefBranch* aPrefBranch) {
   1153  int32_t val;
   1154  if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) {
   1155    mMaxNumberOfCookies =
   1156        static_cast<uint16_t> LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
   1157  }
   1158 
   1159  if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) {
   1160    mCookieQuotaPerHost = static_cast<uint16_t> LIMIT(
   1161        val, 1, mMaxCookiesPerHost, kCookieQuotaPerHost);
   1162  }
   1163 
   1164  if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) {
   1165    mMaxCookiesPerHost = static_cast<uint16_t> LIMIT(
   1166        val, mCookieQuotaPerHost, 0xFFFF, kMaxCookiesPerHost);
   1167  }
   1168 
   1169  if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
   1170    mCookiePurgeAge =
   1171        int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
   1172  }
   1173 }
   1174 
   1175 NS_IMETHODIMP
   1176 CookieStorage::Observe(nsISupports* aSubject, const char* aTopic,
   1177                       const char16_t* /*aData*/) {
   1178  if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
   1179    nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
   1180    if (prefBranch) {
   1181      PrefChanged(prefBranch);
   1182    }
   1183  } else if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
   1184    CollectCookieJarSizeData();
   1185  }
   1186 
   1187  return NS_OK;
   1188 }
   1189 
   1190 }  // namespace net
   1191 }  // namespace mozilla