summaryrefslogtreecommitdiffstats
path: root/netwerk/cookie
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/cookie')
-rw-r--r--netwerk/cookie/CookieServiceChild.cpp243
-rw-r--r--netwerk/cookie/CookieServiceChild.h67
-rw-r--r--netwerk/cookie/CookieServiceParent.cpp157
-rw-r--r--netwerk/cookie/CookieServiceParent.h46
-rw-r--r--netwerk/cookie/PCookieService.ipdl109
-rw-r--r--netwerk/cookie/moz.build55
-rw-r--r--netwerk/cookie/nsCookie.cpp177
-rw-r--r--netwerk/cookie/nsCookie.h140
-rw-r--r--netwerk/cookie/nsCookieService.cpp5164
-rw-r--r--netwerk/cookie/nsCookieService.h371
-rw-r--r--netwerk/cookie/nsICookie.idl85
-rw-r--r--netwerk/cookie/nsICookie2.idl63
-rw-r--r--netwerk/cookie/nsICookieManager.idl88
-rw-r--r--netwerk/cookie/nsICookieManager2.idl164
-rw-r--r--netwerk/cookie/nsICookiePermission.idl109
-rw-r--r--netwerk/cookie/nsICookieService.idl193
-rw-r--r--netwerk/cookie/test/browser/browser.ini5
-rw-r--r--netwerk/cookie/test/browser/browser_originattributes.js113
-rw-r--r--netwerk/cookie/test/browser/file_empty.html3
-rw-r--r--netwerk/cookie/test/unit/test_bug1155169.js73
-rw-r--r--netwerk/cookie/test/unit/test_bug1267910.js196
-rw-r--r--netwerk/cookie/test/unit/test_bug643051.js29
-rw-r--r--netwerk/cookie/test/unit/test_eviction.js296
-rw-r--r--netwerk/cookie/test/unit/test_parser_0001.js29
-rw-r--r--netwerk/cookie/test/unit/test_parser_0019.js29
-rw-r--r--netwerk/cookie/test/unit/xpcshell.ini10
-rw-r--r--netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js9
-rw-r--r--netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js9
-rw-r--r--netwerk/cookie/test/unit_ipc/xpcshell.ini10
29 files changed, 8042 insertions, 0 deletions
diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp
new file mode 100644
index 000000000..9a13b445c
--- /dev/null
+++ b/netwerk/cookie/CookieServiceChild.cpp
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/net/CookieServiceChild.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/NeckoChild.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsServiceManagerUtils.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace net {
+
+// Pref string constants
+static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior";
+static const char kPrefThirdPartySession[] =
+ "network.cookie.thirdparty.sessionOnly";
+
+static CookieServiceChild *gCookieService;
+
+CookieServiceChild*
+CookieServiceChild::GetSingleton()
+{
+ if (!gCookieService)
+ gCookieService = new CookieServiceChild();
+
+ NS_ADDREF(gCookieService);
+ return gCookieService;
+}
+
+NS_IMPL_ISUPPORTS(CookieServiceChild,
+ nsICookieService,
+ nsIObserver,
+ nsISupportsWeakReference)
+
+CookieServiceChild::CookieServiceChild()
+ : mCookieBehavior(nsICookieService::BEHAVIOR_ACCEPT)
+ , mThirdPartySession(false)
+{
+ NS_ASSERTION(IsNeckoChild(), "not a child process");
+
+ // This corresponds to Release() in DeallocPCookieService.
+ NS_ADDREF_THIS();
+
+ // Create a child PCookieService actor.
+ NeckoChild::InitNeckoChild();
+ gNeckoChild->SendPCookieServiceConstructor(this);
+
+ // Init our prefs and observer.
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID);
+ NS_WARNING_ASSERTION(prefBranch, "no prefservice");
+ if (prefBranch) {
+ prefBranch->AddObserver(kPrefCookieBehavior, this, true);
+ prefBranch->AddObserver(kPrefThirdPartySession, this, true);
+ PrefChanged(prefBranch);
+ }
+}
+
+CookieServiceChild::~CookieServiceChild()
+{
+ gCookieService = nullptr;
+}
+
+void
+CookieServiceChild::PrefChanged(nsIPrefBranch *aPrefBranch)
+{
+ int32_t val;
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val)))
+ mCookieBehavior =
+ val >= nsICookieService::BEHAVIOR_ACCEPT &&
+ val <= nsICookieService::BEHAVIOR_LIMIT_FOREIGN
+ ? val : nsICookieService::BEHAVIOR_ACCEPT;
+
+ bool boolval;
+ if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval)))
+ mThirdPartySession = !!boolval;
+
+ if (!mThirdPartyUtil && RequireThirdPartyCheck()) {
+ mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ NS_ASSERTION(mThirdPartyUtil, "require ThirdPartyUtil service");
+ }
+}
+
+bool
+CookieServiceChild::RequireThirdPartyCheck()
+{
+ return mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN ||
+ mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN ||
+ mThirdPartySession;
+}
+
+nsresult
+CookieServiceChild::GetCookieStringInternal(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ char **aCookieString,
+ bool aFromHttp)
+{
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG_POINTER(aCookieString);
+
+ *aCookieString = nullptr;
+
+ // Fast past: don't bother sending IPC messages about nullprincipal'd
+ // documents.
+ nsAutoCString scheme;
+ aHostURI->GetScheme(scheme);
+ if (scheme.EqualsLiteral("moz-nullprincipal"))
+ return NS_OK;
+
+ // Determine whether the request is foreign. Failure is acceptable.
+ bool isForeign = true;
+ if (RequireThirdPartyCheck())
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
+
+ URIParams uriParams;
+ SerializeURI(aHostURI, uriParams);
+
+ mozilla::NeckoOriginAttributes attrs;
+ if (aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+ if (loadInfo) {
+ attrs = loadInfo->GetOriginAttributes();
+ }
+ }
+
+ // Synchronously call the parent.
+ nsAutoCString result;
+ SendGetCookieString(uriParams, !!isForeign, aFromHttp, attrs, &result);
+ if (!result.IsEmpty())
+ *aCookieString = ToNewCString(result);
+
+ return NS_OK;
+}
+
+nsresult
+CookieServiceChild::SetCookieStringInternal(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ const char *aCookieString,
+ const char *aServerTime,
+ bool aFromHttp)
+{
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG_POINTER(aCookieString);
+
+ // Fast past: don't bother sending IPC messages about nullprincipal'd
+ // documents.
+ nsAutoCString scheme;
+ aHostURI->GetScheme(scheme);
+ if (scheme.EqualsLiteral("moz-nullprincipal"))
+ return NS_OK;
+
+ // Determine whether the request is foreign. Failure is acceptable.
+ bool isForeign = true;
+ if (RequireThirdPartyCheck())
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
+
+ nsDependentCString cookieString(aCookieString);
+ nsDependentCString serverTime;
+ if (aServerTime)
+ serverTime.Rebind(aServerTime);
+
+ URIParams uriParams;
+ SerializeURI(aHostURI, uriParams);
+
+ mozilla::NeckoOriginAttributes attrs;
+ if (aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+ if (loadInfo) {
+ attrs = loadInfo->GetOriginAttributes();
+ }
+ }
+
+ // Synchronously call the parent.
+ SendSetCookieString(uriParams, !!isForeign, cookieString, serverTime,
+ aFromHttp, attrs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::Observe(nsISupports *aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ NS_ASSERTION(strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0,
+ "not a pref change topic!");
+
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
+ if (prefBranch)
+ PrefChanged(prefBranch);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::GetCookieString(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ char **aCookieString)
+{
+ return GetCookieStringInternal(aHostURI, aChannel, aCookieString, false);
+}
+
+NS_IMETHODIMP
+CookieServiceChild::GetCookieStringFromHttp(nsIURI *aHostURI,
+ nsIURI *aFirstURI,
+ nsIChannel *aChannel,
+ char **aCookieString)
+{
+ return GetCookieStringInternal(aHostURI, aChannel, aCookieString, true);
+}
+
+NS_IMETHODIMP
+CookieServiceChild::SetCookieString(nsIURI *aHostURI,
+ nsIPrompt *aPrompt,
+ const char *aCookieString,
+ nsIChannel *aChannel)
+{
+ return SetCookieStringInternal(aHostURI, aChannel, aCookieString,
+ nullptr, false);
+}
+
+NS_IMETHODIMP
+CookieServiceChild::SetCookieStringFromHttp(nsIURI *aHostURI,
+ nsIURI *aFirstURI,
+ nsIPrompt *aPrompt,
+ const char *aCookieString,
+ const char *aServerTime,
+ nsIChannel *aChannel)
+{
+ return SetCookieStringInternal(aHostURI, aChannel, aCookieString,
+ aServerTime, true);
+}
+
+} // namespace net
+} // namespace mozilla
+
diff --git a/netwerk/cookie/CookieServiceChild.h b/netwerk/cookie/CookieServiceChild.h
new file mode 100644
index 000000000..bc6efedf4
--- /dev/null
+++ b/netwerk/cookie/CookieServiceChild.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieServiceChild_h__
+#define mozilla_net_CookieServiceChild_h__
+
+#include "mozilla/net/PCookieServiceChild.h"
+#include "nsICookieService.h"
+#include "nsIObserver.h"
+#include "nsIPrefBranch.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace net {
+
+class CookieServiceChild : public PCookieServiceChild
+ , public nsICookieService
+ , public nsIObserver
+ , public nsSupportsWeakReference
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOOKIESERVICE
+ NS_DECL_NSIOBSERVER
+
+ CookieServiceChild();
+
+ static CookieServiceChild* GetSingleton();
+
+protected:
+ virtual ~CookieServiceChild();
+
+ void SerializeURIs(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ nsCString &aHostSpec,
+ nsCString &aHostCharset,
+ nsCString &aOriginatingSpec,
+ nsCString &aOriginatingCharset);
+
+ nsresult GetCookieStringInternal(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ char **aCookieString,
+ bool aFromHttp);
+
+ nsresult SetCookieStringInternal(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ const char *aCookieString,
+ const char *aServerTime,
+ bool aFromHttp);
+
+ void PrefChanged(nsIPrefBranch *aPrefBranch);
+
+ bool RequireThirdPartyCheck();
+
+ nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil;
+ uint8_t mCookieBehavior;
+ bool mThirdPartySession;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieServiceChild_h__
+
diff --git a/netwerk/cookie/CookieServiceParent.cpp b/netwerk/cookie/CookieServiceParent.cpp
new file mode 100644
index 000000000..005ef44b4
--- /dev/null
+++ b/netwerk/cookie/CookieServiceParent.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/net/CookieServiceParent.h"
+#include "mozilla/dom/PContentParent.h"
+#include "mozilla/net/NeckoParent.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "nsCookieService.h"
+#include "nsIChannel.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsNetCID.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla::ipc;
+using mozilla::BasePrincipal;
+using mozilla::NeckoOriginAttributes;
+using mozilla::PrincipalOriginAttributes;
+using mozilla::dom::PContentParent;
+using mozilla::net::NeckoParent;
+
+namespace {
+
+// Ignore failures from this function, as they only affect whether we do or
+// don't show a dialog box in private browsing mode if the user sets a pref.
+void
+CreateDummyChannel(nsIURI* aHostURI, NeckoOriginAttributes& aAttrs, bool aIsPrivate,
+ nsIChannel** aChannel)
+{
+ MOZ_ASSERT(aAttrs.mAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID);
+
+ PrincipalOriginAttributes attrs;
+ attrs.InheritFromNecko(aAttrs);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateCodebasePrincipal(aHostURI, attrs);
+ if (!principal) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> dummyURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(dummyURI), "about:blank");
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // The following channel is never openend, so it does not matter what
+ // securityFlags we pass; let's follow the principle of least privilege.
+ nsCOMPtr<nsIChannel> dummyChannel;
+ NS_NewChannel(getter_AddRefs(dummyChannel), dummyURI, principal,
+ nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+ nsIContentPolicy::TYPE_INVALID);
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(dummyChannel);
+ if (!pbChannel) {
+ return;
+ }
+
+ pbChannel->SetPrivate(aIsPrivate);
+ dummyChannel.forget(aChannel);
+ return;
+}
+
+}
+
+namespace mozilla {
+namespace net {
+
+CookieServiceParent::CookieServiceParent()
+{
+ // Instantiate the cookieservice via the service manager, so it sticks around
+ // until shutdown.
+ nsCOMPtr<nsICookieService> cs = do_GetService(NS_COOKIESERVICE_CONTRACTID);
+
+ // Get the nsCookieService instance directly, so we can call internal methods.
+ mCookieService =
+ already_AddRefed<nsCookieService>(nsCookieService::GetSingleton());
+ NS_ASSERTION(mCookieService, "couldn't get nsICookieService");
+}
+
+CookieServiceParent::~CookieServiceParent()
+{
+}
+
+void
+CookieServiceParent::ActorDestroy(ActorDestroyReason aWhy)
+{
+ // Nothing needed here. Called right before destructor since this is a
+ // non-refcounted class.
+}
+
+bool
+CookieServiceParent::RecvGetCookieString(const URIParams& aHost,
+ const bool& aIsForeign,
+ const bool& aFromHttp,
+ const NeckoOriginAttributes& aAttrs,
+ nsCString* aResult)
+{
+ if (!mCookieService)
+ return true;
+
+ // Deserialize URI. Having a host URI is mandatory and should always be
+ // provided by the child; thus we consider failure fatal.
+ nsCOMPtr<nsIURI> hostURI = DeserializeURI(aHost);
+ if (!hostURI)
+ return false;
+
+ bool isPrivate = aAttrs.mPrivateBrowsingId > 0;
+ mCookieService->GetCookieStringInternal(hostURI, aIsForeign, aFromHttp, aAttrs,
+ isPrivate, *aResult);
+ return true;
+}
+
+bool
+CookieServiceParent::RecvSetCookieString(const URIParams& aHost,
+ const bool& aIsForeign,
+ const nsCString& aCookieString,
+ const nsCString& aServerTime,
+ const bool& aFromHttp,
+ const NeckoOriginAttributes& aAttrs)
+{
+ if (!mCookieService)
+ return true;
+
+ // Deserialize URI. Having a host URI is mandatory and should always be
+ // provided by the child; thus we consider failure fatal.
+ nsCOMPtr<nsIURI> hostURI = DeserializeURI(aHost);
+ if (!hostURI)
+ return false;
+
+ bool isPrivate = aAttrs.mPrivateBrowsingId > 0;
+
+ // This is a gross hack. We've already computed everything we need to know
+ // for whether to set this cookie or not, but we need to communicate all of
+ // this information through to nsICookiePermission, which indirectly
+ // computes the information from the channel. We only care about the
+ // aIsPrivate argument as nsCookieService::SetCookieStringInternal deals
+ // with aIsForeign before we have to worry about nsCookiePermission trying
+ // to use the channel to inspect it.
+ nsCOMPtr<nsIChannel> dummyChannel;
+ CreateDummyChannel(hostURI, const_cast<NeckoOriginAttributes&>(aAttrs),
+ isPrivate, getter_AddRefs(dummyChannel));
+
+ // NB: dummyChannel could be null if something failed in CreateDummyChannel.
+ nsDependentCString cookieString(aCookieString, 0);
+ mCookieService->SetCookieStringInternal(hostURI, aIsForeign, cookieString,
+ aServerTime, aFromHttp, aAttrs,
+ isPrivate, dummyChannel);
+ return true;
+}
+
+} // namespace net
+} // namespace mozilla
+
diff --git a/netwerk/cookie/CookieServiceParent.h b/netwerk/cookie/CookieServiceParent.h
new file mode 100644
index 000000000..7be2c97e9
--- /dev/null
+++ b/netwerk/cookie/CookieServiceParent.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieServiceParent_h
+#define mozilla_net_CookieServiceParent_h
+
+#include "mozilla/net/PCookieServiceParent.h"
+
+class nsCookieService;
+namespace mozilla { class NeckoOriginAttributes; }
+
+namespace mozilla {
+namespace net {
+
+class CookieServiceParent : public PCookieServiceParent
+{
+public:
+ CookieServiceParent();
+ virtual ~CookieServiceParent();
+
+protected:
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual bool RecvGetCookieString(const URIParams& aHost,
+ const bool& aIsForeign,
+ const bool& aFromHttp,
+ const NeckoOriginAttributes& aAttrs,
+ nsCString* aResult) override;
+
+ virtual bool RecvSetCookieString(const URIParams& aHost,
+ const bool& aIsForeign,
+ const nsCString& aCookieString,
+ const nsCString& aServerTime,
+ const bool& aFromHttp,
+ const NeckoOriginAttributes& aAttrs) override;
+
+ RefPtr<nsCookieService> mCookieService;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieServiceParent_h
+
diff --git a/netwerk/cookie/PCookieService.ipdl b/netwerk/cookie/PCookieService.ipdl
new file mode 100644
index 000000000..7d01096d4
--- /dev/null
+++ b/netwerk/cookie/PCookieService.ipdl
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PNecko;
+include URIParams;
+
+using mozilla::NeckoOriginAttributes from "mozilla/ipc/BackgroundUtils.h";
+
+namespace mozilla {
+namespace net {
+
+/**
+ * PCookieService
+ *
+ * Provides IPDL methods for setting and getting cookies. These are stored on
+ * and managed by the parent; the child process goes through the parent for
+ * all cookie operations. Lower-level programmatic operations (i.e. those
+ * provided by the nsICookieManager and nsICookieManager2 interfaces) are not
+ * currently implemented and requesting these interfaces in the child will fail.
+ *
+ * @see nsICookieService
+ * @see nsICookiePermission
+ */
+
+nested(upto inside_cpow) sync protocol PCookieService
+{
+ manager PNecko;
+
+parent:
+
+ /*
+ * Get the complete cookie string associated with the URI. This is a sync
+ * call in order to avoid race conditions -- for instance, an HTTP response
+ * on the parent and script access on the child.
+ *
+ * @param host
+ * Same as the 'aURI' argument to nsICookieService.getCookieString.
+ * @param isForeign
+ * True if the the request is third party, for purposes of allowing
+ * access to cookies. This should be obtained from
+ * mozIThirdPartyUtil.isThirdPartyChannel. Third party requests may be
+ * rejected depending on user preferences; if those checks are
+ * disabled, this parameter is ignored.
+ * @param fromHttp
+ * Whether the result is for an HTTP request header. This should be
+ * true for nsICookieService.getCookieStringFromHttp calls, false
+ * otherwise.
+ * @param attrs
+ * The origin attributes from the HTTP channel or document that the
+ * cookie is being set on.
+ *
+ * @see nsICookieService.getCookieString
+ * @see nsICookieService.getCookieStringFromHttp
+ * @see mozIThirdPartyUtil.isThirdPartyChannel
+ *
+ * @return the resulting cookie string.
+ */
+ nested(inside_cpow) sync GetCookieString(URIParams host,
+ bool isForeign,
+ bool fromHttp,
+ NeckoOriginAttributes attrs)
+ returns (nsCString result);
+
+ /*
+ * Set a cookie string.
+ *
+ * @param host
+ * Same as the 'aURI' argument to nsICookieService.setCookieString.
+ * @param isForeign
+ * True if the the request is third party, for purposes of allowing
+ * access to cookies. This should be obtained from
+ * mozIThirdPartyUtil.isThirdPartyChannel. Third party requests may be
+ * rejected depending on user preferences; if those checks are
+ * disabled, this parameter is ignored.
+ * @param cookieString
+ * Same as the 'aCookie' argument to nsICookieService.setCookieString.
+ * @param serverTime
+ * Same as the 'aServerTime' argument to
+ * nsICookieService.setCookieStringFromHttp. If the string is empty or
+ * null (e.g. for non-HTTP requests), the current local time is used.
+ * @param fromHttp
+ * Whether the result is for an HTTP request header. This should be
+ * true for nsICookieService.setCookieStringFromHttp calls, false
+ * otherwise.
+ * @param attrs
+ * The origin attributes from the HTTP channel or document that the
+ * cookie is being set on.
+ *
+ * @see nsICookieService.setCookieString
+ * @see nsICookieService.setCookieStringFromHttp
+ * @see mozIThirdPartyUtil.isThirdPartyChannel
+ */
+ nested(inside_cpow) async SetCookieString(URIParams host,
+ bool isForeign,
+ nsCString cookieString,
+ nsCString serverTime,
+ bool fromHttp,
+ NeckoOriginAttributes attrs);
+
+ async __delete__();
+};
+
+}
+}
+
diff --git a/netwerk/cookie/moz.build b/netwerk/cookie/moz.build
new file mode 100644
index 000000000..207790008
--- /dev/null
+++ b/netwerk/cookie/moz.build
@@ -0,0 +1,55 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# export required interfaces, even if --disable-cookies has been given
+XPIDL_SOURCES += [
+ 'nsICookie.idl',
+ 'nsICookie2.idl',
+ 'nsICookieManager.idl',
+ 'nsICookieManager2.idl',
+ 'nsICookiePermission.idl',
+ 'nsICookieService.idl',
+]
+
+XPIDL_MODULE = 'necko_cookie'
+
+if CONFIG['NECKO_COOKIES']:
+ EXPORTS.mozilla.net = [
+ 'CookieServiceChild.h',
+ 'CookieServiceParent.h',
+ ]
+ UNIFIED_SOURCES += [
+ 'CookieServiceChild.cpp',
+ 'CookieServiceParent.cpp',
+ 'nsCookie.cpp',
+ ]
+ # nsCookieService.cpp can't be unified because of symbol conflicts
+ SOURCES += [
+ 'nsCookieService.cpp',
+ ]
+ LOCAL_INCLUDES += [
+ '/intl/uconv',
+ ]
+
+ XPCSHELL_TESTS_MANIFESTS += [
+ 'test/unit/xpcshell.ini',
+ 'test/unit_ipc/xpcshell.ini',
+ ]
+
+ BROWSER_CHROME_MANIFESTS += [
+ 'test/browser/browser.ini',
+ ]
+
+IPDL_SOURCES = [
+ 'PCookieService.ipdl',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/netwerk/cookie/nsCookie.cpp b/netwerk/cookie/nsCookie.cpp
new file mode 100644
index 000000000..5afe6fe80
--- /dev/null
+++ b/netwerk/cookie/nsCookie.cpp
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ToJSValue.h"
+#include "nsAutoPtr.h"
+#include "nsCookie.h"
+#include "nsUTF8ConverterService.h"
+#include <stdlib.h>
+
+/******************************************************************************
+ * nsCookie:
+ * string helper impl
+ ******************************************************************************/
+
+// copy aSource strings into contiguous storage provided in aDest1,
+// providing terminating nulls for each destination string.
+static inline void
+StrBlockCopy(const nsACString &aSource1,
+ const nsACString &aSource2,
+ const nsACString &aSource3,
+ const nsACString &aSource4,
+ char *&aDest1,
+ char *&aDest2,
+ char *&aDest3,
+ char *&aDest4,
+ char *&aDestEnd)
+{
+ char *toBegin = aDest1;
+ nsACString::const_iterator fromBegin, fromEnd;
+
+ *copy_string(aSource1.BeginReading(fromBegin), aSource1.EndReading(fromEnd), toBegin) = char(0);
+ aDest2 = ++toBegin;
+ *copy_string(aSource2.BeginReading(fromBegin), aSource2.EndReading(fromEnd), toBegin) = char(0);
+ aDest3 = ++toBegin;
+ *copy_string(aSource3.BeginReading(fromBegin), aSource3.EndReading(fromEnd), toBegin) = char(0);
+ aDest4 = ++toBegin;
+ *copy_string(aSource4.BeginReading(fromBegin), aSource4.EndReading(fromEnd), toBegin) = char(0);
+ aDestEnd = toBegin;
+}
+
+/******************************************************************************
+ * nsCookie:
+ * creation helper
+ ******************************************************************************/
+
+// This is a counter that keeps track of the last used creation time, each time
+// we create a new nsCookie. This is nominally the time (in microseconds) the
+// cookie was created, but is guaranteed to be monotonically increasing for
+// cookies added at runtime after the database has been read in. This is
+// necessary to enforce ordering among cookies whose creation times would
+// otherwise overlap, since it's possible two cookies may be created at the same
+// time, or that the system clock isn't monotonic.
+static int64_t gLastCreationTime;
+
+int64_t
+nsCookie::GenerateUniqueCreationTime(int64_t aCreationTime)
+{
+ // Check if the creation time given to us is greater than the running maximum
+ // (it should always be monotonically increasing).
+ if (aCreationTime > gLastCreationTime) {
+ gLastCreationTime = aCreationTime;
+ return aCreationTime;
+ }
+
+ // Make up our own.
+ return ++gLastCreationTime;
+}
+
+nsCookie *
+nsCookie::Create(const nsACString &aName,
+ const nsACString &aValue,
+ const nsACString &aHost,
+ const nsACString &aPath,
+ int64_t aExpiry,
+ int64_t aLastAccessed,
+ int64_t aCreationTime,
+ bool aIsSession,
+ bool aIsSecure,
+ bool aIsHttpOnly,
+ const OriginAttributes& aOriginAttributes)
+{
+ // Ensure mValue contains a valid UTF-8 sequence. Otherwise XPConnect will
+ // truncate the string after the first invalid octet.
+ RefPtr<nsUTF8ConverterService> converter = new nsUTF8ConverterService();
+ nsAutoCString aUTF8Value;
+ converter->ConvertStringToUTF8(aValue, "UTF-8", false, true, 1, aUTF8Value);
+
+ // find the required string buffer size, adding 4 for the terminating nulls
+ const uint32_t stringLength = aName.Length() + aUTF8Value.Length() +
+ aHost.Length() + aPath.Length() + 4;
+
+ // allocate contiguous space for the nsCookie and its strings -
+ // we store the strings in-line with the nsCookie to save allocations
+ void *place = ::operator new(sizeof(nsCookie) + stringLength);
+ if (!place)
+ return nullptr;
+
+ // assign string members
+ char *name, *value, *host, *path, *end;
+ name = static_cast<char *>(place) + sizeof(nsCookie);
+ StrBlockCopy(aName, aUTF8Value, aHost, aPath,
+ name, value, host, path, end);
+
+ // If the creationTime given to us is higher than the running maximum, update
+ // our maximum.
+ if (aCreationTime > gLastCreationTime)
+ gLastCreationTime = aCreationTime;
+
+ // construct the cookie. placement new, oh yeah!
+ return new (place) nsCookie(name, value, host, path, end,
+ aExpiry, aLastAccessed, aCreationTime,
+ aIsSession, aIsSecure, aIsHttpOnly,
+ aOriginAttributes);
+}
+
+size_t
+nsCookie::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ // There is no need to measure the sizes of the individual string
+ // members, since the strings are stored in-line with the nsCookie.
+ return aMallocSizeOf(this);
+}
+
+bool
+nsCookie::IsStale() const
+{
+ int64_t currentTimeInUsec = PR_Now();
+
+ return currentTimeInUsec - LastAccessed() > mCookieStaleThreshold * PR_USEC_PER_SEC;
+}
+
+/******************************************************************************
+ * nsCookie:
+ * xpcom impl
+ ******************************************************************************/
+
+// xpcom getters
+NS_IMETHODIMP nsCookie::GetName(nsACString &aName) { aName = Name(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetValue(nsACString &aValue) { aValue = Value(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetHost(nsACString &aHost) { aHost = Host(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetRawHost(nsACString &aHost) { aHost = RawHost(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetPath(nsACString &aPath) { aPath = Path(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetExpiry(int64_t *aExpiry) { *aExpiry = Expiry(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetIsSession(bool *aIsSession) { *aIsSession = IsSession(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetIsDomain(bool *aIsDomain) { *aIsDomain = IsDomain(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetIsSecure(bool *aIsSecure) { *aIsSecure = IsSecure(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetIsHttpOnly(bool *aHttpOnly) { *aHttpOnly = IsHttpOnly(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetStatus(nsCookieStatus *aStatus) { *aStatus = 0; return NS_OK; }
+NS_IMETHODIMP nsCookie::GetPolicy(nsCookiePolicy *aPolicy) { *aPolicy = 0; return NS_OK; }
+NS_IMETHODIMP nsCookie::GetCreationTime(int64_t *aCreation){ *aCreation = CreationTime(); return NS_OK; }
+NS_IMETHODIMP nsCookie::GetLastAccessed(int64_t *aTime) { *aTime = LastAccessed(); return NS_OK; }
+
+NS_IMETHODIMP
+nsCookie::GetOriginAttributes(JSContext *aCx, JS::MutableHandle<JS::Value> aVal)
+{
+ if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+// compatibility method, for use with the legacy nsICookie interface.
+// here, expires == 0 denotes a session cookie.
+NS_IMETHODIMP
+nsCookie::GetExpires(uint64_t *aExpires)
+{
+ if (IsSession()) {
+ *aExpires = 0;
+ } else {
+ *aExpires = Expiry() > 0 ? Expiry() : 1;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsCookie, nsICookie2, nsICookie)
diff --git a/netwerk/cookie/nsCookie.h b/netwerk/cookie/nsCookie.h
new file mode 100644
index 000000000..812db3f32
--- /dev/null
+++ b/netwerk/cookie/nsCookie.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCookie_h__
+#define nsCookie_h__
+
+#include "nsICookie.h"
+#include "nsICookie2.h"
+#include "nsString.h"
+
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Preferences.h"
+
+using mozilla::OriginAttributes;
+
+/**
+ * The nsCookie class is the main cookie storage medium for use within cookie
+ * code. It implements nsICookie2, which extends nsICookie, a frozen interface
+ * for xpcom access of cookie objects.
+ */
+
+/******************************************************************************
+ * nsCookie:
+ * implementation
+ ******************************************************************************/
+
+class nsCookie : public nsICookie2
+{
+ public:
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOOKIE
+ NS_DECL_NSICOOKIE2
+
+ private:
+ // for internal use only. see nsCookie::Create().
+ nsCookie(const char *aName,
+ const char *aValue,
+ const char *aHost,
+ const char *aPath,
+ const char *aEnd,
+ int64_t aExpiry,
+ int64_t aLastAccessed,
+ int64_t aCreationTime,
+ bool aIsSession,
+ bool aIsSecure,
+ bool aIsHttpOnly,
+ const OriginAttributes& aOriginAttributes)
+ : mName(aName)
+ , mValue(aValue)
+ , mHost(aHost)
+ , mPath(aPath)
+ , mEnd(aEnd)
+ , mExpiry(aExpiry)
+ , mLastAccessed(aLastAccessed)
+ , mCreationTime(aCreationTime)
+ // Defaults to 60s
+ , mCookieStaleThreshold(mozilla::Preferences::GetInt("network.cookie.staleThreshold", 60))
+ , mIsSession(aIsSession)
+ , mIsSecure(aIsSecure)
+ , mIsHttpOnly(aIsHttpOnly)
+ , mOriginAttributes(aOriginAttributes)
+ {
+ }
+
+ public:
+ // Generate a unique and monotonically increasing creation time. See comment
+ // in nsCookie.cpp.
+ static int64_t GenerateUniqueCreationTime(int64_t aCreationTime);
+
+ // public helper to create an nsCookie object. use |operator delete|
+ // to destroy an object created by this method.
+ static nsCookie * Create(const nsACString &aName,
+ const nsACString &aValue,
+ const nsACString &aHost,
+ const nsACString &aPath,
+ int64_t aExpiry,
+ int64_t aLastAccessed,
+ int64_t aCreationTime,
+ bool aIsSession,
+ bool aIsSecure,
+ bool aIsHttpOnly,
+ const OriginAttributes& aOriginAttributes);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ // fast (inline, non-xpcom) getters
+ inline const nsDependentCString Name() const { return nsDependentCString(mName, mValue - 1); }
+ inline const nsDependentCString Value() const { return nsDependentCString(mValue, mHost - 1); }
+ inline const nsDependentCString Host() const { return nsDependentCString(mHost, mPath - 1); }
+ inline const nsDependentCString RawHost() const { return nsDependentCString(IsDomain() ? mHost + 1 : mHost, mPath - 1); }
+ inline const nsDependentCString Path() const { return nsDependentCString(mPath, mEnd); }
+ inline int64_t Expiry() const { return mExpiry; } // in seconds
+ inline int64_t LastAccessed() const { return mLastAccessed; } // in microseconds
+ inline int64_t CreationTime() const { return mCreationTime; } // in microseconds
+ inline bool IsSession() const { return mIsSession; }
+ inline bool IsDomain() const { return *mHost == '.'; }
+ inline bool IsSecure() const { return mIsSecure; }
+ inline bool IsHttpOnly() const { return mIsHttpOnly; }
+
+ // setters
+ inline void SetExpiry(int64_t aExpiry) { mExpiry = aExpiry; }
+ inline void SetLastAccessed(int64_t aTime) { mLastAccessed = aTime; }
+ inline void SetIsSession(bool aIsSession) { mIsSession = aIsSession; }
+ // Set the creation time manually, overriding the monotonicity checks in
+ // Create(). Use with caution!
+ inline void SetCreationTime(int64_t aTime) { mCreationTime = aTime; }
+
+ bool IsStale() const;
+
+ protected:
+ virtual ~nsCookie() {}
+
+ private:
+ // member variables
+ // we use char* ptrs to store the strings in a contiguous block,
+ // so we save on the overhead of using nsCStrings. However, we
+ // store a terminating null for each string, so we can hand them
+ // out as nsAFlatCStrings.
+ //
+ // Please update SizeOfIncludingThis if this strategy changes.
+ const char *mName;
+ const char *mValue;
+ const char *mHost;
+ const char *mPath;
+ const char *mEnd;
+ int64_t mExpiry;
+ int64_t mLastAccessed;
+ int64_t mCreationTime;
+ int64_t mCookieStaleThreshold;
+ bool mIsSession;
+ bool mIsSecure;
+ bool mIsHttpOnly;
+ mozilla::OriginAttributes mOriginAttributes;
+};
+
+#endif // nsCookie_h__
diff --git a/netwerk/cookie/nsCookieService.cpp b/netwerk/cookie/nsCookieService.cpp
new file mode 100644
index 000000000..cf1d91e2d
--- /dev/null
+++ b/netwerk/cookie/nsCookieService.cpp
@@ -0,0 +1,5164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Likely.h"
+#include "mozilla/Unused.h"
+
+#include "mozilla/net/CookieServiceChild.h"
+#include "mozilla/net/NeckoCommon.h"
+
+#include "nsCookieService.h"
+#include "nsContentUtils.h"
+#include "nsIServiceManager.h"
+
+#include "nsIIOService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIScriptError.h"
+#include "nsICookiePermission.h"
+#include "nsIURI.h"
+#include "nsIURL.h"
+#include "nsIChannel.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsILineInputStream.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIIDNService.h"
+#include "mozIThirdPartyUtil.h"
+
+#include "nsTArray.h"
+#include "nsCOMArray.h"
+#include "nsIMutableArray.h"
+#include "nsArrayEnumerator.h"
+#include "nsEnumeratorUtils.h"
+#include "nsAutoPtr.h"
+#include "nsReadableUtils.h"
+#include "nsCRT.h"
+#include "prprf.h"
+#include "nsNetUtil.h"
+#include "nsNetCID.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIInputStream.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsNetCID.h"
+#include "mozilla/storage.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/FileUtils.h"
+#include "mozilla/Telemetry.h"
+#include "nsIAppsService.h"
+#include "mozIApplication.h"
+#include "mozIApplicationClearPrivateDataParams.h"
+#include "nsIConsoleService.h"
+#include "nsVariant.h"
+
+using namespace mozilla;
+using namespace mozilla::net;
+
+// Create key from baseDomain that will access the default cookie namespace.
+// TODO: When we figure out what the API will look like for nsICookieManager{2}
+// on content processes (see bug 777620), change to use the appropriate app
+// namespace. For now those IDLs aren't supported on child processes.
+#define DEFAULT_APP_KEY(baseDomain) \
+ nsCookieKey(baseDomain, NeckoOriginAttributes())
+
+/******************************************************************************
+ * nsCookieService impl:
+ * useful types & constants
+ ******************************************************************************/
+
+static nsCookieService *gCookieService;
+
+// XXX_hack. See bug 178993.
+// This is a hack to hide HttpOnly cookies from older browsers
+#define HTTP_ONLY_PREFIX "#HttpOnly_"
+
+#define COOKIES_FILE "cookies.sqlite"
+#define COOKIES_SCHEMA_VERSION 7
+
+// parameter indexes; see EnsureReadDomain, EnsureReadComplete and
+// ReadCookieDBListener::HandleResult
+#define IDX_NAME 0
+#define IDX_VALUE 1
+#define IDX_HOST 2
+#define IDX_PATH 3
+#define IDX_EXPIRY 4
+#define IDX_LAST_ACCESSED 5
+#define IDX_CREATION_TIME 6
+#define IDX_SECURE 7
+#define IDX_HTTPONLY 8
+#define IDX_BASE_DOMAIN 9
+#define IDX_ORIGIN_ATTRIBUTES 10
+
+static const int64_t kCookiePurgeAge =
+ int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds
+
+#define OLD_COOKIE_FILE_NAME "cookies.txt"
+
+#undef LIMIT
+#define LIMIT(x, low, high, default) ((x) >= (low) && (x) <= (high) ? (x) : (default))
+
+#undef ADD_TEN_PERCENT
+#define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i)/10)
+
+// default limits for the cookie list. these can be tuned by the
+// network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively.
+static const uint32_t kMaxNumberOfCookies = 3000;
+static const uint32_t kMaxCookiesPerHost = 150;
+static const uint32_t kMaxBytesPerCookie = 4096;
+static const uint32_t kMaxBytesPerPath = 1024;
+
+// pref string constants
+static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior";
+static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber";
+static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost";
+static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge";
+static const char kPrefThirdPartySession[] = "network.cookie.thirdparty.sessionOnly";
+static const char kCookieLeaveSecurityAlone[] = "network.cookie.leave-secure-alone";
+
+// For telemetry COOKIE_LEAVE_SECURE_ALONE
+#define BLOCKED_SECURE_SET_FROM_HTTP 0
+#define BLOCKED_DOWNGRADE_SECURE 1
+#define DOWNGRADE_SECURE_FROM_SECURE 2
+#define EVICTED_NEWER_INSECURE 3
+#define EVICTED_OLDEST_COOKIE 4
+#define EVICTED_PREFERRED_COOKIE 5
+#define EVICTING_SECURE_BLOCKED 6
+
+static void
+bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray,
+ const nsCookieKey &aKey,
+ const nsCookie *aCookie);
+
+// struct for temporarily storing cookie attributes during header parsing
+struct nsCookieAttributes
+{
+ nsAutoCString name;
+ nsAutoCString value;
+ nsAutoCString host;
+ nsAutoCString path;
+ nsAutoCString expires;
+ nsAutoCString maxage;
+ int64_t expiryTime;
+ bool isSession;
+ bool isSecure;
+ bool isHttpOnly;
+};
+
+// stores the nsCookieEntry entryclass and an index into the cookie array
+// within that entryclass, for purposes of storing an iteration state that
+// points to a certain cookie.
+struct nsListIter
+{
+ // default (non-initializing) constructor.
+ nsListIter() = default;
+
+ // explicit constructor to a given iterator state with entryclass 'aEntry'
+ // and index 'aIndex'.
+ explicit
+ nsListIter(nsCookieEntry *aEntry, nsCookieEntry::IndexType aIndex)
+ : entry(aEntry)
+ , index(aIndex)
+ {
+ }
+
+ // get the nsCookie * the iterator currently points to.
+ nsCookie * Cookie() const
+ {
+ return entry->GetCookies()[index];
+ }
+
+ nsCookieEntry *entry;
+ nsCookieEntry::IndexType index;
+};
+
+/******************************************************************************
+ * Cookie logging handlers
+ * used for logging in nsCookieService
+ ******************************************************************************/
+
+// logging handlers
+#ifdef MOZ_LOGGING
+// in order to do logging, the following environment variables need to be set:
+//
+// set MOZ_LOG=cookie:3 -- shows rejected cookies
+// set MOZ_LOG=cookie:4 -- shows accepted and rejected cookies
+// set MOZ_LOG_FILE=cookie.log
+//
+#include "mozilla/Logging.h"
+#endif
+
+// define logging macros for convenience
+#define SET_COOKIE true
+#define GET_COOKIE false
+
+static LazyLogModule gCookieLog("cookie");
+
+#define COOKIE_LOGFAILURE(a, b, c, d) LogFailure(a, b, c, d)
+#define COOKIE_LOGSUCCESS(a, b, c, d, e) LogSuccess(a, b, c, d, e)
+
+#define COOKIE_LOGEVICTED(a, details) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) \
+ LogEvicted(a, details); \
+ PR_END_MACRO
+
+#define COOKIE_LOGSTRING(lvl, fmt) \
+ PR_BEGIN_MACRO \
+ MOZ_LOG(gCookieLog, lvl, fmt); \
+ MOZ_LOG(gCookieLog, lvl, ("\n")); \
+ PR_END_MACRO
+
+static void
+LogFailure(bool aSetCookie, nsIURI *aHostURI, const char *aCookieString, const char *aReason)
+{
+ // if logging isn't enabled, return now to save cycles
+ if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Warning))
+ return;
+
+ nsAutoCString spec;
+ if (aHostURI)
+ aHostURI->GetAsciiSpec(spec);
+
+ MOZ_LOG(gCookieLog, LogLevel::Warning,
+ ("===== %s =====\n", aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT"));
+ MOZ_LOG(gCookieLog, LogLevel::Warning,("request URL: %s\n", spec.get()));
+ if (aSetCookie)
+ MOZ_LOG(gCookieLog, LogLevel::Warning,("cookie string: %s\n", aCookieString));
+
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
+ char timeString[40];
+ PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
+
+ MOZ_LOG(gCookieLog, LogLevel::Warning,("current time: %s", timeString));
+ MOZ_LOG(gCookieLog, LogLevel::Warning,("rejected because %s\n", aReason));
+ MOZ_LOG(gCookieLog, LogLevel::Warning,("\n"));
+}
+
+static void
+LogCookie(nsCookie *aCookie)
+{
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
+ char timeString[40];
+ PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("current time: %s", timeString));
+
+ if (aCookie) {
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("----------------\n"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("name: %s\n", aCookie->Name().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("value: %s\n", aCookie->Value().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("%s: %s\n", aCookie->IsDomain() ? "domain" : "host", aCookie->Host().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("path: %s\n", aCookie->Path().get()));
+
+ PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC),
+ PR_GMTParameters, &explodedTime);
+ PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("expires: %s%s", timeString, aCookie->IsSession() ? " (at end of session)" : ""));
+
+ PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime);
+ PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("created: %s", timeString));
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("is secure: %s\n", aCookie->IsSecure() ? "true" : "false"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false"));
+ }
+}
+
+static void
+LogSuccess(bool aSetCookie, nsIURI *aHostURI, const char *aCookieString, nsCookie *aCookie, bool aReplacing)
+{
+ // if logging isn't enabled, return now to save cycles
+ if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) {
+ return;
+ }
+
+ nsAutoCString spec;
+ if (aHostURI)
+ aHostURI->GetAsciiSpec(spec);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("request URL: %s\n", spec.get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("cookie string: %s\n", aCookieString));
+ if (aSetCookie)
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("replaces existing cookie: %s\n", aReplacing ? "true" : "false"));
+
+ LogCookie(aCookie);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("\n"));
+}
+
+static void
+LogEvicted(nsCookie *aCookie, const char* details)
+{
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("===== COOKIE EVICTED =====\n"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("%s\n", details));
+
+ LogCookie(aCookie);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,("\n"));
+}
+
+// inline wrappers to make passing in nsAFlatCStrings easier
+static inline void
+LogFailure(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, const char *aReason)
+{
+ LogFailure(aSetCookie, aHostURI, aCookieString.get(), aReason);
+}
+
+static inline void
+LogSuccess(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, nsCookie *aCookie, bool aReplacing)
+{
+ LogSuccess(aSetCookie, aHostURI, aCookieString.get(), aCookie, aReplacing);
+}
+
+#ifdef DEBUG
+#define NS_ASSERT_SUCCESS(res) \
+ PR_BEGIN_MACRO \
+ nsresult __rv = res; /* Do not evaluate |res| more than once! */ \
+ if (NS_FAILED(__rv)) { \
+ char *msg = PR_smprintf("NS_ASSERT_SUCCESS(%s) failed with result 0x%X", \
+ #res, __rv); \
+ NS_ASSERTION(NS_SUCCEEDED(__rv), msg); \
+ PR_smprintf_free(msg); \
+ } \
+ PR_END_MACRO
+#else
+#define NS_ASSERT_SUCCESS(res) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO
+#endif
+
+/******************************************************************************
+ * DBListenerErrorHandler impl:
+ * Parent class for our async storage listeners that handles the logging of
+ * errors.
+ ******************************************************************************/
+class DBListenerErrorHandler : public mozIStorageStatementCallback
+{
+protected:
+ explicit DBListenerErrorHandler(DBState* dbState) : mDBState(dbState) { }
+ RefPtr<DBState> mDBState;
+ virtual const char *GetOpType() = 0;
+
+public:
+ NS_IMETHOD HandleError(mozIStorageError* aError) override
+ {
+ if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) {
+ int32_t result = -1;
+ aError->GetResult(&result);
+
+ nsAutoCString message;
+ aError->GetMessage(message);
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("DBListenerErrorHandler::HandleError(): Error %d occurred while "
+ "performing operation '%s' with message '%s'; rebuilding database.",
+ result, GetOpType(), message.get()));
+ }
+
+ // Rebuild the database.
+ gCookieService->HandleCorruptDB(mDBState);
+
+ return NS_OK;
+ }
+};
+
+/******************************************************************************
+ * InsertCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous insertion operations.
+ ******************************************************************************/
+class InsertCookieDBListener final : public DBListenerErrorHandler
+{
+private:
+ const char *GetOpType() override { return "INSERT"; }
+
+ ~InsertCookieDBListener() = default;
+
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit InsertCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { }
+ NS_IMETHOD HandleResult(mozIStorageResultSet*) override
+ {
+ NS_NOTREACHED("Unexpected call to InsertCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ // If we were rebuilding the db and we succeeded, make our corruptFlag say
+ // so.
+ if (mDBState->corruptFlag == DBState::REBUILDING &&
+ aReason == mozIStorageStatementCallback::REASON_FINISHED) {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("InsertCookieDBListener::HandleCompletion(): rebuild complete"));
+ mDBState->corruptFlag = DBState::OK;
+ }
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * UpdateCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous update operations.
+ ******************************************************************************/
+class UpdateCookieDBListener final : public DBListenerErrorHandler
+{
+private:
+ const char *GetOpType() override { return "UPDATE"; }
+
+ ~UpdateCookieDBListener() = default;
+
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit UpdateCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { }
+ NS_IMETHOD HandleResult(mozIStorageResultSet*) override
+ {
+ NS_NOTREACHED("Unexpected call to UpdateCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * RemoveCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous removal operations.
+ ******************************************************************************/
+class RemoveCookieDBListener final : public DBListenerErrorHandler
+{
+private:
+ const char *GetOpType() override { return "REMOVE"; }
+
+ ~RemoveCookieDBListener() = default;
+
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit RemoveCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { }
+ NS_IMETHOD HandleResult(mozIStorageResultSet*) override
+ {
+ NS_NOTREACHED("Unexpected call to RemoveCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * ReadCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous removal operations.
+ ******************************************************************************/
+class ReadCookieDBListener final : public DBListenerErrorHandler
+{
+private:
+ const char *GetOpType() override { return "READ"; }
+ bool mCanceled;
+
+ ~ReadCookieDBListener() = default;
+
+public:
+ NS_DECL_ISUPPORTS
+
+ explicit ReadCookieDBListener(DBState* dbState)
+ : DBListenerErrorHandler(dbState)
+ , mCanceled(false)
+ {
+ }
+
+ void Cancel() { mCanceled = true; }
+
+ NS_IMETHOD HandleResult(mozIStorageResultSet *aResult) override
+ {
+ nsCOMPtr<mozIStorageRow> row;
+
+ while (true) {
+ DebugOnly<nsresult> rv = aResult->GetNextRow(getter_AddRefs(row));
+ NS_ASSERT_SUCCESS(rv);
+
+ if (!row)
+ break;
+
+ CookieDomainTuple *tuple = mDBState->hostArray.AppendElement();
+ row->GetUTF8String(IDX_BASE_DOMAIN, tuple->key.mBaseDomain);
+
+ nsAutoCString suffix;
+ row->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix);
+ DebugOnly<bool> success = tuple->key.mOriginAttributes.PopulateFromSuffix(suffix);
+ MOZ_ASSERT(success);
+
+ tuple->cookie =
+ gCookieService->GetCookieFromRow(row, tuple->key.mOriginAttributes);
+ }
+
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override
+ {
+ // Process the completion of the read operation. If we have been canceled,
+ // we cannot assume that the cookieservice still has an open connection
+ // or that it even refers to the same database, so we must return early.
+ // Conversely, the cookieservice guarantees that if we have not been
+ // canceled, the database connection is still alive and we can safely
+ // operate on it.
+
+ if (mCanceled) {
+ // We may receive a REASON_FINISHED after being canceled;
+ // tweak the reason accordingly.
+ aReason = mozIStorageStatementCallback::REASON_CANCELED;
+ }
+
+ switch (aReason) {
+ case mozIStorageStatementCallback::REASON_FINISHED:
+ gCookieService->AsyncReadComplete();
+ break;
+ case mozIStorageStatementCallback::REASON_CANCELED:
+ // Nothing more to do here. The partially read data has already been
+ // thrown away.
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Read canceled"));
+ break;
+ case mozIStorageStatementCallback::REASON_ERROR:
+ // Nothing more to do here. DBListenerErrorHandler::HandleError()
+ // can handle it.
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Read error"));
+ break;
+ default:
+ NS_NOTREACHED("invalid reason");
+ }
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(ReadCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * CloseCookieDBListener imp:
+ * Static mozIStorageCompletionCallback used to notify when the database is
+ * successfully closed.
+ ******************************************************************************/
+class CloseCookieDBListener final : public mozIStorageCompletionCallback
+{
+ ~CloseCookieDBListener() = default;
+
+public:
+ explicit CloseCookieDBListener(DBState* dbState) : mDBState(dbState) { }
+ RefPtr<DBState> mDBState;
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Complete(nsresult, nsISupports*) override
+ {
+ gCookieService->HandleDBClosed(mDBState);
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback)
+
+namespace {
+
+class AppClearDataObserver final : public nsIObserver {
+
+ ~AppClearDataObserver() = default;
+
+public:
+ NS_DECL_ISUPPORTS
+
+ // nsIObserver implementation.
+ NS_IMETHOD
+ Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) override
+ {
+ MOZ_ASSERT(!nsCRT::strcmp(aTopic, TOPIC_CLEAR_ORIGIN_DATA));
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsICookieManager2> cookieManager
+ = do_GetService(NS_COOKIEMANAGER_CONTRACTID);
+ MOZ_ASSERT(cookieManager);
+
+ return cookieManager->RemoveCookiesWithOriginAttributes(nsDependentString(aData), EmptyCString());
+ }
+};
+
+NS_IMPL_ISUPPORTS(AppClearDataObserver, nsIObserver)
+
+} // namespace
+
+size_t
+nsCookieKey::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ return mBaseDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+}
+
+size_t
+nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ size_t amount = nsCookieKey::SizeOfExcludingThis(aMallocSizeOf);
+
+ amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mCookies.Length(); ++i) {
+ amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+size_t
+CookieDomainTuple::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ size_t amount = 0;
+
+ amount += key.SizeOfExcludingThis(aMallocSizeOf);
+ amount += cookie->SizeOfIncludingThis(aMallocSizeOf);
+
+ return amount;
+}
+
+size_t
+DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ size_t amount = 0;
+
+ amount += aMallocSizeOf(this);
+ amount += hostTable.SizeOfExcludingThis(aMallocSizeOf);
+ amount += hostArray.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < hostArray.Length(); ++i) {
+ amount += hostArray[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ amount += readSet.SizeOfExcludingThis(aMallocSizeOf);
+
+ return amount;
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * singleton instance ctor/dtor methods
+ ******************************************************************************/
+
+nsICookieService*
+nsCookieService::GetXPCOMSingleton()
+{
+ if (IsNeckoChild())
+ return CookieServiceChild::GetSingleton();
+
+ return GetSingleton();
+}
+
+nsCookieService*
+nsCookieService::GetSingleton()
+{
+ NS_ASSERTION(!IsNeckoChild(), "not a parent process");
+
+ if (gCookieService) {
+ NS_ADDREF(gCookieService);
+ return gCookieService;
+ }
+
+ // Create a new singleton nsCookieService.
+ // We AddRef only once since XPCOM has rules about the ordering of module
+ // teardowns - by the time our module destructor is called, it's too late to
+ // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC
+ // cycles have already been completed and would result in serious leaks.
+ // See bug 209571.
+ gCookieService = new nsCookieService();
+ if (gCookieService) {
+ NS_ADDREF(gCookieService);
+ if (NS_FAILED(gCookieService->Init())) {
+ NS_RELEASE(gCookieService);
+ }
+ }
+
+ return gCookieService;
+}
+
+/* static */ void
+nsCookieService::AppClearDataObserverInit()
+{
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ nsCOMPtr<nsIObserver> obs = new AppClearDataObserver();
+ observerService->AddObserver(obs, TOPIC_CLEAR_ORIGIN_DATA,
+ /* ownsWeak= */ false);
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * public methods
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(nsCookieService,
+ nsICookieService,
+ nsICookieManager,
+ nsICookieManager2,
+ nsIObserver,
+ nsISupportsWeakReference,
+ nsIMemoryReporter)
+
+nsCookieService::nsCookieService()
+ : mDBState(nullptr)
+ , mCookieBehavior(nsICookieService::BEHAVIOR_ACCEPT)
+ , mThirdPartySession(false)
+ , mLeaveSecureAlone(true)
+ , mMaxNumberOfCookies(kMaxNumberOfCookies)
+ , mMaxCookiesPerHost(kMaxCookiesPerHost)
+ , mCookiePurgeAge(kCookiePurgeAge)
+{
+}
+
+nsresult
+nsCookieService::Init()
+{
+ nsresult rv;
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // init our pref and observer
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefBranch) {
+ prefBranch->AddObserver(kPrefCookieBehavior, this, true);
+ prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
+ prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
+ prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
+ prefBranch->AddObserver(kPrefThirdPartySession, this, true);
+ prefBranch->AddObserver(kCookieLeaveSecurityAlone, this, true);
+ PrefChanged(prefBranch);
+ }
+
+ mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Init our default, and possibly private DBStates.
+ InitDBStates();
+
+ RegisterWeakMemoryReporter(this);
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(os);
+ os->AddObserver(this, "profile-before-change", true);
+ os->AddObserver(this, "profile-do-change", true);
+ os->AddObserver(this, "last-pb-context-exited", true);
+
+ mPermissionService = do_GetService(NS_COOKIEPERMISSION_CONTRACTID);
+ if (!mPermissionService) {
+ NS_WARNING("nsICookiePermission implementation not available - some features won't work!");
+ COOKIE_LOGSTRING(LogLevel::Warning, ("Init(): nsICookiePermission implementation not available"));
+ }
+
+ return NS_OK;
+}
+
+void
+nsCookieService::InitDBStates()
+{
+ NS_ASSERTION(!mDBState, "already have a DBState");
+ NS_ASSERTION(!mDefaultDBState, "already have a default DBState");
+ NS_ASSERTION(!mPrivateDBState, "already have a private DBState");
+
+ // Create a new default DBState and set our current one.
+ mDefaultDBState = new DBState();
+ mDBState = mDefaultDBState;
+
+ mPrivateDBState = new DBState();
+
+ // Get our cookie file.
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mDefaultDBState->cookieFile));
+ if (NS_FAILED(rv)) {
+ // We've already set up our DBStates appropriately; nothing more to do.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("InitDBStates(): couldn't get cookie file"));
+ return;
+ }
+ mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE));
+
+ // Attempt to open and read the database. If TryInitDB() returns RESULT_RETRY,
+ // do so.
+ OpenDBResult result = TryInitDB(false);
+ if (result == RESULT_RETRY) {
+ // Database may be corrupt. Synchronously close the connection, clean up the
+ // default DBState, and try again.
+ COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBStates(): retrying TryInitDB()"));
+ CleanupCachedStatements();
+ CleanupDefaultDBConnection();
+ result = TryInitDB(true);
+ if (result == RESULT_RETRY) {
+ // We're done. Change the code to failure so we clean up below.
+ result = RESULT_FAILURE;
+ }
+ }
+
+ if (result == RESULT_FAILURE) {
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("InitDBStates(): TryInitDB() failed, closing connection"));
+
+ // Connection failure is unrecoverable. Clean up our connection. We can run
+ // fine without persistent storage -- e.g. if there's no profile.
+ CleanupCachedStatements();
+ CleanupDefaultDBConnection();
+ }
+}
+
+namespace {
+
+class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction
+{
+ ~ConvertAppIdToOriginAttrsSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction);
+
+NS_IMETHODIMP
+ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult)
+{
+ nsresult rv;
+ int32_t appId, inIsolatedMozBrowser;
+
+ rv = aFunctionArguments->GetInt32(0, &appId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create an originAttributes object by appId and inIsolatedMozBrowser.
+ // Then create the originSuffix string from this object.
+ NeckoOriginAttributes attrs(appId, (inIsolatedMozBrowser ? 1 : 0));
+ nsAutoCString suffix;
+ attrs.CreateSuffix(suffix);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsAUTF8String(suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+class SetAppIdFromOriginAttributesSQLFunction final : public mozIStorageFunction
+{
+ ~SetAppIdFromOriginAttributesSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction);
+
+NS_IMETHODIMP
+SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult)
+{
+ nsresult rv;
+ nsAutoCString suffix;
+ NeckoOriginAttributes attrs;
+
+ rv = aFunctionArguments->GetUTF8String(0, suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool success = attrs.PopulateFromSuffix(suffix);
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsInt32(attrs.mAppId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+class SetInBrowserFromOriginAttributesSQLFunction final :
+ public mozIStorageFunction
+{
+ ~SetInBrowserFromOriginAttributesSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction,
+ mozIStorageFunction);
+
+NS_IMETHODIMP
+SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult)
+{
+ nsresult rv;
+ nsAutoCString suffix;
+ NeckoOriginAttributes attrs;
+
+ rv = aFunctionArguments->GetUTF8String(0, suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool success = attrs.PopulateFromSuffix(suffix);
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsInt32(attrs.mInIsolatedMozBrowser);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+} // namespace
+
+/* Attempt to open and read the database. If 'aRecreateDB' is true, try to
+ * move the existing database file out of the way and create a new one.
+ *
+ * @returns RESULT_OK if opening or creating the database succeeded;
+ * RESULT_RETRY if the database cannot be opened, is corrupt, or some
+ * other failure occurred that might be resolved by recreating the
+ * database; or RESULT_FAILED if there was an unrecoverable error and
+ * we must run without a database.
+ *
+ * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform
+ * cleanup of the default DBState.
+ */
+OpenDBResult
+nsCookieService::TryInitDB(bool aRecreateDB)
+{
+ NS_ASSERTION(!mDefaultDBState->dbConn, "nonnull dbConn");
+ NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert");
+ NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener");
+ NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn");
+
+ // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't
+ // want to delete it outright, since it may be useful for debugging purposes,
+ // so we move it out of the way.
+ nsresult rv;
+ if (aRecreateDB) {
+ nsCOMPtr<nsIFile> backupFile;
+ mDefaultDBState->cookieFile->Clone(getter_AddRefs(backupFile));
+ rv = backupFile->MoveToNative(nullptr,
+ NS_LITERAL_CSTRING(COOKIES_FILE ".bak"));
+ NS_ENSURE_SUCCESS(rv, RESULT_FAILURE);
+ }
+
+ // This block provides scope for the Telemetry AutoTimer
+ {
+ Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS>
+ telemetry;
+ ReadAheadFile(mDefaultDBState->cookieFile);
+
+ // open a connection to the cookie database, and only cache our connection
+ // and statements upon success. The connection is opened unshared to eliminate
+ // cache contention between the main and background threads.
+ rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile,
+ getter_AddRefs(mDefaultDBState->dbConn));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ // Set up our listeners.
+ mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState);
+ mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState);
+ mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState);
+ mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState);
+
+ // Grow cookie db in 512KB increments
+ mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString());
+
+ bool tableExists = false;
+ mDefaultDBState->dbConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"),
+ &tableExists);
+ if (!tableExists) {
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ } else {
+ // table already exists; check the schema version before reading
+ int32_t dbSchemaVersion;
+ rv = mDefaultDBState->dbConn->GetSchemaVersion(&dbSchemaVersion);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Start a transaction for the whole migration block.
+ mozStorageTransaction transaction(mDefaultDBState->dbConn, true);
+
+ switch (dbSchemaVersion) {
+ // Upgrading.
+ // Every time you increment the database schema, you need to implement
+ // the upgrading code from the previous version to the new one. If migration
+ // fails for any reason, it's a bug -- so we return RESULT_RETRY such that
+ // the original database will be saved, in the hopes that we might one day
+ // see it and fix it.
+ case 1:
+ {
+ // Add the lastAccessed column to the table.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies ADD lastAccessed INTEGER"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ MOZ_FALLTHROUGH;
+
+ case 2:
+ {
+ // Add the baseDomain column and index to the table.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies ADD baseDomain TEXT"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Compute the baseDomains for the table. This must be done eagerly
+ // otherwise we won't be able to synchronously read in individual
+ // domains on demand.
+ const int64_t SCHEMA2_IDX_ID = 0;
+ const int64_t SCHEMA2_IDX_HOST = 1;
+ nsCOMPtr<mozIStorageStatement> select;
+ rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, host FROM moz_cookies"), getter_AddRefs(select));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageStatement> update;
+ rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id"),
+ getter_AddRefs(update));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCString baseDomain, host;
+ bool hasResult;
+ while (true) {
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (!hasResult)
+ break;
+
+ int64_t id = select->AsInt64(SCHEMA2_IDX_ID);
+ select->GetUTF8String(SCHEMA2_IDX_HOST, host);
+
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ mozStorageStatementScoper scoper(update);
+
+ rv = update->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
+ baseDomain);
+ NS_ASSERT_SUCCESS(rv);
+ rv = update->BindInt64ByName(NS_LITERAL_CSTRING("id"),
+ id);
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = update->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ // Create an index on baseDomain.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ MOZ_FALLTHROUGH;
+
+ case 3:
+ {
+ // Add the creationTime column to the table, and create a unique index
+ // on (name, host, path). Before we do this, we have to purge the table
+ // of expired cookies such that we know that the (name, host, path)
+ // index is truly unique -- otherwise we can't create the index. Note
+ // that we can't just execute a statement to delete all rows where the
+ // expiry column is in the past -- doing so would rely on the clock
+ // (both now and when previous cookies were set) being monotonic.
+
+ // Select the whole table, and order by the fields we're interested in.
+ // This means we can simply do a linear traversal of the results and
+ // check for duplicates as we go.
+ const int64_t SCHEMA3_IDX_ID = 0;
+ const int64_t SCHEMA3_IDX_NAME = 1;
+ const int64_t SCHEMA3_IDX_HOST = 2;
+ const int64_t SCHEMA3_IDX_PATH = 3;
+ nsCOMPtr<mozIStorageStatement> select;
+ rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT id, name, host, path FROM moz_cookies "
+ "ORDER BY name ASC, host ASC, path ASC, expiry ASC"),
+ getter_AddRefs(select));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageStatement> deleteExpired;
+ rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_cookies WHERE id = :id"),
+ getter_AddRefs(deleteExpired));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Read the first row.
+ bool hasResult;
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (hasResult) {
+ nsCString name1, host1, path1;
+ int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID);
+ select->GetUTF8String(SCHEMA3_IDX_NAME, name1);
+ select->GetUTF8String(SCHEMA3_IDX_HOST, host1);
+ select->GetUTF8String(SCHEMA3_IDX_PATH, path1);
+
+ nsCString name2, host2, path2;
+ while (true) {
+ // Read the second row.
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (!hasResult)
+ break;
+
+ int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID);
+ select->GetUTF8String(SCHEMA3_IDX_NAME, name2);
+ select->GetUTF8String(SCHEMA3_IDX_HOST, host2);
+ select->GetUTF8String(SCHEMA3_IDX_PATH, path2);
+
+ // If the two rows match in (name, host, path), we know the earlier
+ // row has an earlier expiry time. Delete it.
+ if (name1 == name2 && host1 == host2 && path1 == path2) {
+ mozStorageStatementScoper scoper(deleteExpired);
+
+ rv = deleteExpired->BindInt64ByName(NS_LITERAL_CSTRING("id"),
+ id1);
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = deleteExpired->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ // Make the second row the first for the next iteration.
+ name1 = name2;
+ host1 = host2;
+ path1 = path2;
+ id1 = id2;
+ }
+ }
+
+ // Add the creationTime column to the table.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies ADD creationTime INTEGER"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy the id of each row into the new creationTime column.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_cookies SET creationTime = "
+ "(SELECT id WHERE id = moz_cookies.id)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create a unique index on (name, host, path) to allow fast lookup.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE UNIQUE INDEX moz_uniqueid "
+ "ON moz_cookies (name, host, path)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ MOZ_FALLTHROUGH;
+
+ case 4:
+ {
+ // We need to add appId/inBrowserElement, plus change a constraint on
+ // the table (unique entries now include appId/inBrowserElement):
+ // this requires creating a new table and copying the data to it. We
+ // then rename the new table to the old name.
+ //
+ // Why we made this change: appId/inBrowserElement allow "cookie jars"
+ // for Firefox OS. We create a separate cookie namespace per {appId,
+ // inBrowserElement}. When upgrading, we convert existing cookies
+ // (which imply we're on desktop/mobile) to use {0, false}, as that is
+ // the only namespace used by a non-Firefox-OS implementation.
+
+ // Rename existing table
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop existing index (CreateTable will create new one for new table)
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX moz_basedomain"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create new table (with new fields and new unique constraint)
+ rv = CreateTableForSchemaVersion5();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy data from old table, using appId/inBrowser=0 for existing rows
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_cookies "
+ "(baseDomain, appId, inBrowserElement, name, value, host, path, expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly) "
+ "SELECT baseDomain, 0, 0, name, value, host, path, expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly "
+ "FROM moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop old table
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 5"));
+ }
+ // Fall through to the next upgrade.
+ MOZ_FALLTHROUGH;
+
+ case 5:
+ {
+ // Change in the version: Replace the columns |appId| and
+ // |inBrowserElement| by a single column |originAttributes|.
+ //
+ // Why we made this change: FxOS new security model (NSec) encapsulates
+ // "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to make
+ // it easier to modify the contents of this structure in the future.
+ //
+ // We do the migration in several steps:
+ // 1. Rename the old table.
+ // 2. Create a new table.
+ // 3. Copy data from the old table to the new table; convert appId and
+ // inBrowserElement to originAttributes in the meantime.
+
+ // Rename existing table.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop existing index (CreateTable will create new one for new table).
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP INDEX moz_basedomain"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create new table with new fields and new unique constraint.
+ rv = CreateTableForSchemaVersion6();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy data from old table without the two deprecated columns appId and
+ // inBrowserElement.
+ nsCOMPtr<mozIStorageFunction>
+ convertToOriginAttrs(new ConvertAppIdToOriginAttrsSQLFunction());
+ NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY);
+
+ NS_NAMED_LITERAL_CSTRING(convertToOriginAttrsName,
+ "CONVERT_TO_ORIGIN_ATTRIBUTES");
+
+ rv = mDefaultDBState->dbConn->CreateFunction(convertToOriginAttrsName,
+ 2, convertToOriginAttrs);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_cookies "
+ "(baseDomain, originAttributes, name, value, host, path, expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly) "
+ "SELECT baseDomain, "
+ " CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement),"
+ " name, value, host, path, expiry, lastAccessed, creationTime, "
+ " isSecure, isHttpOnly "
+ "FROM moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->RemoveFunction(convertToOriginAttrsName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop old table
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 6"));
+ }
+ MOZ_FALLTHROUGH;
+
+ case 6:
+ {
+ // We made a mistake in schema version 6. We cannot remove expected
+ // columns of any version (checked in the default case) from cookie
+ // database, because doing this would destroy the possibility of
+ // downgrading database.
+ //
+ // This version simply restores appId and inBrowserElement columns in
+ // order to fix downgrading issue even though these two columns are no
+ // longer used in the latest schema.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Compute and populate the values of appId and inBrwoserElement from
+ // originAttributes.
+ nsCOMPtr<mozIStorageFunction>
+ setAppId(new SetAppIdFromOriginAttributesSQLFunction());
+ NS_ENSURE_TRUE(setAppId, RESULT_RETRY);
+
+ NS_NAMED_LITERAL_CSTRING(setAppIdName, "SET_APP_ID");
+
+ rv = mDefaultDBState->dbConn->CreateFunction(setAppIdName, 1, setAppId);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageFunction>
+ setInBrowser(new SetInBrowserFromOriginAttributesSQLFunction());
+ NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY);
+
+ NS_NAMED_LITERAL_CSTRING(setInBrowserName, "SET_IN_BROWSER");
+
+ rv = mDefaultDBState->dbConn->CreateFunction(setInBrowserName, 1,
+ setInBrowser);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), "
+ "inBrowserElement = SET_IN_BROWSER(originAttributes);"
+ ));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->RemoveFunction(setAppIdName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->RemoveFunction(setInBrowserName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 7"));
+ }
+
+ // No more upgrades. Update the schema version.
+ rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ MOZ_FALLTHROUGH;
+
+ case COOKIES_SCHEMA_VERSION:
+ break;
+
+ case 0:
+ {
+ NS_WARNING("couldn't get schema version!");
+
+ // the table may be usable; someone might've just clobbered the schema
+ // version. we can treat this case like a downgrade using the codepath
+ // below, by verifying the columns we care about are all there. for now,
+ // re-set the schema version in the db, in case the checks succeed (if
+ // they don't, we're dropping the table anyway).
+ rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // fall through to downgrade check
+ MOZ_FALLTHROUGH;
+
+ // downgrading.
+ // if columns have been added to the table, we can still use the ones we
+ // understand safely. if columns have been deleted or altered, just
+ // blow away the table and start from scratch! if you change the way
+ // a column is interpreted, make sure you also change its name so this
+ // check will catch it.
+ default:
+ {
+ // check if all the expected columns exist
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT "
+ "id, "
+ "baseDomain, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly "
+ "FROM moz_cookies"), getter_AddRefs(stmt));
+ if (NS_SUCCEEDED(rv))
+ break;
+
+ // our columns aren't there - drop the table!
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DROP TABLE moz_cookies"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ break;
+ }
+ }
+
+ // make operations on the table asynchronous, for performance
+ mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "PRAGMA synchronous = OFF"));
+
+ // Use write-ahead-logging for performance. We cap the autocheckpoint limit at
+ // 16 pages (around 500KB).
+ mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL"));
+ mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "PRAGMA wal_autocheckpoint = 16"));
+
+ // cache frequently used statements (for insertion, deletion, and updating)
+ rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO moz_cookies ("
+ "baseDomain, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly"
+ ") VALUES ("
+ ":baseDomain, "
+ ":originAttributes, "
+ ":name, "
+ ":value, "
+ ":host, "
+ ":path, "
+ ":expiry, "
+ ":lastAccessed, "
+ ":creationTime, "
+ ":isSecure, "
+ ":isHttpOnly"
+ ")"),
+ getter_AddRefs(mDefaultDBState->stmtInsert));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_cookies "
+ "WHERE name = :name AND host = :host AND path = :path"),
+ getter_AddRefs(mDefaultDBState->stmtDelete));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "UPDATE moz_cookies SET lastAccessed = :lastAccessed "
+ "WHERE name = :name AND host = :host AND path = :path"),
+ getter_AddRefs(mDefaultDBState->stmtUpdate));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // if we deleted a corrupt db, don't attempt to import - return now
+ if (aRecreateDB)
+ return RESULT_OK;
+
+ // check whether to import or just read in the db
+ if (tableExists)
+ return Read();
+
+ nsCOMPtr<nsIFile> oldCookieFile;
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(oldCookieFile));
+ if (NS_FAILED(rv)) return RESULT_OK;
+
+ // Import cookies, and clean up the old file regardless of success or failure.
+ // Note that we have to switch out our DBState temporarily, in case we're in
+ // private browsing mode; otherwise ImportCookies() won't be happy.
+ DBState* initialState = mDBState;
+ mDBState = mDefaultDBState;
+ oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME));
+ ImportCookies(oldCookieFile);
+ oldCookieFile->Remove(false);
+ mDBState = initialState;
+
+ return RESULT_OK;
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult
+nsCookieService::CreateTable()
+{
+ // Set the schema version, before creating the table.
+ nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion(
+ COOKIES_SCHEMA_VERSION);
+ if (NS_FAILED(rv)) return rv;
+
+ // Create the table.
+ // We default originAttributes to empty string: this is so if users revert to
+ // an older Firefox version that doesn't know about this field, any cookies
+ // set will still work once they upgrade back.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE moz_cookies ("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "appId INTEGER DEFAULT 0, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
+ ")"));
+ if (NS_FAILED(rv)) return rv;
+
+ // Create an index on baseDomain.
+ return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
+ "originAttributes)"));
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult
+nsCookieService::CreateTableForSchemaVersion6()
+{
+ // Set the schema version, before creating the table.
+ nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion(6);
+ if (NS_FAILED(rv)) return rv;
+
+ // Create the table.
+ // We default originAttributes to empty string: this is so if users revert to
+ // an older Firefox version that doesn't know about this field, any cookies
+ // set will still work once they upgrade back.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE moz_cookies ("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
+ ")"));
+ if (NS_FAILED(rv)) return rv;
+
+ // Create an index on baseDomain.
+ return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
+ "originAttributes)"));
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult
+nsCookieService::CreateTableForSchemaVersion5()
+{
+ // Set the schema version, before creating the table.
+ nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion(5);
+ if (NS_FAILED(rv)) return rv;
+
+ // Create the table. We default appId/inBrowserElement to 0: this is so if
+ // users revert to an older Firefox version that doesn't know about these
+ // fields, any cookies set will still work once they upgrade back.
+ rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE moz_cookies ("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "appId INTEGER DEFAULT 0, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, appId, inBrowserElement)"
+ ")"));
+ if (NS_FAILED(rv)) return rv;
+
+ // Create an index on baseDomain.
+ return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
+ "appId, "
+ "inBrowserElement)"));
+}
+
+void
+nsCookieService::CloseDBStates()
+{
+ // Null out our private and pointer DBStates regardless.
+ mPrivateDBState = nullptr;
+ mDBState = nullptr;
+
+ // If we don't have a default DBState, we're done.
+ if (!mDefaultDBState)
+ return;
+
+ // Cleanup cached statements before we can close anything.
+ CleanupCachedStatements();
+
+ if (mDefaultDBState->dbConn) {
+ // Cancel any pending read. No further results will be received by our
+ // read listener.
+ if (mDefaultDBState->pendingRead) {
+ CancelAsyncRead(true);
+ }
+
+ // Asynchronously close the connection. We will null it below.
+ mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
+ }
+
+ CleanupDefaultDBConnection();
+
+ mDefaultDBState = nullptr;
+}
+
+// Null out the statements.
+// This must be done before closing the connection.
+void
+nsCookieService::CleanupCachedStatements()
+{
+ mDefaultDBState->stmtInsert = nullptr;
+ mDefaultDBState->stmtDelete = nullptr;
+ mDefaultDBState->stmtUpdate = nullptr;
+}
+
+// Null out the listeners, and the database connection itself. This
+// will not null out the statements, cancel a pending read or
+// asynchronously close the connection -- these must be done
+// beforehand if necessary.
+void
+nsCookieService::CleanupDefaultDBConnection()
+{
+ MOZ_ASSERT(!mDefaultDBState->stmtInsert, "stmtInsert has been cleaned up");
+ MOZ_ASSERT(!mDefaultDBState->stmtDelete, "stmtDelete has been cleaned up");
+ MOZ_ASSERT(!mDefaultDBState->stmtUpdate, "stmtUpdate has been cleaned up");
+
+ // Null out the database connections. If 'dbConn' has not been used for any
+ // asynchronous operations yet, this will synchronously close it; otherwise,
+ // it's expected that the caller has performed an AsyncClose prior.
+ mDefaultDBState->dbConn = nullptr;
+ mDefaultDBState->syncConn = nullptr;
+
+ // Manually null out our listeners. This is necessary because they hold a
+ // strong ref to the DBState itself. They'll stay alive until whatever
+ // statements are still executing complete.
+ mDefaultDBState->readListener = nullptr;
+ mDefaultDBState->insertListener = nullptr;
+ mDefaultDBState->updateListener = nullptr;
+ mDefaultDBState->removeListener = nullptr;
+ mDefaultDBState->closeListener = nullptr;
+}
+
+void
+nsCookieService::HandleDBClosed(DBState* aDBState)
+{
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("HandleDBClosed(): DBState %x closed", aDBState));
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+
+ switch (aDBState->corruptFlag) {
+ case DBState::OK: {
+ // Database is healthy. Notify of closure.
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ break;
+ }
+ case DBState::CLOSING_FOR_REBUILD: {
+ // Our close finished. Start the rebuild, and notify of db closure later.
+ RebuildCorruptDB(aDBState);
+ break;
+ }
+ case DBState::REBUILDING: {
+ // We encountered an error during rebuild, closed the database, and now
+ // here we are. We already have a 'cookies.sqlite.bak' from the original
+ // dead database; we don't want to overwrite it, so let's move this one to
+ // 'cookies.sqlite.bak-rebuild'.
+ nsCOMPtr<nsIFile> backupFile;
+ aDBState->cookieFile->Clone(getter_AddRefs(backupFile));
+ nsresult rv = backupFile->MoveToNative(nullptr,
+ NS_LITERAL_CSTRING(COOKIES_FILE ".bak-rebuild"));
+
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("HandleDBClosed(): DBState %x encountered error rebuilding db; move to "
+ "'cookies.sqlite.bak-rebuild' gave rv 0x%x", aDBState, rv));
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ break;
+ }
+ }
+}
+
+void
+nsCookieService::HandleCorruptDB(DBState* aDBState)
+{
+ if (mDefaultDBState != aDBState) {
+ // We've either closed the state or we've switched profiles. It's getting
+ // a bit late to rebuild -- bail instead.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("HandleCorruptDB(): DBState %x is already closed, aborting", aDBState));
+ return;
+ }
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("HandleCorruptDB(): DBState %x has corruptFlag %u", aDBState,
+ aDBState->corruptFlag));
+
+ // Mark the database corrupt, so the close listener can begin reconstructing
+ // it.
+ switch (mDefaultDBState->corruptFlag) {
+ case DBState::OK: {
+ // Move to 'closing' state.
+ mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD;
+
+ // Cancel any pending read and close the database. If we do have an
+ // in-flight read we want to throw away all the results so far -- we have no
+ // idea how consistent the database is. Note that we may have already
+ // canceled the read but not emptied our readSet; do so now.
+ mDefaultDBState->readSet.Clear();
+ if (mDefaultDBState->pendingRead) {
+ CancelAsyncRead(true);
+ mDefaultDBState->syncConn = nullptr;
+ }
+
+ CleanupCachedStatements();
+ mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
+ CleanupDefaultDBConnection();
+ break;
+ }
+ case DBState::CLOSING_FOR_REBUILD: {
+ // We had an error while waiting for close completion. That's OK, just
+ // ignore it -- we're rebuilding anyway.
+ return;
+ }
+ case DBState::REBUILDING: {
+ // We had an error while rebuilding the DB. Game over. Close the database
+ // and let the close handler do nothing; then we'll move it out of the way.
+ CleanupCachedStatements();
+ if (mDefaultDBState->dbConn) {
+ mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
+ }
+ CleanupDefaultDBConnection();
+ break;
+ }
+ }
+}
+
+void
+nsCookieService::RebuildCorruptDB(DBState* aDBState)
+{
+ NS_ASSERTION(!aDBState->dbConn, "shouldn't have an open db connection");
+ NS_ASSERTION(aDBState->corruptFlag == DBState::CLOSING_FOR_REBUILD,
+ "should be in CLOSING_FOR_REBUILD state");
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+
+ aDBState->corruptFlag = DBState::REBUILDING;
+
+ if (mDefaultDBState != aDBState) {
+ // We've either closed the state or we've switched profiles. It's getting
+ // a bit late to rebuild -- bail instead. In any case, we were waiting
+ // on rebuild completion to notify of the db closure, which won't happen --
+ // do so now.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("RebuildCorruptDB(): DBState %x is stale, aborting", aDBState));
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ return;
+ }
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("RebuildCorruptDB(): creating new database"));
+
+ // The database has been closed, and we're ready to rebuild. Open a
+ // connection.
+ OpenDBResult result = TryInitDB(true);
+ if (result != RESULT_OK) {
+ // We're done. Reset our DB connection and statements, and notify of
+ // closure.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("RebuildCorruptDB(): TryInitDB() failed with result %u", result));
+ CleanupCachedStatements();
+ CleanupDefaultDBConnection();
+ mDefaultDBState->corruptFlag = DBState::OK;
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ return;
+ }
+
+ // Notify observers that we're beginning the rebuild.
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr);
+ }
+
+ // Enumerate the hash, and add cookies to the params array.
+ mozIStorageAsyncStatement* stmt = aDBState->stmtInsert;
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (auto iter = aDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
+ nsCookieEntry* entry = iter.Get();
+
+ const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ nsCookie* cookie = cookies[i];
+
+ if (!cookie->IsSession()) {
+ bindCookieParameters(paramsArray, nsCookieKey(entry), cookie);
+ }
+ }
+ }
+
+ // Make sure we've got something to write. If we don't, we're done.
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length == 0) {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("RebuildCorruptDB(): nothing to write, rebuild complete"));
+ mDefaultDBState->corruptFlag = DBState::OK;
+ return;
+ }
+
+ // Execute the statement. If any errors crop up, we won't try again.
+ DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(aDBState->insertListener, getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+}
+
+nsCookieService::~nsCookieService()
+{
+ CloseDBStates();
+
+ UnregisterWeakMemoryReporter(this);
+
+ gCookieService = nullptr;
+}
+
+NS_IMETHODIMP
+nsCookieService::Observe(nsISupports *aSubject,
+ const char *aTopic,
+ const char16_t *aData)
+{
+ // check the topic
+ if (!strcmp(aTopic, "profile-before-change")) {
+ // The profile is about to change,
+ // or is going away because the application is shutting down.
+
+ // Close the default DB connection and null out our DBStates before
+ // changing.
+ CloseDBStates();
+
+ } else if (!strcmp(aTopic, "profile-do-change")) {
+ NS_ASSERTION(!mDefaultDBState, "shouldn't have a default DBState");
+ NS_ASSERTION(!mPrivateDBState, "shouldn't have a private DBState");
+
+ // the profile has already changed; init the db from the new location.
+ // if we are in the private browsing state, however, we do not want to read
+ // data into it - we should instead put it into the default state, so it's
+ // ready for us if and when we switch back to it.
+ InitDBStates();
+
+ } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
+ if (prefBranch)
+ PrefChanged(prefBranch);
+
+ } else if (!strcmp(aTopic, "last-pb-context-exited")) {
+ // Flush all the cookies stored by private browsing contexts
+ mPrivateDBState = new DBState();
+ }
+
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::GetCookieString(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ char **aCookie)
+{
+ return GetCookieStringCommon(aHostURI, aChannel, false, aCookie);
+}
+
+NS_IMETHODIMP
+nsCookieService::GetCookieStringFromHttp(nsIURI *aHostURI,
+ nsIURI *aFirstURI,
+ nsIChannel *aChannel,
+ char **aCookie)
+{
+ return GetCookieStringCommon(aHostURI, aChannel, true, aCookie);
+}
+
+nsresult
+nsCookieService::GetCookieStringCommon(nsIURI *aHostURI,
+ nsIChannel *aChannel,
+ bool aHttpBound,
+ char** aCookie)
+{
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG(aCookie);
+
+ // Determine whether the request is foreign. Failure is acceptable.
+ bool isForeign = true;
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
+
+ // Get originAttributes.
+ NeckoOriginAttributes attrs;
+ if (aChannel) {
+ NS_GetOriginAttributes(aChannel, attrs);
+ }
+
+ bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel);
+
+ nsAutoCString result;
+ GetCookieStringInternal(aHostURI, isForeign, aHttpBound, attrs,
+ isPrivate, result);
+ *aCookie = result.IsEmpty() ? nullptr : ToNewCString(result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::SetCookieString(nsIURI *aHostURI,
+ nsIPrompt *aPrompt,
+ const char *aCookieHeader,
+ nsIChannel *aChannel)
+{
+ // The aPrompt argument is deprecated and unused. Avoid introducing new
+ // code that uses this argument by warning if the value is non-null.
+ MOZ_ASSERT(!aPrompt);
+ if (aPrompt) {
+ nsCOMPtr<nsIConsoleService> aConsoleService =
+ do_GetService("@mozilla.org/consoleservice;1");
+ if (aConsoleService) {
+ aConsoleService->LogStringMessage(
+ u"Non-null prompt ignored by nsCookieService.");
+ }
+ }
+ return SetCookieStringCommon(aHostURI, aCookieHeader, nullptr, aChannel,
+ false);
+}
+
+NS_IMETHODIMP
+nsCookieService::SetCookieStringFromHttp(nsIURI *aHostURI,
+ nsIURI *aFirstURI,
+ nsIPrompt *aPrompt,
+ const char *aCookieHeader,
+ const char *aServerTime,
+ nsIChannel *aChannel)
+{
+ // The aPrompt argument is deprecated and unused. Avoid introducing new
+ // code that uses this argument by warning if the value is non-null.
+ MOZ_ASSERT(!aPrompt);
+ if (aPrompt) {
+ nsCOMPtr<nsIConsoleService> aConsoleService =
+ do_GetService("@mozilla.org/consoleservice;1");
+ if (aConsoleService) {
+ aConsoleService->LogStringMessage(
+ u"Non-null prompt ignored by nsCookieService.");
+ }
+ }
+ return SetCookieStringCommon(aHostURI, aCookieHeader, aServerTime, aChannel,
+ true);
+}
+
+nsresult
+nsCookieService::SetCookieStringCommon(nsIURI *aHostURI,
+ const char *aCookieHeader,
+ const char *aServerTime,
+ nsIChannel *aChannel,
+ bool aFromHttp)
+{
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG(aCookieHeader);
+
+ // Determine whether the request is foreign. Failure is acceptable.
+ bool isForeign = true;
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
+
+ // Get originAttributes.
+ NeckoOriginAttributes attrs;
+ if (aChannel) {
+ NS_GetOriginAttributes(aChannel, attrs);
+ }
+
+ bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel);
+
+ nsDependentCString cookieString(aCookieHeader);
+ nsDependentCString serverTime(aServerTime ? aServerTime : "");
+ SetCookieStringInternal(aHostURI, isForeign, cookieString,
+ serverTime, aFromHttp, attrs,
+ isPrivate, aChannel);
+ return NS_OK;
+}
+
+void
+nsCookieService::SetCookieStringInternal(nsIURI *aHostURI,
+ bool aIsForeign,
+ nsDependentCString &aCookieHeader,
+ const nsCString &aServerTime,
+ bool aFromHttp,
+ const NeckoOriginAttributes &aOriginAttrs,
+ bool aIsPrivate,
+ nsIChannel *aChannel)
+{
+ NS_ASSERTION(aHostURI, "null host!");
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return;
+ }
+
+ AutoRestore<DBState*> savePrevDBState(mDBState);
+ mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState;
+
+ // get the base domain for the host URI.
+ // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
+ // file:// URI's (i.e. with an empty host) are allowed, but any other
+ // scheme must have a non-empty host. A trailing dot in the host
+ // is acceptable.
+ bool requireHostMatch;
+ nsAutoCString baseDomain;
+ nsresult rv = GetBaseDomain(aHostURI, baseDomain, requireHostMatch);
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "couldn't get base domain from URI");
+ return;
+ }
+
+ nsCookieKey key(baseDomain, aOriginAttrs);
+
+ // check default prefs
+ CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, aCookieHeader.get());
+
+ // fire a notification if third party or if cookie was rejected
+ // (but not if there was an error)
+ switch (cookieStatus) {
+ case STATUS_REJECTED:
+ NotifyRejected(aHostURI);
+ if (aIsForeign) {
+ NotifyThirdParty(aHostURI, false, aChannel);
+ }
+ return; // Stop here
+ case STATUS_REJECTED_WITH_ERROR:
+ return;
+ case STATUS_ACCEPTED: // Fallthrough
+ case STATUS_ACCEPT_SESSION:
+ if (aIsForeign) {
+ NotifyThirdParty(aHostURI, true, aChannel);
+ }
+ break;
+ default:
+ break;
+ }
+
+ // parse server local time. this is not just done here for efficiency
+ // reasons - if there's an error parsing it, and we need to default it
+ // to the current time, we must do it here since the current time in
+ // SetCookieInternal() will change for each cookie processed (e.g. if the
+ // user is prompted).
+ PRTime tempServerTime;
+ int64_t serverTime;
+ PRStatus result = PR_ParseTimeString(aServerTime.get(), true,
+ &tempServerTime);
+ if (result == PR_SUCCESS) {
+ serverTime = tempServerTime / int64_t(PR_USEC_PER_SEC);
+ } else {
+ serverTime = PR_Now() / PR_USEC_PER_SEC;
+ }
+
+ // process each cookie in the header
+ while (SetCookieInternal(aHostURI, key, requireHostMatch, cookieStatus,
+ aCookieHeader, serverTime, aFromHttp, aChannel)) {
+ // document.cookie can only set one cookie at a time
+ if (!aFromHttp)
+ break;
+ }
+}
+
+// notify observers that a cookie was rejected due to the users' prefs.
+void
+nsCookieService::NotifyRejected(nsIURI *aHostURI)
+{
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(aHostURI, "cookie-rejected", nullptr);
+ }
+}
+
+// notify observers that a third-party cookie was accepted/rejected
+// if the cookie issuer is unknown, it defaults to "?"
+void
+nsCookieService::NotifyThirdParty(nsIURI *aHostURI, bool aIsAccepted, nsIChannel *aChannel)
+{
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (!os) {
+ return;
+ }
+
+ const char* topic;
+
+ if (mDBState != mPrivateDBState) {
+ // Regular (non-private) browsing
+ if (aIsAccepted) {
+ topic = "third-party-cookie-accepted";
+ } else {
+ topic = "third-party-cookie-rejected";
+ }
+ } else {
+ // Private browsing
+ if (aIsAccepted) {
+ topic = "private-third-party-cookie-accepted";
+ } else {
+ topic = "private-third-party-cookie-rejected";
+ }
+ }
+
+ do {
+ // Attempt to find the host of aChannel.
+ if (!aChannel) {
+ break;
+ }
+ nsCOMPtr<nsIURI> channelURI;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI));
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ nsAutoCString referringHost;
+ rv = channelURI->GetHost(referringHost);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ nsAutoString referringHostUTF16 = NS_ConvertUTF8toUTF16(referringHost);
+ os->NotifyObservers(aHostURI, topic, referringHostUTF16.get());
+ return;
+ } while (false);
+
+ // This can fail for a number of reasons, in which kind we fallback to "?"
+ os->NotifyObservers(aHostURI, topic, u"?");
+}
+
+// notify observers that the cookie list changed. there are five possible
+// values for aData:
+// "deleted" means a cookie was deleted. aSubject is the deleted cookie.
+// "added" means a cookie was added. aSubject is the added cookie.
+// "changed" means a cookie was altered. aSubject is the new cookie.
+// "cleared" means the entire cookie list was cleared. aSubject is null.
+// "batch-deleted" means a set of cookies was purged. aSubject is the list of
+// cookies.
+void
+nsCookieService::NotifyChanged(nsISupports *aSubject,
+ const char16_t *aData)
+{
+ const char* topic = mDBState == mPrivateDBState ?
+ "private-cookie-changed" : "cookie-changed";
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(aSubject, topic, aData);
+ }
+}
+
+already_AddRefed<nsIArray>
+nsCookieService::CreatePurgeList(nsICookie2* aCookie)
+{
+ nsCOMPtr<nsIMutableArray> removedList =
+ do_CreateInstance(NS_ARRAY_CONTRACTID);
+ removedList->AppendElement(aCookie, false);
+ return removedList.forget();
+}
+
+/******************************************************************************
+ * nsCookieService:
+ * pref observer impl
+ ******************************************************************************/
+
+void
+nsCookieService::PrefChanged(nsIPrefBranch *aPrefBranch)
+{
+ int32_t val;
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val)))
+ mCookieBehavior = (uint8_t) LIMIT(val, 0, 3, 0);
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val)))
+ mMaxNumberOfCookies = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val)))
+ mMaxCookiesPerHost = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxCookiesPerHost);
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
+ mCookiePurgeAge =
+ int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
+ }
+
+ bool boolval;
+ if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval)))
+ mThirdPartySession = boolval;
+
+ if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kCookieLeaveSecurityAlone, &boolval)))
+ mLeaveSecureAlone = boolval;
+}
+
+/******************************************************************************
+ * nsICookieManager impl:
+ * nsICookieManager
+ ******************************************************************************/
+
+NS_IMETHODIMP
+nsCookieService::RemoveAll()
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RemoveAllFromMemory();
+
+ // clear the cookie file
+ if (mDBState->dbConn) {
+ NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state");
+
+ // Cancel any pending read. No further results will be received by our
+ // read listener.
+ if (mDefaultDBState->pendingRead) {
+ CancelAsyncRead(true);
+ }
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_cookies"), getter_AddRefs(stmt));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mDefaultDBState->removeListener,
+ getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ } else {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("RemoveAll(): corruption detected with rv 0x%x", rv));
+ HandleCorruptDB(mDefaultDBState);
+ }
+ }
+
+ NotifyChanged(nullptr, u"cleared");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ EnsureReadComplete();
+
+ nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
+ for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
+ const nsCookieEntry::ArrayType& cookies = iter.Get()->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ cookieList.AppendObject(cookies[i]);
+ }
+ }
+
+ return NS_NewArrayEnumerator(aEnumerator, cookieList);
+}
+
+static nsresult
+InitializeOriginAttributes(NeckoOriginAttributes* aAttrs,
+ JS::HandleValue aOriginAttributes,
+ JSContext* aCx,
+ uint8_t aArgc,
+ const char16_t* aAPI,
+ const char16_t* aInterfaceSuffix)
+{
+ MOZ_ASSERT(aAttrs);
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aAPI);
+ MOZ_ASSERT(aInterfaceSuffix);
+
+ if (aArgc == 0) {
+ const char16_t* params[] = {
+ aAPI,
+ aInterfaceSuffix
+ };
+
+ // This is supposed to be temporary and in 1 or 2 releases we want to
+ // have originAttributes param as mandatory. But for now, we don't want to
+ // break existing addons, so we write a console message to inform the addon
+ // developers about it.
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ NS_LITERAL_CSTRING("Cookie Manager"),
+ nullptr,
+ nsContentUtils::eNECKO_PROPERTIES,
+ "nsICookieManagerAPIDeprecated",
+ params, ArrayLength(params));
+ } else if (aArgc == 1) {
+ if (!aOriginAttributes.isObject() ||
+ !aAttrs->Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::Add(const nsACString &aHost,
+ const nsACString &aPath,
+ const nsACString &aName,
+ const nsACString &aValue,
+ bool aIsSecure,
+ bool aIsHttpOnly,
+ bool aIsSession,
+ int64_t aExpiry,
+ JS::HandleValue aOriginAttributes,
+ JSContext* aCx,
+ uint8_t aArgc)
+{
+ MOZ_ASSERT(aArgc == 0 || aArgc == 1);
+
+ NeckoOriginAttributes attrs;
+ nsresult rv = InitializeOriginAttributes(&attrs,
+ aOriginAttributes,
+ aCx,
+ aArgc,
+ u"nsICookieManager2.add()",
+ u"2");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly,
+ aIsSession, aExpiry, &attrs);
+}
+
+NS_IMETHODIMP_(nsresult)
+nsCookieService::AddNative(const nsACString &aHost,
+ const nsACString &aPath,
+ const nsACString &aName,
+ const nsACString &aValue,
+ bool aIsSecure,
+ bool aIsHttpOnly,
+ bool aIsSession,
+ int64_t aExpiry,
+ NeckoOriginAttributes* aOriginAttributes)
+{
+ if (NS_WARN_IF(!aOriginAttributes)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the base domain for the host URI.
+ // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t currentTimeInUsec = PR_Now();
+ nsCookieKey key = nsCookieKey(baseDomain, *aOriginAttributes);
+
+ RefPtr<nsCookie> cookie =
+ nsCookie::Create(aName, aValue, host, aPath,
+ aExpiry,
+ currentTimeInUsec,
+ nsCookie::GenerateUniqueCreationTime(currentTimeInUsec),
+ aIsSession,
+ aIsSecure,
+ aIsHttpOnly,
+ key.mOriginAttributes);
+ if (!cookie) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ AddInternal(key, cookie, currentTimeInUsec, nullptr, nullptr, true);
+ return NS_OK;
+}
+
+
+nsresult
+nsCookieService::Remove(const nsACString& aHost, const NeckoOriginAttributes& aAttrs,
+ const nsACString& aName, const nsACString& aPath,
+ bool aBlocked)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsListIter matchIter;
+ RefPtr<nsCookie> cookie;
+ if (FindCookie(nsCookieKey(baseDomain, aAttrs),
+ host,
+ PromiseFlatCString(aName),
+ PromiseFlatCString(aPath),
+ matchIter)) {
+ cookie = matchIter.Cookie();
+ RemoveCookieFromList(matchIter);
+ }
+
+ // check if we need to add the host to the permissions blacklist.
+ if (aBlocked && mPermissionService) {
+ // strip off the domain dot, if necessary
+ if (!host.IsEmpty() && host.First() == '.')
+ host.Cut(0, 1);
+
+ host.Insert(NS_LITERAL_CSTRING("http://"), 0);
+
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), host);
+
+ if (uri)
+ mPermissionService->SetAccess(uri, nsICookiePermission::ACCESS_DENY);
+ }
+
+ if (cookie) {
+ // Everything's done. Notify observers.
+ NotifyChanged(cookie, u"deleted");
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::Remove(const nsACString &aHost,
+ const nsACString &aName,
+ const nsACString &aPath,
+ bool aBlocked,
+ JS::HandleValue aOriginAttributes,
+ JSContext* aCx,
+ uint8_t aArgc)
+{
+ MOZ_ASSERT(aArgc == 0 || aArgc == 1);
+
+ NeckoOriginAttributes attrs;
+ nsresult rv = InitializeOriginAttributes(&attrs,
+ aOriginAttributes,
+ aCx,
+ aArgc,
+ u"nsICookieManager.remove()",
+ u"");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RemoveNative(aHost, aName, aPath, aBlocked, &attrs);
+}
+
+NS_IMETHODIMP_(nsresult)
+nsCookieService::RemoveNative(const nsACString &aHost,
+ const nsACString &aName,
+ const nsACString &aPath,
+ bool aBlocked,
+ NeckoOriginAttributes* aOriginAttributes)
+{
+ if (NS_WARN_IF(!aOriginAttributes)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath, aBlocked);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCookieService::UsePrivateMode(bool aIsPrivate,
+ nsIPrivateModeCallback* aCallback)
+{
+ if (!aCallback) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ AutoRestore<DBState*> savePrevDBState(mDBState);
+ mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState;
+
+ return aCallback->Callback();
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * private file I/O functions
+ ******************************************************************************/
+
+// Begin an asynchronous read from the database.
+OpenDBResult
+nsCookieService::Read()
+{
+ // Set up a statement for the read. Note that our query specifies that
+ // 'baseDomain' not be nullptr -- see below for why.
+ nsCOMPtr<mozIStorageAsyncStatement> stmtRead;
+ nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "baseDomain, "
+ "originAttributes "
+ "FROM moz_cookies "
+ "WHERE baseDomain NOTNULL"), getter_AddRefs(stmtRead));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Set up a statement to delete any rows with a nullptr 'baseDomain'
+ // column. This takes care of any cookies set by browsers that don't
+ // understand the 'baseDomain' column, where the database schema version
+ // is from one that does. (This would occur when downgrading.)
+ nsCOMPtr<mozIStorageAsyncStatement> stmtDeleteNull;
+ rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_cookies WHERE baseDomain ISNULL"),
+ getter_AddRefs(stmtDeleteNull));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Start a new connection for sync reads, to reduce contention with the
+ // background thread. We need to do this before we kick off write statements,
+ // since they can lock the database and prevent connections from being opened.
+ rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile,
+ getter_AddRefs(mDefaultDBState->syncConn));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Init our readSet hash and execute the statements. Note that, after this
+ // point, we cannot fail without altering the cleanup code in InitDBStates()
+ // to handle closing of the now-asynchronous connection.
+ mDefaultDBState->hostArray.SetCapacity(kMaxNumberOfCookies);
+
+ mDefaultDBState->readListener = new ReadCookieDBListener(mDefaultDBState);
+ rv = stmtRead->ExecuteAsync(mDefaultDBState->readListener,
+ getter_AddRefs(mDefaultDBState->pendingRead));
+ NS_ASSERT_SUCCESS(rv);
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmtDeleteNull->ExecuteAsync(mDefaultDBState->removeListener,
+ getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+
+ return RESULT_OK;
+}
+
+// Extract data from a single result row and create an nsCookie.
+// This is templated since 'T' is different for sync vs async results.
+template<class T> nsCookie*
+nsCookieService::GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes)
+{
+ // Skip reading 'baseDomain' -- up to the caller.
+ nsCString name, value, host, path;
+ DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name);
+ NS_ASSERT_SUCCESS(rv);
+ rv = aRow->GetUTF8String(IDX_VALUE, value);
+ NS_ASSERT_SUCCESS(rv);
+ rv = aRow->GetUTF8String(IDX_HOST, host);
+ NS_ASSERT_SUCCESS(rv);
+ rv = aRow->GetUTF8String(IDX_PATH, path);
+ NS_ASSERT_SUCCESS(rv);
+
+ int64_t expiry = aRow->AsInt64(IDX_EXPIRY);
+ int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED);
+ int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME);
+ bool isSecure = 0 != aRow->AsInt32(IDX_SECURE);
+ bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY);
+
+ // Create a new nsCookie and assign the data.
+ return nsCookie::Create(name, value, host, path,
+ expiry,
+ lastAccessed,
+ creationTime,
+ false,
+ isSecure,
+ isHttpOnly,
+ aOriginAttributes);
+}
+
+void
+nsCookieService::AsyncReadComplete()
+{
+ // We may be in the private browsing DB state, with a pending read on the
+ // default DB state. (This would occur if we started up in private browsing
+ // mode.) As long as we do all our operations on the default state, we're OK.
+ NS_ASSERTION(mDefaultDBState, "no default DBState");
+ NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read");
+ NS_ASSERTION(mDefaultDBState->readListener, "no read listener");
+
+ // Merge the data read on the background thread with the data synchronously
+ // read on the main thread. Note that transactions on the cookie table may
+ // have occurred on the main thread since, making the background data stale.
+ for (uint32_t i = 0; i < mDefaultDBState->hostArray.Length(); ++i) {
+ const CookieDomainTuple &tuple = mDefaultDBState->hostArray[i];
+
+ // Tiebreak: if the given base domain has already been read in, ignore
+ // the background data. Note that readSet may contain domains that were
+ // queried but found not to be in the db -- that's harmless.
+ if (mDefaultDBState->readSet.GetEntry(tuple.key))
+ continue;
+
+ AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, false);
+ }
+
+ mDefaultDBState->stmtReadDomain = nullptr;
+ mDefaultDBState->pendingRead = nullptr;
+ mDefaultDBState->readListener = nullptr;
+ mDefaultDBState->syncConn = nullptr;
+ mDefaultDBState->hostArray.Clear();
+ mDefaultDBState->readSet.Clear();
+
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): %ld cookies read",
+ mDefaultDBState->cookieCount));
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-read", nullptr);
+ }
+}
+
+void
+nsCookieService::CancelAsyncRead(bool aPurgeReadSet)
+{
+ // We may be in the private browsing DB state, with a pending read on the
+ // default DB state. (This would occur if we started up in private browsing
+ // mode.) As long as we do all our operations on the default state, we're OK.
+ NS_ASSERTION(mDefaultDBState, "no default DBState");
+ NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read");
+ NS_ASSERTION(mDefaultDBState->readListener, "no read listener");
+
+ // Cancel the pending read, kill the read listener, and empty the array
+ // of data already read in on the background thread.
+ mDefaultDBState->readListener->Cancel();
+ DebugOnly<nsresult> rv = mDefaultDBState->pendingRead->Cancel();
+ NS_ASSERT_SUCCESS(rv);
+
+ mDefaultDBState->stmtReadDomain = nullptr;
+ mDefaultDBState->pendingRead = nullptr;
+ mDefaultDBState->readListener = nullptr;
+ mDefaultDBState->hostArray.Clear();
+
+ // Only clear the 'readSet' table if we no longer need to know what set of
+ // data is already accounted for.
+ if (aPurgeReadSet)
+ mDefaultDBState->readSet.Clear();
+}
+
+void
+nsCookieService::EnsureReadDomain(const nsCookieKey &aKey)
+{
+ NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState,
+ "not in default db state");
+
+ // Fast path 1: nothing to read, or we've already finished reading.
+ if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead))
+ return;
+
+ // Fast path 2: already read in this particular domain.
+ if (MOZ_LIKELY(mDefaultDBState->readSet.GetEntry(aKey)))
+ return;
+
+ // Read in the data synchronously.
+ // see IDX_NAME, etc. for parameter indexes
+ nsresult rv;
+ if (!mDefaultDBState->stmtReadDomain) {
+ // Cache the statement, since it's likely to be used again.
+ rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly "
+ "FROM moz_cookies "
+ "WHERE baseDomain = :baseDomain "
+ " AND originAttributes = :originAttributes"),
+ getter_AddRefs(mDefaultDBState->stmtReadDomain));
+
+ if (NS_FAILED(rv)) {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadDomain(): corruption detected when creating statement "
+ "with rv 0x%x", rv));
+ HandleCorruptDB(mDefaultDBState);
+ return;
+ }
+ }
+
+ NS_ASSERTION(mDefaultDBState->syncConn, "should have a sync db connection");
+
+ mozStorageStatementScoper scoper(mDefaultDBState->stmtReadDomain);
+
+ rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("baseDomain"), aKey.mBaseDomain);
+ NS_ASSERT_SUCCESS(rv);
+
+ nsAutoCString suffix;
+ aKey.mOriginAttributes.CreateSuffix(suffix);
+ rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName(
+ NS_LITERAL_CSTRING("originAttributes"), suffix);
+ NS_ASSERT_SUCCESS(rv);
+
+ bool hasResult;
+ nsCString name, value, host, path;
+ AutoTArray<RefPtr<nsCookie>, kMaxCookiesPerHost> array;
+ while (true) {
+ rv = mDefaultDBState->stmtReadDomain->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv)) {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadDomain(): corruption detected when reading result "
+ "with rv 0x%x", rv));
+ HandleCorruptDB(mDefaultDBState);
+ return;
+ }
+
+ if (!hasResult)
+ break;
+
+ array.AppendElement(GetCookieFromRow(mDefaultDBState->stmtReadDomain,
+ aKey.mOriginAttributes));
+ }
+
+ // Add the cookies to the table in a single operation. This makes sure that
+ // either all the cookies get added, or in the case of corruption, none.
+ for (uint32_t i = 0; i < array.Length(); ++i) {
+ AddCookieToList(aKey, array[i], mDefaultDBState, nullptr, false);
+ }
+
+ // Add it to the hashset of read entries, so we don't read it again.
+ mDefaultDBState->readSet.PutEntry(aKey);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadDomain(): %ld cookies read for base domain %s, "
+ " originAttributes = %s", array.Length(), aKey.mBaseDomain.get(),
+ suffix.get()));
+}
+
+void
+nsCookieService::EnsureReadComplete()
+{
+ NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState,
+ "not in default db state");
+
+ // Fast path 1: nothing to read, or we've already finished reading.
+ if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead))
+ return;
+
+ // Cancel the pending read, so we don't get any more results.
+ CancelAsyncRead(false);
+
+ // Read in the data synchronously.
+ // see IDX_NAME, etc. for parameter indexes
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "baseDomain, "
+ "originAttributes "
+ "FROM moz_cookies "
+ "WHERE baseDomain NOTNULL"), getter_AddRefs(stmt));
+
+ if (NS_FAILED(rv)) {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadComplete(): corruption detected when creating statement "
+ "with rv 0x%x", rv));
+ HandleCorruptDB(mDefaultDBState);
+ return;
+ }
+
+ nsCString baseDomain, name, value, host, path;
+ bool hasResult;
+ nsTArray<CookieDomainTuple> array(kMaxNumberOfCookies);
+ while (true) {
+ rv = stmt->ExecuteStep(&hasResult);
+ if (NS_FAILED(rv)) {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadComplete(): corruption detected when reading result "
+ "with rv 0x%x", rv));
+ HandleCorruptDB(mDefaultDBState);
+ return;
+ }
+
+ if (!hasResult)
+ break;
+
+ // Make sure we haven't already read the data.
+ stmt->GetUTF8String(IDX_BASE_DOMAIN, baseDomain);
+
+ nsAutoCString suffix;
+ NeckoOriginAttributes attrs;
+ stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix);
+ // If PopulateFromSuffix failed we just ignore the OA attributes
+ // that we don't support
+ Unused << attrs.PopulateFromSuffix(suffix);
+
+ nsCookieKey key(baseDomain, attrs);
+ if (mDefaultDBState->readSet.GetEntry(key))
+ continue;
+
+ CookieDomainTuple* tuple = array.AppendElement();
+ tuple->key = key;
+ tuple->cookie = GetCookieFromRow(stmt, attrs);
+ }
+
+ // Add the cookies to the table in a single operation. This makes sure that
+ // either all the cookies get added, or in the case of corruption, none.
+ for (uint32_t i = 0; i < array.Length(); ++i) {
+ CookieDomainTuple& tuple = array[i];
+ AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr,
+ false);
+ }
+
+ mDefaultDBState->syncConn = nullptr;
+ mDefaultDBState->readSet.Clear();
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("EnsureReadComplete(): %ld cookies read", array.Length()));
+}
+
+NS_IMETHODIMP
+nsCookieService::ImportCookies(nsIFile *aCookieFile)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Make sure we're in the default DB state. We don't want people importing
+ // cookies into a private browsing session!
+ if (mDBState != mDefaultDBState) {
+ NS_WARNING("Trying to import cookies in a private browsing session!");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIInputStream> fileInputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(fileInputStream, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // First, ensure we've read in everything from the database, if we have one.
+ EnsureReadComplete();
+
+ static const char kTrue[] = "TRUE";
+
+ nsAutoCString buffer, baseDomain;
+ bool isMore = true;
+ int32_t hostIndex, isDomainIndex, pathIndex, secureIndex, expiresIndex, nameIndex, cookieIndex;
+ nsASingleFragmentCString::char_iterator iter;
+ int32_t numInts;
+ int64_t expires;
+ bool isDomain, isHttpOnly = false;
+ uint32_t originalCookieCount = mDefaultDBState->cookieCount;
+
+ int64_t currentTimeInUsec = PR_Now();
+ int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
+ // we use lastAccessedCounter to keep cookies in recently-used order,
+ // so we start by initializing to currentTime (somewhat arbitrary)
+ int64_t lastAccessedCounter = currentTimeInUsec;
+
+ /* file format is:
+ *
+ * host \t isDomain \t path \t secure \t expires \t name \t cookie
+ *
+ * if this format isn't respected we move onto the next line in the file.
+ * isDomain is "TRUE" or "FALSE" (default to "FALSE")
+ * isSecure is "TRUE" or "FALSE" (default to "TRUE")
+ * expires is a int64_t integer
+ * note 1: cookie can contain tabs.
+ * note 2: cookies will be stored in order of lastAccessed time:
+ * most-recently used come first; least-recently-used come last.
+ */
+
+ /*
+ * ...but due to bug 178933, we hide HttpOnly cookies from older code
+ * in a comment, so they don't expose HttpOnly cookies to JS.
+ *
+ * The format for HttpOnly cookies is
+ *
+ * #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie
+ *
+ */
+
+ // We will likely be adding a bunch of cookies to the DB, so we use async
+ // batching with storage to make this super fast.
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ if (originalCookieCount == 0 && mDefaultDBState->dbConn) {
+ mDefaultDBState->stmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
+ if (StringBeginsWith(buffer, NS_LITERAL_CSTRING(HTTP_ONLY_PREFIX))) {
+ isHttpOnly = true;
+ hostIndex = sizeof(HTTP_ONLY_PREFIX) - 1;
+ } else if (buffer.IsEmpty() || buffer.First() == '#') {
+ continue;
+ } else {
+ isHttpOnly = false;
+ hostIndex = 0;
+ }
+
+ // this is a cheap, cheesy way of parsing a tab-delimited line into
+ // string indexes, which can be lopped off into substrings. just for
+ // purposes of obfuscation, it also checks that each token was found.
+ // todo: use iterators?
+ if ((isDomainIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 ||
+ (pathIndex = buffer.FindChar('\t', isDomainIndex) + 1) == 0 ||
+ (secureIndex = buffer.FindChar('\t', pathIndex) + 1) == 0 ||
+ (expiresIndex = buffer.FindChar('\t', secureIndex) + 1) == 0 ||
+ (nameIndex = buffer.FindChar('\t', expiresIndex) + 1) == 0 ||
+ (cookieIndex = buffer.FindChar('\t', nameIndex) + 1) == 0) {
+ continue;
+ }
+
+ // check the expirytime first - if it's expired, ignore
+ // nullstomp the trailing tab, to avoid copying the string
+ buffer.BeginWriting(iter);
+ *(iter += nameIndex - 1) = char(0);
+ numInts = PR_sscanf(buffer.get() + expiresIndex, "%lld", &expires);
+ if (numInts != 1 || expires < currentTime) {
+ continue;
+ }
+
+ isDomain = Substring(buffer, isDomainIndex, pathIndex - isDomainIndex - 1).EqualsLiteral(kTrue);
+ const nsASingleFragmentCString &host = Substring(buffer, hostIndex, isDomainIndex - hostIndex - 1);
+ // check for bad legacy cookies (domain not starting with a dot, or containing a port),
+ // and discard
+ if ((isDomain && !host.IsEmpty() && host.First() != '.') ||
+ host.Contains(':')) {
+ continue;
+ }
+
+ // compute the baseDomain from the host
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ if (NS_FAILED(rv))
+ continue;
+
+ // pre-existing cookies have appId=0, inIsolatedMozBrowser=false set by default
+ // constructor of NeckoOriginAttributes().
+ nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
+
+ // Create a new nsCookie and assign the data. We don't know the cookie
+ // creation time, so just use the current time to generate a unique one.
+ RefPtr<nsCookie> newCookie =
+ nsCookie::Create(Substring(buffer, nameIndex, cookieIndex - nameIndex - 1),
+ Substring(buffer, cookieIndex, buffer.Length() - cookieIndex),
+ host,
+ Substring(buffer, pathIndex, secureIndex - pathIndex - 1),
+ expires,
+ lastAccessedCounter,
+ nsCookie::GenerateUniqueCreationTime(currentTimeInUsec),
+ false,
+ Substring(buffer, secureIndex, expiresIndex - secureIndex - 1).EqualsLiteral(kTrue),
+ isHttpOnly,
+ key.mOriginAttributes);
+ if (!newCookie) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // trick: preserve the most-recently-used cookie ordering,
+ // by successively decrementing the lastAccessed time
+ lastAccessedCounter--;
+
+ if (originalCookieCount == 0) {
+ AddCookieToList(key, newCookie, mDefaultDBState, paramsArray);
+ }
+ else {
+ AddInternal(key, newCookie, currentTimeInUsec,
+ nullptr, nullptr, true);
+ }
+ }
+
+ // If we need to write to disk, do so now.
+ if (paramsArray) {
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length) {
+ rv = mDefaultDBState->stmtInsert->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = mDefaultDBState->stmtInsert->ExecuteAsync(
+ mDefaultDBState->insertListener, getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ }
+ }
+
+
+ COOKIE_LOGSTRING(LogLevel::Debug, ("ImportCookies(): %ld cookies imported",
+ mDefaultDBState->cookieCount));
+
+ return NS_OK;
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * private GetCookie/SetCookie helpers
+ ******************************************************************************/
+
+// helper function for GetCookieList
+static inline bool ispathdelimiter(char c) { return c == '/' || c == '?' || c == '#' || c == ';'; }
+
+// Comparator class for sorting cookies before sending to a server.
+class CompareCookiesForSending
+{
+public:
+ bool Equals(const nsCookie* aCookie1, const nsCookie* aCookie2) const
+ {
+ return aCookie1->CreationTime() == aCookie2->CreationTime() &&
+ aCookie2->Path().Length() == aCookie1->Path().Length();
+ }
+
+ bool LessThan(const nsCookie* aCookie1, const nsCookie* aCookie2) const
+ {
+ // compare by cookie path length in accordance with RFC2109
+ int32_t result = aCookie2->Path().Length() - aCookie1->Path().Length();
+ if (result != 0)
+ return result < 0;
+
+ // when path lengths match, older cookies should be listed first. this is
+ // required for backwards compatibility since some websites erroneously
+ // depend on receiving cookies in the order in which they were sent to the
+ // browser! see bug 236772.
+ return aCookie1->CreationTime() < aCookie2->CreationTime();
+ }
+};
+
+static bool
+DomainMatches(nsCookie* aCookie, const nsACString& aHost) {
+ // first, check for an exact host or domain cookie match, e.g. "google.com"
+ // or ".google.com"; second a subdomain match, e.g.
+ // host = "mail.google.com", cookie domain = ".google.com".
+ return aCookie->RawHost() == aHost ||
+ (aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host()));
+}
+
+static bool
+PathMatches(nsCookie* aCookie, const nsACString& aPath) {
+ // calculate cookie path length, excluding trailing '/'
+ uint32_t cookiePathLen = aCookie->Path().Length();
+ if (cookiePathLen > 0 && aCookie->Path().Last() == '/')
+ --cookiePathLen;
+
+ // if the given path is shorter than the cookie path, it doesn't match
+ // if the given path doesn't start with the cookie path, it doesn't match.
+ if (!StringBeginsWith(aPath, Substring(aCookie->Path(), 0, cookiePathLen)))
+ return false;
+
+ // if the given path is longer than the cookie path, and the first char after
+ // the cookie path is not a path delimiter, it doesn't match.
+ if (aPath.Length() > cookiePathLen &&
+ !ispathdelimiter(aPath.CharAt(cookiePathLen))) {
+ /*
+ * |ispathdelimiter| tests four cases: '/', '?', '#', and ';'.
+ * '/' is the "standard" case; the '?' test allows a site at host/abc?def
+ * to receive a cookie that has a path attribute of abc. this seems
+ * strange but at least one major site (citibank, bug 156725) depends
+ * on it. The test for # and ; are put in to proactively avoid problems
+ * with other sites - these are the only other chars allowed in the path.
+ */
+ return false;
+ }
+
+ // either the paths match exactly, or the cookie path is a prefix of
+ // the given path.
+ return true;
+}
+
+void
+nsCookieService::GetCookieStringInternal(nsIURI *aHostURI,
+ bool aIsForeign,
+ bool aHttpBound,
+ const NeckoOriginAttributes aOriginAttrs,
+ bool aIsPrivate,
+ nsCString &aCookieString)
+{
+ NS_ASSERTION(aHostURI, "null host!");
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return;
+ }
+
+ AutoRestore<DBState*> savePrevDBState(mDBState);
+ mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState;
+
+ // get the base domain, host, and path from the URI.
+ // e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk".
+ // file:// URI's (i.e. with an empty host) are allowed, but any other
+ // scheme must have a non-empty host. A trailing dot in the host
+ // is acceptable.
+ bool requireHostMatch;
+ nsAutoCString baseDomain, hostFromURI, pathFromURI;
+ nsresult rv = GetBaseDomain(aHostURI, baseDomain, requireHostMatch);
+ if (NS_SUCCEEDED(rv))
+ rv = aHostURI->GetAsciiHost(hostFromURI);
+ if (NS_SUCCEEDED(rv))
+ rv = aHostURI->GetPath(pathFromURI);
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nullptr, "invalid host/path from URI");
+ return;
+ }
+
+ // check default prefs
+ CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, nullptr);
+
+ // for GetCookie(), we don't fire rejection notifications.
+ switch (cookieStatus) {
+ case STATUS_REJECTED:
+ case STATUS_REJECTED_WITH_ERROR:
+ return;
+ default:
+ break;
+ }
+
+ // Note: The following permissions logic is mirrored in
+ // toolkit/modules/addons/MatchPattern.jsm:MatchPattern.matchesCookie().
+ // If it changes, please update that function, or file a bug for someone
+ // else to do so.
+
+ // check if aHostURI is using an https secure protocol.
+ // if it isn't, then we can't send a secure cookie over the connection.
+ // if SchemeIs fails, assume an insecure connection, to be on the safe side
+ bool isSecure;
+ if (NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) {
+ isSecure = false;
+ }
+
+ nsCookie *cookie;
+ AutoTArray<nsCookie*, 8> foundCookieList;
+ int64_t currentTimeInUsec = PR_Now();
+ int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
+ bool stale = false;
+
+ nsCookieKey key(baseDomain, aOriginAttrs);
+ EnsureReadDomain(key);
+
+ // perform the hash lookup
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
+ if (!entry)
+ return;
+
+ // iterate the cookies!
+ const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ cookie = cookies[i];
+
+ // check the host, since the base domain lookup is conservative.
+ if (!DomainMatches(cookie, hostFromURI))
+ continue;
+
+ // if the cookie is secure and the host scheme isn't, we can't send it
+ if (cookie->IsSecure() && !isSecure)
+ continue;
+
+ // if the cookie is httpOnly and it's not going directly to the HTTP
+ // connection, don't send it
+ if (cookie->IsHttpOnly() && !aHttpBound)
+ continue;
+
+ // if the nsIURI path doesn't match the cookie path, don't send it back
+ if (!PathMatches(cookie, pathFromURI))
+ continue;
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ continue;
+ }
+
+ // all checks passed - add to list and check if lastAccessed stamp needs updating
+ foundCookieList.AppendElement(cookie);
+ if (cookie->IsStale()) {
+ stale = true;
+ }
+ }
+
+ int32_t count = foundCookieList.Length();
+ if (count == 0)
+ return;
+
+ // update lastAccessed timestamps. we only do this if the timestamp is stale
+ // by a certain amount, to avoid thrashing the db during pageload.
+ if (stale) {
+ // Create an array of parameters to bind to our update statement. Batching
+ // is OK here since we're updating cookies with no interleaved operations.
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ mozIStorageAsyncStatement* stmt = mDBState->stmtUpdate;
+ if (mDBState->dbConn) {
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ for (int32_t i = 0; i < count; ++i) {
+ cookie = foundCookieList.ElementAt(i);
+
+ if (cookie->IsStale()) {
+ UpdateCookieInList(cookie, currentTimeInUsec, paramsArray);
+ }
+ }
+ // Update the database now if necessary.
+ if (paramsArray) {
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length) {
+ DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mDBState->updateListener,
+ getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ }
+ }
+ }
+
+ // return cookies in order of path length; longest to shortest.
+ // this is required per RFC2109. if cookies match in length,
+ // then sort by creation time (see bug 236772).
+ foundCookieList.Sort(CompareCookiesForSending());
+
+ for (int32_t i = 0; i < count; ++i) {
+ cookie = foundCookieList.ElementAt(i);
+
+ // check if we have anything to write
+ if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
+ // if we've already added a cookie to the return list, append a "; " so
+ // that subsequent cookies are delimited in the final list.
+ if (!aCookieString.IsEmpty()) {
+ aCookieString.AppendLiteral("; ");
+ }
+
+ if (!cookie->Name().IsEmpty()) {
+ // we have a name and value - write both
+ aCookieString += cookie->Name() + NS_LITERAL_CSTRING("=") + cookie->Value();
+ } else {
+ // just write value
+ aCookieString += cookie->Value();
+ }
+ }
+ }
+
+ if (!aCookieString.IsEmpty())
+ COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false);
+}
+
+// processes a single cookie, and returns true if there are more cookies
+// to be processed
+bool
+nsCookieService::SetCookieInternal(nsIURI *aHostURI,
+ const nsCookieKey &aKey,
+ bool aRequireHostMatch,
+ CookieStatus aStatus,
+ nsDependentCString &aCookieHeader,
+ int64_t aServerTime,
+ bool aFromHttp,
+ nsIChannel *aChannel)
+{
+ NS_ASSERTION(aHostURI, "null host!");
+
+ // create a stack-based nsCookieAttributes, to store all the
+ // attributes parsed from the cookie
+ nsCookieAttributes cookieAttributes;
+
+ // init expiryTime such that session cookies won't prematurely expire
+ cookieAttributes.expiryTime = INT64_MAX;
+
+ // aCookieHeader is an in/out param to point to the next cookie, if
+ // there is one. Save the present value for logging purposes
+ nsDependentCString savedCookieHeader(aCookieHeader);
+
+ // newCookie says whether there are multiple cookies in the header;
+ // so we can handle them separately.
+ bool newCookie = ParseAttributes(aCookieHeader, cookieAttributes);
+
+ // Collect telemetry on how often secure cookies are set from non-secure
+ // origins, and vice-versa.
+ //
+ // 0 = nonsecure and "http:"
+ // 1 = nonsecure and "https:"
+ // 2 = secure and "http:"
+ // 3 = secure and "https:"
+ bool isHTTPS;
+ nsresult rv = aHostURI->SchemeIs("https", &isHTTPS);
+ if (NS_SUCCEEDED(rv)) {
+ Telemetry::Accumulate(Telemetry::COOKIE_SCHEME_SECURITY,
+ ((cookieAttributes.isSecure)? 0x02 : 0x00) |
+ ((isHTTPS)? 0x01 : 0x00));
+ }
+
+ int64_t currentTimeInUsec = PR_Now();
+
+ // calculate expiry time of cookie.
+ cookieAttributes.isSession = GetExpiry(cookieAttributes, aServerTime,
+ currentTimeInUsec / PR_USEC_PER_SEC);
+ if (aStatus == STATUS_ACCEPT_SESSION) {
+ // force lifetime to session. note that the expiration time, if set above,
+ // will still apply.
+ cookieAttributes.isSession = true;
+ }
+
+ // reject cookie if it's over the size limit, per RFC2109
+ if ((cookieAttributes.name.Length() + cookieAttributes.value.Length()) > kMaxBytesPerCookie) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie too big (> 4kb)");
+ return newCookie;
+ }
+
+ const char illegalNameCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
+ 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12,
+ 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
+ 0x1F, 0x00 };
+ if (cookieAttributes.name.FindCharInSet(illegalNameCharacters, 0) != -1) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "invalid name character");
+ return newCookie;
+ }
+
+ // domain & path checks
+ if (!CheckDomain(cookieAttributes, aHostURI, aKey.mBaseDomain, aRequireHostMatch)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the domain tests");
+ return newCookie;
+ }
+ if (!CheckPath(cookieAttributes, aHostURI)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the path tests");
+ return newCookie;
+ }
+ // magic prefix checks. MUST be run after CheckDomain() and CheckPath()
+ if (!CheckPrefixes(cookieAttributes, isHTTPS)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the prefix tests");
+ return newCookie;
+ }
+
+ // reject cookie if value contains an RFC 6265 disallowed character - see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423
+ // NOTE: this is not the full set of characters disallowed by 6265 - notably
+ // 0x09, 0x20, 0x22, 0x2C, 0x5C, and 0x7F are missing from this list. This is
+ // for parity with Chrome. This only applies to cookies set via the Set-Cookie
+ // header, as document.cookie is defined to be UTF-8. Hooray for
+ // symmetry!</sarcasm>
+ const char illegalCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
+ 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
+ 0x1E, 0x1F, 0x3B, 0x00 };
+ if (aFromHttp && (cookieAttributes.value.FindCharInSet(illegalCharacters, 0) != -1)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "invalid value character");
+ return newCookie;
+ }
+
+ // create a new nsCookie and copy attributes
+ RefPtr<nsCookie> cookie =
+ nsCookie::Create(cookieAttributes.name,
+ cookieAttributes.value,
+ cookieAttributes.host,
+ cookieAttributes.path,
+ cookieAttributes.expiryTime,
+ currentTimeInUsec,
+ nsCookie::GenerateUniqueCreationTime(currentTimeInUsec),
+ cookieAttributes.isSession,
+ cookieAttributes.isSecure,
+ cookieAttributes.isHttpOnly,
+ aKey.mOriginAttributes);
+ if (!cookie)
+ return newCookie;
+
+ // check permissions from site permission list, or ask the user,
+ // to determine if we can set the cookie
+ if (mPermissionService) {
+ bool permission;
+ mPermissionService->CanSetCookie(aHostURI,
+ aChannel,
+ static_cast<nsICookie2*>(static_cast<nsCookie*>(cookie)),
+ &cookieAttributes.isSession,
+ &cookieAttributes.expiryTime,
+ &permission);
+ if (!permission) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie rejected by permission manager");
+ NotifyRejected(aHostURI);
+ return newCookie;
+ }
+
+ // update isSession and expiry attributes, in case they changed
+ cookie->SetIsSession(cookieAttributes.isSession);
+ cookie->SetExpiry(cookieAttributes.expiryTime);
+ }
+
+ // add the cookie to the list. AddInternal() takes care of logging.
+ // we get the current time again here, since it may have changed during prompting
+ AddInternal(aKey, cookie, PR_Now(), aHostURI, savedCookieHeader.get(),
+ aFromHttp);
+ return newCookie;
+}
+
+// this is a backend function for adding a cookie to the list, via SetCookie.
+// also used in the cookie manager, for profile migration from IE.
+// it either replaces an existing cookie; or adds the cookie to the hashtable,
+// and deletes a cookie (if maximum number of cookies has been
+// reached). also performs list maintenance by removing expired cookies.
+void
+nsCookieService::AddInternal(const nsCookieKey &aKey,
+ nsCookie *aCookie,
+ int64_t aCurrentTimeInUsec,
+ nsIURI *aHostURI,
+ const char *aCookieHeader,
+ bool aFromHttp)
+{
+ int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
+
+ // if the new cookie is httponly, make sure we're not coming from script
+ if (!aFromHttp && aCookie->IsHttpOnly()) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie is httponly; coming from script");
+ return;
+ }
+
+ bool isSecure = true;
+ if (aHostURI && NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) {
+ isSecure = false;
+ }
+
+ // If the new cookie is non-https and wants to set secure flag,
+ // browser have to ignore this new cookie.
+ // (draft-ietf-httpbis-cookie-alone section 3.1)
+ if (mLeaveSecureAlone && aCookie->IsSecure() && !isSecure) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "non-https cookie can't set secure flag");
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ BLOCKED_SECURE_SET_FROM_HTTP);
+ return;
+ }
+ nsListIter exactIter;
+ bool foundCookie = false;
+ if (mLeaveSecureAlone) {
+ // Step1, call FindSecureCookie(). FindSecureCookie() would
+ // find the existing cookie with the security flag and has
+ // the same name, host and path of the new cookie, if there is any.
+ // Step2, Confirm new cookie's security setting. If any targeted
+ // cookie had been found in Step1, then confirm whether the
+ // new cookie could modify it. If the new created cookie’s
+ // "secure-only-flag" is not set, and the "scheme" component
+ // of the "request-uri" does not denote a "secure" protocol,
+ // then ignore the new cookie.
+ // (draft-ietf-httpbis-cookie-alone section 3.2)
+ foundCookie = FindSecureCookie(aKey, aCookie);
+ if (foundCookie && !aCookie->IsSecure()) {
+ if (!isSecure) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie can't save because older cookie is secure cookie but newer cookie is non-secure cookie");
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ BLOCKED_DOWNGRADE_SECURE);
+ return;
+ } else {
+ // A secure site is allowed to downgrade a secure cookie
+ // but we want to measure anyway
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ DOWNGRADE_SECURE_FROM_SECURE);
+ }
+ }
+ }
+
+ foundCookie = FindCookie(aKey, aCookie->Host(),
+ aCookie->Name(), aCookie->Path(), exactIter);
+
+ RefPtr<nsCookie> oldCookie;
+ nsCOMPtr<nsIArray> purgedList;
+ if (foundCookie) {
+ oldCookie = exactIter.Cookie();
+
+ // Check if the old cookie is stale (i.e. has already expired). If so, we
+ // need to be careful about the semantics of removing it and adding the new
+ // cookie: we want the behavior wrt adding the new cookie to be the same as
+ // if it didn't exist, but we still want to fire a removal notification.
+ if (oldCookie->Expiry() <= currentTime) {
+ if (aCookie->Expiry() <= currentTime) {
+ // The new cookie has expired and the old one is stale. Nothing to do.
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie has already expired");
+ return;
+ }
+
+ // Remove the stale cookie. We save notification for later, once all list
+ // modifications are complete.
+ RemoveCookieFromList(exactIter);
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "stale cookie was purged");
+ purgedList = CreatePurgeList(oldCookie);
+
+ // We've done all we need to wrt removing and notifying the stale cookie.
+ // From here on out, we pretend pretend it didn't exist, so that we
+ // preserve expected notification semantics when adding the new cookie.
+ foundCookie = false;
+
+ } else {
+ // If the old cookie is httponly, make sure we're not coming from script.
+ if (!aFromHttp && oldCookie->IsHttpOnly()) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "previously stored cookie is httponly; coming from script");
+ return;
+ }
+
+ // If the new cookie has the same value, expiry date, and isSecure,
+ // isSession, and isHttpOnly flags then we can just keep the old one.
+ // Only if any of these differ we would want to override the cookie.
+ if (oldCookie->Value().Equals(aCookie->Value()) &&
+ oldCookie->Expiry() == aCookie->Expiry() &&
+ oldCookie->IsSecure() == aCookie->IsSecure() &&
+ oldCookie->IsSession() == aCookie->IsSession() &&
+ oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
+ // We don't want to perform this optimization if the cookie is
+ // considered stale, since in this case we would need to update the
+ // database.
+ !oldCookie->IsStale()) {
+ // Update the last access time on the old cookie.
+ oldCookie->SetLastAccessed(aCookie->LastAccessed());
+ UpdateCookieOldestTime(mDBState, oldCookie);
+ return;
+ }
+
+ // Remove the old cookie.
+ RemoveCookieFromList(exactIter);
+
+ // If the new cookie has expired -- i.e. the intent was simply to delete
+ // the old cookie -- then we're done.
+ if (aCookie->Expiry() <= currentTime) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "previously stored cookie was deleted");
+ NotifyChanged(oldCookie, u"deleted");
+ return;
+ }
+
+ // Preserve creation time of cookie for ordering purposes.
+ aCookie->SetCreationTime(oldCookie->CreationTime());
+ }
+
+ } else {
+ // check if cookie has already expired
+ if (aCookie->Expiry() <= currentTime) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie has already expired");
+ return;
+ }
+
+ // check if we have to delete an old cookie.
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
+ if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
+ nsListIter iter;
+ // Prioritize evicting insecure cookies.
+ // (draft-ietf-httpbis-cookie-alone section 3.3)
+ mozilla::Maybe<bool> optionalSecurity = mLeaveSecureAlone ? Some(false) : Nothing();
+ int64_t oldestCookieTime = FindStaleCookie(entry, currentTime, aHostURI, optionalSecurity, iter);
+ if (iter.entry == nullptr) {
+ if (aCookie->IsSecure()) {
+ // It's valid to evict a secure cookie for another secure cookie.
+ oldestCookieTime = FindStaleCookie(entry, currentTime, aHostURI, Some(true), iter);
+ } else {
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ EVICTING_SECURE_BLOCKED);
+ COOKIE_LOGEVICTED(aCookie,
+ "Too many cookies for this domain and the new cookie is not a secure cookie");
+ return;
+ }
+ }
+
+ MOZ_ASSERT(iter.entry);
+
+ oldCookie = iter.Cookie();
+ if (oldestCookieTime > 0 && mLeaveSecureAlone) {
+ TelemetryForEvictingStaleCookie(oldCookie, oldestCookieTime);
+ }
+
+ // remove the oldest cookie from the domain
+ RemoveCookieFromList(iter);
+ COOKIE_LOGEVICTED(oldCookie, "Too many cookies for this domain");
+ purgedList = CreatePurgeList(oldCookie);
+ } else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
+ int64_t maxAge = aCurrentTimeInUsec - mDBState->cookieOldestTime;
+ int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
+ if (maxAge >= purgeAge) {
+ // we're over both size and age limits by 10%; time to purge the table!
+ // do this by:
+ // 1) removing expired cookies;
+ // 2) evicting the balance of old cookies until we reach the size limit.
+ // note that the cookieOldestTime indicator can be pessimistic - if it's
+ // older than the actual oldest cookie, we'll just purge more eagerly.
+ purgedList = PurgeCookies(aCurrentTimeInUsec);
+ }
+ }
+ }
+
+ // Add the cookie to the db. We do not supply a params array for batching
+ // because this might result in removals and additions being out of order.
+ AddCookieToList(aKey, aCookie, mDBState, nullptr);
+ COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
+
+ // Now that list mutations are complete, notify observers. We do it here
+ // because observers may themselves attempt to mutate the list.
+ if (purgedList) {
+ NotifyChanged(purgedList, u"batch-deleted");
+ }
+
+ NotifyChanged(aCookie, foundCookie ? u"changed" : u"added");
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * private cookie header parsing functions
+ ******************************************************************************/
+
+// The following comment block elucidates the function of ParseAttributes.
+/******************************************************************************
+ ** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1
+ ** please note: this BNF deviates from both specifications, and reflects this
+ ** implementation. <bnf> indicates a reference to the defined grammar "bnf".
+
+ ** Differences from RFC2109/2616 and explanations:
+ 1. implied *LWS
+ The grammar described by this specification is word-based. Except
+ where noted otherwise, linear white space (<LWS>) can be included
+ between any two adjacent words (token or quoted-string), and
+ between adjacent words and separators, without changing the
+ interpretation of a field.
+ <LWS> according to spec is SP|HT|CR|LF, but here, we allow only SP | HT.
+
+ 2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in
+ common use inside values.
+
+ 3. tokens and values have looser restrictions on allowed characters than
+ spec. This is also due to certain characters being in common use inside
+ values. We allow only '=' to separate token/value pairs, and ';' to
+ terminate tokens or values. <LWS> is allowed within tokens and values
+ (see bug 206022).
+
+ 4. where appropriate, full <OCTET>s are allowed, where the spec dictates to
+ reject control chars or non-ASCII chars. This is erring on the loose
+ side, since there's probably no good reason to enforce this strictness.
+
+ 5. cookie <NAME> is optional, where spec requires it. This is a fairly
+ trivial case, but allows the flexibility of setting only a cookie <VALUE>
+ with a blank <NAME> and is required by some sites (see bug 169091).
+
+ 6. Attribute "HttpOnly", not covered in the RFCs, is supported
+ (see bug 178993).
+
+ ** Begin BNF:
+ token = 1*<any allowed-chars except separators>
+ value = 1*<any allowed-chars except value-sep>
+ separators = ";" | "="
+ value-sep = ";"
+ cookie-sep = CR | LF
+ allowed-chars = <any OCTET except NUL or cookie-sep>
+ OCTET = <any 8-bit sequence of data>
+ LWS = SP | HT
+ NUL = <US-ASCII NUL, null control character (0)>
+ CR = <US-ASCII CR, carriage return (13)>
+ LF = <US-ASCII LF, linefeed (10)>
+ SP = <US-ASCII SP, space (32)>
+ HT = <US-ASCII HT, horizontal-tab (9)>
+
+ set-cookie = "Set-Cookie:" cookies
+ cookies = cookie *( cookie-sep cookie )
+ cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first
+ NAME = token ; cookie name
+ VALUE = value ; cookie value
+ cookie-av = token ["=" value]
+
+ valid values for cookie-av (checked post-parsing) are:
+ cookie-av = "Path" "=" value
+ | "Domain" "=" value
+ | "Expires" "=" value
+ | "Max-Age" "=" value
+ | "Comment" "=" value
+ | "Version" "=" value
+ | "Secure"
+ | "HttpOnly"
+
+******************************************************************************/
+
+// helper functions for GetTokenValue
+static inline bool iswhitespace (char c) { return c == ' ' || c == '\t'; }
+static inline bool isterminator (char c) { return c == '\n' || c == '\r'; }
+static inline bool isvalueseparator (char c) { return isterminator(c) || c == ';'; }
+static inline bool istokenseparator (char c) { return isvalueseparator(c) || c == '='; }
+
+// Parse a single token/value pair.
+// Returns true if a cookie terminator is found, so caller can parse new cookie.
+bool
+nsCookieService::GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter,
+ nsASingleFragmentCString::const_char_iterator &aEndIter,
+ nsDependentCSubstring &aTokenString,
+ nsDependentCSubstring &aTokenValue,
+ bool &aEqualsFound)
+{
+ nsASingleFragmentCString::const_char_iterator start, lastSpace;
+ // initialize value string to clear garbage
+ aTokenValue.Rebind(aIter, aIter);
+
+ // find <token>, including any <LWS> between the end-of-token and the
+ // token separator. we'll remove trailing <LWS> next
+ while (aIter != aEndIter && iswhitespace(*aIter))
+ ++aIter;
+ start = aIter;
+ while (aIter != aEndIter && !istokenseparator(*aIter))
+ ++aIter;
+
+ // remove trailing <LWS>; first check we're not at the beginning
+ lastSpace = aIter;
+ if (lastSpace != start) {
+ while (--lastSpace != start && iswhitespace(*lastSpace))
+ continue;
+ ++lastSpace;
+ }
+ aTokenString.Rebind(start, lastSpace);
+
+ aEqualsFound = (*aIter == '=');
+ if (aEqualsFound) {
+ // find <value>
+ while (++aIter != aEndIter && iswhitespace(*aIter))
+ continue;
+
+ start = aIter;
+
+ // process <token>
+ // just look for ';' to terminate ('=' allowed)
+ while (aIter != aEndIter && !isvalueseparator(*aIter))
+ ++aIter;
+
+ // remove trailing <LWS>; first check we're not at the beginning
+ if (aIter != start) {
+ lastSpace = aIter;
+ while (--lastSpace != start && iswhitespace(*lastSpace))
+ continue;
+ aTokenValue.Rebind(start, ++lastSpace);
+ }
+ }
+
+ // aIter is on ';', or terminator, or EOS
+ if (aIter != aEndIter) {
+ // if on terminator, increment past & return true to process new cookie
+ if (isterminator(*aIter)) {
+ ++aIter;
+ return true;
+ }
+ // fall-through: aIter is on ';', increment and return false
+ ++aIter;
+ }
+ return false;
+}
+
+// Parses attributes from cookie header. expires/max-age attributes aren't folded into the
+// cookie struct here, because we don't know which one to use until we've parsed the header.
+bool
+nsCookieService::ParseAttributes(nsDependentCString &aCookieHeader,
+ nsCookieAttributes &aCookieAttributes)
+{
+ static const char kPath[] = "path";
+ static const char kDomain[] = "domain";
+ static const char kExpires[] = "expires";
+ static const char kMaxage[] = "max-age";
+ static const char kSecure[] = "secure";
+ static const char kHttpOnly[] = "httponly";
+
+ nsASingleFragmentCString::const_char_iterator tempBegin, tempEnd;
+ nsASingleFragmentCString::const_char_iterator cookieStart, cookieEnd;
+ aCookieHeader.BeginReading(cookieStart);
+ aCookieHeader.EndReading(cookieEnd);
+
+ aCookieAttributes.isSecure = false;
+ aCookieAttributes.isHttpOnly = false;
+
+ nsDependentCSubstring tokenString(cookieStart, cookieStart);
+ nsDependentCSubstring tokenValue (cookieStart, cookieStart);
+ bool newCookie, equalsFound;
+
+ // extract cookie <NAME> & <VALUE> (first attribute), and copy the strings.
+ // if we find multiple cookies, return for processing
+ // note: if there's no '=', we assume token is <VALUE>. this is required by
+ // some sites (see bug 169091).
+ // XXX fix the parser to parse according to <VALUE> grammar for this case
+ newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound);
+ if (equalsFound) {
+ aCookieAttributes.name = tokenString;
+ aCookieAttributes.value = tokenValue;
+ } else {
+ aCookieAttributes.value = tokenString;
+ }
+
+ // extract remaining attributes
+ while (cookieStart != cookieEnd && !newCookie) {
+ newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound);
+
+ if (!tokenValue.IsEmpty()) {
+ tokenValue.BeginReading(tempBegin);
+ tokenValue.EndReading(tempEnd);
+ }
+
+ // decide which attribute we have, and copy the string
+ if (tokenString.LowerCaseEqualsLiteral(kPath))
+ aCookieAttributes.path = tokenValue;
+
+ else if (tokenString.LowerCaseEqualsLiteral(kDomain))
+ aCookieAttributes.host = tokenValue;
+
+ else if (tokenString.LowerCaseEqualsLiteral(kExpires))
+ aCookieAttributes.expires = tokenValue;
+
+ else if (tokenString.LowerCaseEqualsLiteral(kMaxage))
+ aCookieAttributes.maxage = tokenValue;
+
+ // ignore any tokenValue for isSecure; just set the boolean
+ else if (tokenString.LowerCaseEqualsLiteral(kSecure))
+ aCookieAttributes.isSecure = true;
+
+ // ignore any tokenValue for isHttpOnly (see bug 178993);
+ // just set the boolean
+ else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly))
+ aCookieAttributes.isHttpOnly = true;
+ }
+
+ // rebind aCookieHeader, in case we need to process another cookie
+ aCookieHeader.Rebind(cookieStart, cookieEnd);
+ return newCookie;
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * private domain & permission compliance enforcement functions
+ ******************************************************************************/
+
+// Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be
+// "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing
+// dot may be present. If aHostURI is an IP address, an alias such as
+// 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will
+// be the exact host, and aRequireHostMatch will be true to indicate that
+// substring matches should not be performed.
+nsresult
+nsCookieService::GetBaseDomain(nsIURI *aHostURI,
+ nsCString &aBaseDomain,
+ bool &aRequireHostMatch)
+{
+ // get the base domain. this will fail if the host contains a leading dot,
+ // more than one trailing dot, or is otherwise malformed.
+ nsresult rv = mTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain);
+ aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
+ if (aRequireHostMatch) {
+ // aHostURI is either an IP address, an alias such as 'localhost', an eTLD
+ // such as 'co.uk', or the empty string. use the host as a key in such
+ // cases.
+ rv = aHostURI->GetAsciiHost(aBaseDomain);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // aHost (and thus aBaseDomain) may be the string '.'. If so, fail.
+ if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.')
+ return NS_ERROR_INVALID_ARG;
+
+ // block any URIs without a host that aren't file:// URIs.
+ if (aBaseDomain.IsEmpty()) {
+ bool isFileURI = false;
+ aHostURI->SchemeIs("file", &isFileURI);
+ if (!isFileURI)
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+// Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be
+// "bbc.co.uk". This is done differently than GetBaseDomain(): it is assumed
+// that aHost is already normalized, and it may contain a leading dot
+// (indicating that it represents a domain). A trailing dot may be present.
+// If aHost is an IP address, an alias such as 'localhost', an eTLD such as
+// 'co.uk', or the empty string, aBaseDomain will be the exact host, and a
+// leading dot will be treated as an error.
+nsresult
+nsCookieService::GetBaseDomainFromHost(const nsACString &aHost,
+ nsCString &aBaseDomain)
+{
+ // aHost must not be the string '.'.
+ if (aHost.Length() == 1 && aHost.Last() == '.')
+ return NS_ERROR_INVALID_ARG;
+
+ // aHost may contain a leading dot; if so, strip it now.
+ bool domain = !aHost.IsEmpty() && aHost.First() == '.';
+
+ // get the base domain. this will fail if the host contains a leading dot,
+ // more than one trailing dot, or is otherwise malformed.
+ nsresult rv = mTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0, aBaseDomain);
+ if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ // aHost is either an IP address, an alias such as 'localhost', an eTLD
+ // such as 'co.uk', or the empty string. use the host as a key in such
+ // cases; however, we reject any such hosts with a leading dot, since it
+ // doesn't make sense for them to be domain cookies.
+ if (domain)
+ return NS_ERROR_INVALID_ARG;
+
+ aBaseDomain = aHost;
+ return NS_OK;
+ }
+ return rv;
+}
+
+// Normalizes the given hostname, component by component. ASCII/ACE
+// components are lower-cased, and UTF-8 components are normalized per
+// RFC 3454 and converted to ACE.
+nsresult
+nsCookieService::NormalizeHost(nsCString &aHost)
+{
+ if (!IsASCII(aHost)) {
+ nsAutoCString host;
+ nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host);
+ if (NS_FAILED(rv))
+ return rv;
+
+ aHost = host;
+ }
+
+ ToLowerCase(aHost);
+ return NS_OK;
+}
+
+// returns true if 'a' is equal to or a subdomain of 'b',
+// assuming no leading dots are present.
+static inline bool IsSubdomainOf(const nsCString &a, const nsCString &b)
+{
+ if (a == b)
+ return true;
+ if (a.Length() > b.Length())
+ return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b);
+ return false;
+}
+
+CookieStatus
+nsCookieService::CheckPrefs(nsIURI *aHostURI,
+ bool aIsForeign,
+ const char *aCookieHeader)
+{
+ nsresult rv;
+
+ // don't let ftp sites get/set cookies (could be a security issue)
+ bool ftp;
+ if (NS_SUCCEEDED(aHostURI->SchemeIs("ftp", &ftp)) && ftp) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "ftp sites cannot read cookies");
+ return STATUS_REJECTED_WITH_ERROR;
+ }
+
+ // check the permission list first; if we find an entry, it overrides
+ // default prefs. see bug 184059.
+ if (mPermissionService) {
+ nsCookieAccess access;
+ // Not passing an nsIChannel here is probably OK; our implementation
+ // doesn't do anything with it anyway.
+ rv = mPermissionService->CanAccess(aHostURI, nullptr, &access);
+
+ // if we found an entry, use it
+ if (NS_SUCCEEDED(rv)) {
+ switch (access) {
+ case nsICookiePermission::ACCESS_DENY:
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
+ aCookieHeader, "cookies are blocked for this site");
+ return STATUS_REJECTED;
+
+ case nsICookiePermission::ACCESS_ALLOW:
+ return STATUS_ACCEPTED;
+
+ case nsICookiePermission::ACCESS_ALLOW_FIRST_PARTY_ONLY:
+ if (aIsForeign) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
+ aCookieHeader, "third party cookies are blocked "
+ "for this site");
+ return STATUS_REJECTED;
+
+ }
+ return STATUS_ACCEPTED;
+
+ case nsICookiePermission::ACCESS_LIMIT_THIRD_PARTY:
+ if (!aIsForeign)
+ return STATUS_ACCEPTED;
+ uint32_t priorCookieCount = 0;
+ nsAutoCString hostFromURI;
+ aHostURI->GetHost(hostFromURI);
+ CountCookiesFromHost(hostFromURI, &priorCookieCount);
+ if (priorCookieCount == 0) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
+ aCookieHeader, "third party cookies are blocked "
+ "for this site");
+ return STATUS_REJECTED;
+ }
+ return STATUS_ACCEPTED;
+ }
+ }
+ }
+
+ // check default prefs
+ if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "cookies are disabled");
+ return STATUS_REJECTED;
+ }
+
+ // check if cookie is foreign
+ if (aIsForeign) {
+ if (mCookieBehavior == nsICookieService::BEHAVIOR_ACCEPT && mThirdPartySession)
+ return STATUS_ACCEPT_SESSION;
+
+ if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party");
+ return STATUS_REJECTED;
+ }
+
+ if (mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) {
+ uint32_t priorCookieCount = 0;
+ nsAutoCString hostFromURI;
+ aHostURI->GetHost(hostFromURI);
+ CountCookiesFromHost(hostFromURI, &priorCookieCount);
+ if (priorCookieCount == 0) {
+ COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party");
+ return STATUS_REJECTED;
+ }
+ if (mThirdPartySession)
+ return STATUS_ACCEPT_SESSION;
+ }
+ }
+
+ // if nothing has complained, accept cookie
+ return STATUS_ACCEPTED;
+}
+
+// processes domain attribute, and returns true if host has permission to set for this domain.
+bool
+nsCookieService::CheckDomain(nsCookieAttributes &aCookieAttributes,
+ nsIURI *aHostURI,
+ const nsCString &aBaseDomain,
+ bool aRequireHostMatch)
+{
+ // Note: The logic in this function is mirrored in
+ // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions().
+ // If it changes, please update that function, or file a bug for someone
+ // else to do so.
+
+ // get host from aHostURI
+ nsAutoCString hostFromURI;
+ aHostURI->GetAsciiHost(hostFromURI);
+
+ // if a domain is given, check the host has permission
+ if (!aCookieAttributes.host.IsEmpty()) {
+ // Tolerate leading '.' characters, but not if it's otherwise an empty host.
+ if (aCookieAttributes.host.Length() > 1 &&
+ aCookieAttributes.host.First() == '.') {
+ aCookieAttributes.host.Cut(0, 1);
+ }
+
+ // switch to lowercase now, to avoid case-insensitive compares everywhere
+ ToLowerCase(aCookieAttributes.host);
+
+ // check whether the host is either an IP address, an alias such as
+ // 'localhost', an eTLD such as 'co.uk', or the empty string. in these
+ // cases, require an exact string match for the domain, and leave the cookie
+ // as a non-domain one. bug 105917 originally noted the requirement to deal
+ // with IP addresses.
+ if (aRequireHostMatch)
+ return hostFromURI.Equals(aCookieAttributes.host);
+
+ // ensure the proposed domain is derived from the base domain; and also
+ // that the host domain is derived from the proposed domain (per RFC2109).
+ if (IsSubdomainOf(aCookieAttributes.host, aBaseDomain) &&
+ IsSubdomainOf(hostFromURI, aCookieAttributes.host)) {
+ // prepend a dot to indicate a domain cookie
+ aCookieAttributes.host.Insert(NS_LITERAL_CSTRING("."), 0);
+ return true;
+ }
+
+ /*
+ * note: RFC2109 section 4.3.2 requires that we check the following:
+ * that the portion of host not in domain does not contain a dot.
+ * this prevents hosts of the form x.y.co.nz from setting cookies in the
+ * entire .co.nz domain. however, it's only a only a partial solution and
+ * it breaks sites (IE doesn't enforce it), so we don't perform this check.
+ */
+ return false;
+ }
+
+ // no domain specified, use hostFromURI
+ aCookieAttributes.host = hostFromURI;
+ return true;
+}
+
+nsCString
+GetPathFromURI(nsIURI* aHostURI)
+{
+ // strip down everything after the last slash to get the path,
+ // ignoring slashes in the query string part.
+ // if we can QI to nsIURL, that'll take care of the query string portion.
+ // otherwise, it's not an nsIURL and can't have a query string, so just find the last slash.
+ nsAutoCString path;
+ nsCOMPtr<nsIURL> hostURL = do_QueryInterface(aHostURI);
+ if (hostURL) {
+ hostURL->GetDirectory(path);
+ } else {
+ aHostURI->GetPath(path);
+ int32_t slash = path.RFindChar('/');
+ if (slash != kNotFound) {
+ path.Truncate(slash + 1);
+ }
+ }
+ return path;
+}
+
+bool
+nsCookieService::CheckPath(nsCookieAttributes &aCookieAttributes,
+ nsIURI *aHostURI)
+{
+ // if a path is given, check the host has permission
+ if (aCookieAttributes.path.IsEmpty() || aCookieAttributes.path.First() != '/') {
+ aCookieAttributes.path = GetPathFromURI(aHostURI);
+
+#if 0
+ } else {
+ /**
+ * The following test is part of the RFC2109 spec. Loosely speaking, it says that a site
+ * cannot set a cookie for a path that it is not on. See bug 155083. However this patch
+ * broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has
+ * been disabled, unless we can evangelize these sites.
+ */
+ // get path from aHostURI
+ nsAutoCString pathFromURI;
+ if (NS_FAILED(aHostURI->GetPath(pathFromURI)) ||
+ !StringBeginsWith(pathFromURI, aCookieAttributes.path)) {
+ return false;
+ }
+#endif
+ }
+
+ if (aCookieAttributes.path.Length() > kMaxBytesPerPath ||
+ aCookieAttributes.path.Contains('\t'))
+ return false;
+
+ return true;
+}
+
+// CheckPrefixes
+//
+// Reject cookies whose name starts with the magic prefixes from
+// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00
+// if they do not meet the criteria required by the prefix.
+//
+// Must not be called until after CheckDomain() and CheckPath() have
+// regularized and validated the nsCookieAttributes values!
+bool
+nsCookieService::CheckPrefixes(nsCookieAttributes &aCookieAttributes,
+ bool aSecureRequest)
+{
+ static const char kSecure[] = "__Secure-";
+ static const char kHost[] = "__Host-";
+ static const int kSecureLen = sizeof( kSecure ) - 1;
+ static const int kHostLen = sizeof( kHost ) - 1;
+
+ bool isSecure = strncmp( aCookieAttributes.name.get(), kSecure, kSecureLen ) == 0;
+ bool isHost = strncmp( aCookieAttributes.name.get(), kHost, kHostLen ) == 0;
+
+ if ( !isSecure && !isHost ) {
+ // not one of the magic prefixes: carry on
+ return true;
+ }
+
+ if ( !aSecureRequest || !aCookieAttributes.isSecure ) {
+ // the magic prefixes may only be used from a secure request and
+ // the secure attribute must be set on the cookie
+ return false;
+ }
+
+ if ( isHost ) {
+ // The host prefix requires that the path is "/" and that the cookie
+ // had no domain attribute. CheckDomain() and CheckPath() MUST be run
+ // first to make sure invalid attributes are rejected and to regularlize
+ // them. In particular all explicit domain attributes result in a host
+ // that starts with a dot, and if the host doesn't start with a dot it
+ // correctly matches the true host.
+ if ( aCookieAttributes.host[0] == '.' ||
+ !aCookieAttributes.path.EqualsLiteral( "/" )) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool
+nsCookieService::GetExpiry(nsCookieAttributes &aCookieAttributes,
+ int64_t aServerTime,
+ int64_t aCurrentTime)
+{
+ /* Determine when the cookie should expire. This is done by taking the difference between
+ * the server time and the time the server wants the cookie to expire, and adding that
+ * difference to the client time. This localizes the client time regardless of whether or
+ * not the TZ environment variable was set on the client.
+ *
+ * Note: We need to consider accounting for network lag here, per RFC.
+ */
+ // check for max-age attribute first; this overrides expires attribute
+ if (!aCookieAttributes.maxage.IsEmpty()) {
+ // obtain numeric value of maxageAttribute
+ int64_t maxage;
+ int32_t numInts = PR_sscanf(aCookieAttributes.maxage.get(), "%lld", &maxage);
+
+ // default to session cookie if the conversion failed
+ if (numInts != 1) {
+ return true;
+ }
+
+ // if this addition overflows, expiryTime will be less than currentTime
+ // and the cookie will be expired - that's okay.
+ aCookieAttributes.expiryTime = aCurrentTime + maxage;
+
+ // check for expires attribute
+ } else if (!aCookieAttributes.expires.IsEmpty()) {
+ PRTime expires;
+
+ // parse expiry time
+ if (PR_ParseTimeString(aCookieAttributes.expires.get(), true, &expires) != PR_SUCCESS) {
+ return true;
+ }
+
+ // If set-cookie used absolute time to set expiration, and it can't use
+ // client time to set expiration.
+ // Because if current time be set in the future, but the cookie expire
+ // time be set less than current time and more than server time.
+ // The cookie item have to be used to the expired cookie.
+ aCookieAttributes.expiryTime = expires / int64_t(PR_USEC_PER_SEC);
+
+ // default to session cookie if no attributes found
+ } else {
+ return true;
+ }
+
+ return false;
+}
+
+/******************************************************************************
+ * nsCookieService impl:
+ * private cookielist management functions
+ ******************************************************************************/
+
+void
+nsCookieService::RemoveAllFromMemory()
+{
+ // clearing the hashtable will call each nsCookieEntry's dtor,
+ // which releases all their respective children.
+ mDBState->hostTable.Clear();
+ mDBState->cookieCount = 0;
+ mDBState->cookieOldestTime = INT64_MAX;
+}
+
+// comparator class for lastaccessed times of cookies.
+class CompareCookiesByAge {
+public:
+ bool Equals(const nsListIter &a, const nsListIter &b) const
+ {
+ return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
+ a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
+ }
+
+ bool LessThan(const nsListIter &a, const nsListIter &b) const
+ {
+ // compare by lastAccessed time, and tiebreak by creationTime.
+ int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
+ if (result != 0)
+ return result < 0;
+
+ return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
+ }
+};
+
+// comparator class for sorting cookies by entry and index.
+class CompareCookiesByIndex {
+public:
+ bool Equals(const nsListIter &a, const nsListIter &b) const
+ {
+ NS_ASSERTION(a.entry != b.entry || a.index != b.index,
+ "cookie indexes should never be equal");
+ return false;
+ }
+
+ bool LessThan(const nsListIter &a, const nsListIter &b) const
+ {
+ // compare by entryclass pointer, then by index.
+ if (a.entry != b.entry)
+ return a.entry < b.entry;
+
+ return a.index < b.index;
+ }
+};
+
+// purges expired and old cookies in a batch operation.
+already_AddRefed<nsIArray>
+nsCookieService::PurgeCookies(int64_t aCurrentTimeInUsec)
+{
+ NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty");
+ EnsureReadComplete();
+
+ uint32_t initialCookieCount = mDBState->cookieCount;
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("PurgeCookies(): beginning purge with %ld cookies and %lld oldest age",
+ mDBState->cookieCount, aCurrentTimeInUsec - mDBState->cookieOldestTime));
+
+ typedef nsTArray<nsListIter> PurgeList;
+ PurgeList purgeList(kMaxNumberOfCookies);
+
+ nsCOMPtr<nsIMutableArray> removedList = do_CreateInstance(NS_ARRAY_CONTRACTID);
+
+ // Create a params array to batch the removals. This is OK here because
+ // all the removals are in order, and there are no interleaved additions.
+ mozIStorageAsyncStatement *stmt = mDBState->stmtDelete;
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ if (mDBState->dbConn) {
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
+ int64_t purgeTime = aCurrentTimeInUsec - mCookiePurgeAge;
+ int64_t oldestTime = INT64_MAX;
+
+ for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
+ nsCookieEntry* entry = iter.Get();
+
+ const nsCookieEntry::ArrayType& cookies = entry->GetCookies();
+ auto length = cookies.Length();
+ for (nsCookieEntry::IndexType i = 0; i < length; ) {
+ nsListIter iter(entry, i);
+ nsCookie* cookie = cookies[i];
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ removedList->AppendElement(cookie, false);
+ COOKIE_LOGEVICTED(cookie, "Cookie expired");
+
+ // remove from list; do not increment our iterator unless we're the last
+ // in the list already.
+ gCookieService->RemoveCookieFromList(iter, paramsArray);
+ if (i == --length) {
+ break;
+ }
+ } else {
+ // check if the cookie is over the age limit
+ if (cookie->LastAccessed() <= purgeTime) {
+ purgeList.AppendElement(iter);
+
+ } else if (cookie->LastAccessed() < oldestTime) {
+ // reset our indicator
+ oldestTime = cookie->LastAccessed();
+ }
+
+ ++i;
+ }
+ MOZ_ASSERT(length == cookies.Length());
+ }
+ }
+
+ uint32_t postExpiryCookieCount = mDBState->cookieCount;
+
+ // now we have a list of iterators for cookies over the age limit.
+ // sort them by age, and then we'll see how many to remove...
+ purgeList.Sort(CompareCookiesByAge());
+
+ // only remove old cookies until we reach the max cookie limit, no more.
+ uint32_t excess = mDBState->cookieCount > mMaxNumberOfCookies ?
+ mDBState->cookieCount - mMaxNumberOfCookies : 0;
+ if (purgeList.Length() > excess) {
+ // We're not purging everything in the list, so update our indicator.
+ oldestTime = purgeList[excess].Cookie()->LastAccessed();
+
+ purgeList.SetLength(excess);
+ }
+
+ // sort the list again, this time grouping cookies with a common entryclass
+ // together, and with ascending index. this allows us to iterate backwards
+ // over the list removing cookies, without having to adjust indexes as we go.
+ purgeList.Sort(CompareCookiesByIndex());
+ for (PurgeList::index_type i = purgeList.Length(); i--; ) {
+ nsCookie *cookie = purgeList[i].Cookie();
+ removedList->AppendElement(cookie, false);
+ COOKIE_LOGEVICTED(cookie, "Cookie too old");
+
+ RemoveCookieFromList(purgeList[i], paramsArray);
+ }
+
+ // Update the database if we have entries to purge.
+ if (paramsArray) {
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length) {
+ DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ }
+ }
+
+ // reset the oldest time indicator
+ mDBState->cookieOldestTime = oldestTime;
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("PurgeCookies(): %ld expired; %ld purged; %ld remain; %lld oldest age",
+ initialCookieCount - postExpiryCookieCount,
+ postExpiryCookieCount - mDBState->cookieCount,
+ mDBState->cookieCount,
+ aCurrentTimeInUsec - mDBState->cookieOldestTime));
+
+ return removedList.forget();
+}
+
+// find whether a given cookie has been previously set. this is provided by the
+// nsICookieManager2 interface.
+NS_IMETHODIMP
+nsCookieService::CookieExists(nsICookie2* aCookie,
+ JS::HandleValue aOriginAttributes,
+ JSContext* aCx,
+ uint8_t aArgc,
+ bool* aFoundCookie)
+{
+ NS_ENSURE_ARG_POINTER(aCookie);
+ NS_ENSURE_ARG_POINTER(aCx);
+ NS_ENSURE_ARG_POINTER(aFoundCookie);
+ MOZ_ASSERT(aArgc == 0 || aArgc == 1);
+
+ NeckoOriginAttributes attrs;
+ nsresult rv = InitializeOriginAttributes(&attrs,
+ aOriginAttributes,
+ aCx,
+ aArgc,
+ u"nsICookieManager2.cookieExists()",
+ u"2");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CookieExistsNative(aCookie, &attrs, aFoundCookie);
+}
+
+NS_IMETHODIMP_(nsresult)
+nsCookieService::CookieExistsNative(nsICookie2* aCookie,
+ NeckoOriginAttributes* aOriginAttributes,
+ bool* aFoundCookie)
+{
+ NS_ENSURE_ARG_POINTER(aCookie);
+ NS_ENSURE_ARG_POINTER(aOriginAttributes);
+ NS_ENSURE_ARG_POINTER(aFoundCookie);
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsAutoCString host, name, path;
+ nsresult rv = aCookie->GetHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aCookie->GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aCookie->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsListIter iter;
+ *aFoundCookie = FindCookie(nsCookieKey(baseDomain, *aOriginAttributes),
+ host, name, path, iter);
+ return NS_OK;
+}
+
+// For a given base domain, find either an expired cookie or the oldest cookie
+// by lastAccessed time.
+int64_t
+nsCookieService::FindStaleCookie(nsCookieEntry *aEntry,
+ int64_t aCurrentTime,
+ nsIURI* aSource,
+ mozilla::Maybe<bool> aIsSecure,
+ nsListIter &aIter)
+{
+ aIter.entry = nullptr;
+ bool requireHostMatch = true;
+ nsAutoCString baseDomain, sourceHost, sourcePath;
+ if (aSource) {
+ GetBaseDomain(aSource, baseDomain, requireHostMatch);
+ aSource->GetAsciiHost(sourceHost);
+ sourcePath = GetPathFromURI(aSource);
+ }
+
+ const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies();
+
+ int64_t oldestNonMatchingSessionCookieTime = 0;
+ nsListIter oldestNonMatchingSessionCookie;
+ oldestNonMatchingSessionCookie.entry = nullptr;
+
+ int64_t oldestSessionCookieTime = 0;
+ nsListIter oldestSessionCookie;
+ oldestSessionCookie.entry = nullptr;
+
+ int64_t oldestNonMatchingNonSessionCookieTime = 0;
+ nsListIter oldestNonMatchingNonSessionCookie;
+ oldestNonMatchingNonSessionCookie.entry = nullptr;
+
+ int64_t oldestCookieTime = 0;
+ nsListIter oldestCookie;
+ oldestCookie.entry = nullptr;
+
+ int64_t actualOldestCookieTime = cookies.Length() ? cookies[0]->LastAccessed() : 0;
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ nsCookie *cookie = cookies[i];
+
+ // If we found an expired cookie, we're done.
+ if (cookie->Expiry() <= aCurrentTime) {
+ aIter.entry = aEntry;
+ aIter.index = i;
+ return -1;
+ }
+
+ int64_t lastAccessed = cookie->LastAccessed();
+ // Record the age of the oldest cookie that is stored for this host.
+ // oldestCookieTime is the age of the oldest cookie with a matching
+ // secure flag, which may be more recent than an older cookie with
+ // a non-matching secure flag.
+ if (actualOldestCookieTime > lastAccessed) {
+ actualOldestCookieTime = lastAccessed;
+ }
+ if (aIsSecure.isSome() && !aIsSecure.value()) {
+ // We want to look for the oldest non-secure cookie first time through,
+ // then find the oldest secure cookie the second time we are called.
+ if (cookie->IsSecure()) {
+ continue;
+ }
+ }
+
+ // Update our various records of oldest cookies fitting several restrictions:
+ // * session cookies
+ // * non-session cookies
+ // * cookies with paths and domains that don't match the cookie triggering this purge
+
+ // This cookie is a candidate for eviction if we have no information about
+ // the source request, or if it is not a path or domain match against the
+ // source request.
+ bool isPrimaryEvictionCandidate = true;
+ if (aSource) {
+ isPrimaryEvictionCandidate = !PathMatches(cookie, sourcePath) || !DomainMatches(cookie, sourceHost);
+ }
+
+ if (cookie->IsSession()) {
+ if (!oldestSessionCookie.entry || oldestSessionCookieTime > lastAccessed) {
+ oldestSessionCookieTime = lastAccessed;
+ oldestSessionCookie.entry = aEntry;
+ oldestSessionCookie.index = i;
+ }
+
+ if (isPrimaryEvictionCandidate &&
+ (!oldestNonMatchingSessionCookie.entry ||
+ oldestNonMatchingSessionCookieTime > lastAccessed)) {
+ oldestNonMatchingSessionCookieTime = lastAccessed;
+ oldestNonMatchingSessionCookie.entry = aEntry;
+ oldestNonMatchingSessionCookie.index = i;
+ }
+ } else if (isPrimaryEvictionCandidate &&
+ (!oldestNonMatchingNonSessionCookie.entry ||
+ oldestNonMatchingNonSessionCookieTime > lastAccessed)) {
+ oldestNonMatchingNonSessionCookieTime = lastAccessed;
+ oldestNonMatchingNonSessionCookie.entry = aEntry;
+ oldestNonMatchingNonSessionCookie.index = i;
+ }
+
+ // Check if we've found the oldest cookie so far.
+ if (!oldestCookie.entry || oldestCookieTime > lastAccessed) {
+ oldestCookieTime = lastAccessed;
+ oldestCookie.entry = aEntry;
+ oldestCookie.index = i;
+ }
+ }
+
+ // Prefer to evict the oldest session cookies with a non-matching path/domain,
+ // followed by the oldest session cookie with a matching path/domain,
+ // followed by the oldest non-session cookie with a non-matching path/domain,
+ // resorting to the oldest non-session cookie with a matching path/domain.
+ if (oldestNonMatchingSessionCookie.entry) {
+ aIter = oldestNonMatchingSessionCookie;
+ } else if (oldestSessionCookie.entry) {
+ aIter = oldestSessionCookie;
+ } else if (oldestNonMatchingNonSessionCookie.entry) {
+ aIter = oldestNonMatchingNonSessionCookie;
+ } else {
+ aIter = oldestCookie;
+ }
+
+ return actualOldestCookieTime;
+}
+
+void
+nsCookieService::TelemetryForEvictingStaleCookie(nsCookie *aEvicted,
+ int64_t oldestCookieTime)
+{
+ // We need to record the evicting cookie to telemetry.
+ if (!aEvicted->IsSecure()) {
+ if (aEvicted->LastAccessed() > oldestCookieTime) {
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ EVICTED_NEWER_INSECURE);
+ } else {
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ EVICTED_OLDEST_COOKIE);
+ }
+ } else {
+ Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
+ EVICTED_PREFERRED_COOKIE);
+ }
+}
+
+// count the number of cookies stored by a particular host. this is provided by the
+// nsICookieManager2 interface.
+NS_IMETHODIMP
+nsCookieService::CountCookiesFromHost(const nsACString &aHost,
+ uint32_t *aCountFromHost)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
+ EnsureReadDomain(key);
+
+ // Return a count of all cookies, including expired.
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
+ *aCountFromHost = entry ? entry->GetCookies().Length() : 0;
+ return NS_OK;
+}
+
+// get an enumerator of cookies stored by a particular host. this is provided by the
+// nsICookieManager2 interface.
+NS_IMETHODIMP
+nsCookieService::GetCookiesFromHost(const nsACString &aHost,
+ JS::HandleValue aOriginAttributes,
+ JSContext* aCx,
+ uint8_t aArgc,
+ nsISimpleEnumerator **aEnumerator)
+{
+ MOZ_ASSERT(aArgc == 0 || aArgc == 1);
+
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NeckoOriginAttributes attrs;
+ rv = InitializeOriginAttributes(&attrs,
+ aOriginAttributes,
+ aCx,
+ aArgc,
+ u"nsICookieManager2.getCookiesFromHost()",
+ u"2");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCookieKey key = nsCookieKey(baseDomain, attrs);
+ EnsureReadDomain(key);
+
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
+ if (!entry)
+ return NS_NewEmptyEnumerator(aEnumerator);
+
+ nsCOMArray<nsICookie> cookieList(mMaxCookiesPerHost);
+ const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ cookieList.AppendObject(cookies[i]);
+ }
+
+ return NS_NewArrayEnumerator(aEnumerator, cookieList);
+}
+
+NS_IMETHODIMP
+nsCookieService::GetCookiesWithOriginAttributes(const nsAString& aPattern,
+ const nsACString& aHost,
+ nsISimpleEnumerator **aEnumerator)
+{
+ mozilla::OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetCookiesWithOriginAttributes(pattern, baseDomain, aEnumerator);
+}
+
+nsresult
+nsCookieService::GetCookiesWithOriginAttributes(
+ const mozilla::OriginAttributesPattern& aPattern,
+ const nsCString& aBaseDomain,
+ nsISimpleEnumerator **aEnumerator)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already closed?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aPattern.mAppId.WasPassed() && aPattern.mAppId.Value() == NECKO_UNKNOWN_APP_ID) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMArray<nsICookie> cookies;
+ for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
+ nsCookieEntry* entry = iter.Get();
+
+ if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(entry->mOriginAttributes)) {
+ continue;
+ }
+
+ const nsCookieEntry::ArrayType& entryCookies = entry->GetCookies();
+
+ for (nsCookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
+ cookies.AppendObject(entryCookies[i]);
+ }
+ }
+
+ return NS_NewArrayEnumerator(aEnumerator, cookies);
+}
+
+NS_IMETHODIMP
+nsCookieService::RemoveCookiesWithOriginAttributes(const nsAString& aPattern,
+ const nsACString& aHost)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ mozilla::OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomainFromHost(host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RemoveCookiesWithOriginAttributes(pattern, baseDomain);
+}
+
+nsresult
+nsCookieService::RemoveCookiesWithOriginAttributes(
+ const mozilla::OriginAttributesPattern& aPattern,
+ const nsCString& aBaseDomain)
+{
+ if (!mDBState) {
+ NS_WARNING("No DBState! Profile already close?");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Iterate the hash table of nsCookieEntry.
+ for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
+ nsCookieEntry* entry = iter.Get();
+
+ if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(entry->mOriginAttributes)) {
+ continue;
+ }
+
+ // Pattern matches. Delete all cookies within this nsCookieEntry.
+ uint32_t cookiesCount = entry->GetCookies().Length();
+
+ for (nsCookieEntry::IndexType i = 0 ; i < cookiesCount; ++i) {
+ // Remove the first cookie from the list.
+ nsListIter iter(entry, 0);
+ RefPtr<nsCookie> cookie = iter.Cookie();
+
+ // Remove the cookie.
+ RemoveCookieFromList(iter);
+
+ if (cookie) {
+ NotifyChanged(cookie, u"deleted");
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// find an secure cookie specified by host and name
+bool
+nsCookieService::FindSecureCookie(const nsCookieKey &aKey,
+ nsCookie *aCookie)
+{
+ EnsureReadDomain(aKey);
+
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
+ if (!entry)
+ return false;
+
+ const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ nsCookie *cookie = cookies[i];
+ // isn't a match if insecure or a different name
+ if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name()))
+ continue;
+
+ // The host must "domain-match" an existing cookie or vice-versa
+ if (DomainMatches(cookie, aCookie->Host()) ||
+ DomainMatches(aCookie, cookie->Host())) {
+ // If the path of new cookie and the path of existing cookie
+ // aren't "/", then this situation needs to compare paths to
+ // ensure only that a newly-created non-secure cookie does not
+ // overlay an existing secure cookie.
+ if (PathMatches(cookie, aCookie->Path())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// find an exact cookie specified by host, name, and path that hasn't expired.
+bool
+nsCookieService::FindCookie(const nsCookieKey &aKey,
+ const nsAFlatCString &aHost,
+ const nsAFlatCString &aName,
+ const nsAFlatCString &aPath,
+ nsListIter &aIter)
+{
+ EnsureReadDomain(aKey);
+
+ nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
+ if (!entry)
+ return false;
+
+ const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
+ for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ nsCookie *cookie = cookies[i];
+
+ if (aHost.Equals(cookie->Host()) &&
+ aPath.Equals(cookie->Path()) &&
+ aName.Equals(cookie->Name())) {
+ aIter = nsListIter(entry, i);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// remove a cookie from the hashtable, and update the iterator state.
+void
+nsCookieService::RemoveCookieFromList(const nsListIter &aIter,
+ mozIStorageBindingParamsArray *aParamsArray)
+{
+ // if it's a non-session cookie, remove it from the db
+ if (!aIter.Cookie()->IsSession() && mDBState->dbConn) {
+ // Use the asynchronous binding methods to ensure that we do not acquire
+ // the database lock.
+ mozIStorageAsyncStatement *stmt = mDBState->stmtDelete;
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
+ if (!paramsArray) {
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+
+ DebugOnly<nsresult> rv =
+ params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
+ aIter.Cookie()->Name());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
+ aIter.Cookie()->Host());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
+ aIter.Cookie()->Path());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = paramsArray->AddParams(params);
+ NS_ASSERT_SUCCESS(rv);
+
+ // If we weren't given a params array, we'll need to remove it ourselves.
+ if (!aParamsArray) {
+ rv = stmt->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ }
+ }
+
+ if (aIter.entry->GetCookies().Length() == 1) {
+ // we're removing the last element in the array - so just remove the entry
+ // from the hash. note that the entryclass' dtor will take care of
+ // releasing this last element for us!
+ mDBState->hostTable.RawRemoveEntry(aIter.entry);
+
+ } else {
+ // just remove the element from the list
+ aIter.entry->GetCookies().RemoveElementAt(aIter.index);
+ }
+
+ --mDBState->cookieCount;
+}
+
+void
+bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray,
+ const nsCookieKey &aKey,
+ const nsCookie *aCookie)
+{
+ NS_ASSERTION(aParamsArray, "Null params array passed to bindCookieParameters!");
+ NS_ASSERTION(aCookie, "Null cookie passed to bindCookieParameters!");
+
+ // Use the asynchronous binding methods to ensure that we do not acquire the
+ // database lock.
+ nsCOMPtr<mozIStorageBindingParams> params;
+ DebugOnly<nsresult> rv =
+ aParamsArray->NewBindingParams(getter_AddRefs(params));
+ NS_ASSERT_SUCCESS(rv);
+
+ // Bind our values to params
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
+ aKey.mBaseDomain);
+ NS_ASSERT_SUCCESS(rv);
+
+ nsAutoCString suffix;
+ aKey.mOriginAttributes.CreateSuffix(suffix);
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
+ suffix);
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
+ aCookie->Name());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("value"),
+ aCookie->Value());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
+ aCookie->Host());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
+ aCookie->Path());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindInt64ByName(NS_LITERAL_CSTRING("expiry"),
+ aCookie->Expiry());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"),
+ aCookie->LastAccessed());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindInt64ByName(NS_LITERAL_CSTRING("creationTime"),
+ aCookie->CreationTime());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isSecure"),
+ aCookie->IsSecure());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isHttpOnly"),
+ aCookie->IsHttpOnly());
+ NS_ASSERT_SUCCESS(rv);
+
+ // Bind the params to the array.
+ rv = aParamsArray->AddParams(params);
+ NS_ASSERT_SUCCESS(rv);
+}
+
+void
+nsCookieService::UpdateCookieOldestTime(DBState* aDBState,
+ nsCookie* aCookie)
+{
+ if (aCookie->LastAccessed() < aDBState->cookieOldestTime) {
+ aDBState->cookieOldestTime = aCookie->LastAccessed();
+ }
+}
+
+void
+nsCookieService::AddCookieToList(const nsCookieKey &aKey,
+ nsCookie *aCookie,
+ DBState *aDBState,
+ mozIStorageBindingParamsArray *aParamsArray,
+ bool aWriteToDB)
+{
+ NS_ASSERTION(!(aDBState->dbConn && !aWriteToDB && aParamsArray),
+ "Not writing to the DB but have a params array?");
+ NS_ASSERTION(!(!aDBState->dbConn && aParamsArray),
+ "Do not have a DB connection but have a params array?");
+
+ nsCookieEntry *entry = aDBState->hostTable.PutEntry(aKey);
+ NS_ASSERTION(entry, "can't insert element into a null entry!");
+
+ entry->GetCookies().AppendElement(aCookie);
+ ++aDBState->cookieCount;
+
+ // keep track of the oldest cookie, for when it comes time to purge
+ UpdateCookieOldestTime(aDBState, aCookie);
+
+ // if it's a non-session cookie and hasn't just been read from the db, write it out.
+ if (aWriteToDB && !aCookie->IsSession() && aDBState->dbConn) {
+ mozIStorageAsyncStatement *stmt = aDBState->stmtInsert;
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
+ if (!paramsArray) {
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+ bindCookieParameters(paramsArray, aKey, aCookie);
+
+ // If we were supplied an array to store parameters, we shouldn't call
+ // executeAsync - someone up the stack will do this for us.
+ if (!aParamsArray) {
+ DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
+ NS_ASSERT_SUCCESS(rv);
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mDBState->insertListener, getter_AddRefs(handle));
+ NS_ASSERT_SUCCESS(rv);
+ }
+ }
+}
+
+void
+nsCookieService::UpdateCookieInList(nsCookie *aCookie,
+ int64_t aLastAccessed,
+ mozIStorageBindingParamsArray *aParamsArray)
+{
+ NS_ASSERTION(aCookie, "Passing a null cookie to UpdateCookieInList!");
+
+ // udpate the lastAccessed timestamp
+ aCookie->SetLastAccessed(aLastAccessed);
+
+ // if it's a non-session cookie, update it in the db too
+ if (!aCookie->IsSession() && aParamsArray) {
+ // Create our params holder.
+ nsCOMPtr<mozIStorageBindingParams> params;
+ aParamsArray->NewBindingParams(getter_AddRefs(params));
+
+ // Bind our parameters.
+ DebugOnly<nsresult> rv =
+ params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"),
+ aLastAccessed);
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
+ aCookie->Name());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
+ aCookie->Host());
+ NS_ASSERT_SUCCESS(rv);
+
+ rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
+ aCookie->Path());
+ NS_ASSERT_SUCCESS(rv);
+
+ // Add our bound parameters to the array.
+ rv = aParamsArray->AddParams(params);
+ NS_ASSERT_SUCCESS(rv);
+ }
+}
+
+size_t
+nsCookieService::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+{
+ size_t n = aMallocSizeOf(this);
+
+ if (mDefaultDBState) {
+ n += mDefaultDBState->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mPrivateDBState) {
+ n += mPrivateDBState->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf)
+
+NS_IMETHODIMP
+nsCookieService::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize)
+{
+ MOZ_COLLECT_REPORT(
+ "explicit/cookie-service", KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(CookieServiceMallocSizeOf),
+ "Memory used by the cookie service.");
+
+ return NS_OK;
+}
diff --git a/netwerk/cookie/nsCookieService.h b/netwerk/cookie/nsCookieService.h
new file mode 100644
index 000000000..e3b2d3e8a
--- /dev/null
+++ b/netwerk/cookie/nsCookieService.h
@@ -0,0 +1,371 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCookieService_h__
+#define nsCookieService_h__
+
+#include "nsICookieService.h"
+#include "nsICookieManager.h"
+#include "nsICookieManager2.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+
+#include "nsCookie.h"
+#include "nsString.h"
+#include "nsAutoPtr.h"
+#include "nsHashKeys.h"
+#include "nsIMemoryReporter.h"
+#include "nsTHashtable.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageAsyncStatement.h"
+#include "mozIStoragePendingStatement.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageRow.h"
+#include "mozIStorageCompletionCallback.h"
+#include "mozIStorageStatementCallback.h"
+#include "mozIStorageFunction.h"
+#include "nsIVariant.h"
+#include "nsIFile.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Maybe.h"
+
+using mozilla::NeckoOriginAttributes;
+using mozilla::OriginAttributes;
+
+class nsICookiePermission;
+class nsIEffectiveTLDService;
+class nsIIDNService;
+class nsIPrefBranch;
+class nsIObserverService;
+class nsIURI;
+class nsIChannel;
+class nsIArray;
+class mozIStorageService;
+class mozIThirdPartyUtil;
+class ReadCookieDBListener;
+
+struct nsCookieAttributes;
+struct nsListIter;
+
+namespace mozilla {
+namespace net {
+class CookieServiceParent;
+} // namespace net
+} // namespace mozilla
+
+// hash key class
+class nsCookieKey : public PLDHashEntryHdr
+{
+public:
+ typedef const nsCookieKey& KeyType;
+ typedef const nsCookieKey* KeyTypePointer;
+
+ nsCookieKey()
+ {}
+
+ nsCookieKey(const nsCString &baseDomain, const NeckoOriginAttributes &attrs)
+ : mBaseDomain(baseDomain)
+ , mOriginAttributes(attrs)
+ {}
+
+ explicit nsCookieKey(KeyTypePointer other)
+ : mBaseDomain(other->mBaseDomain)
+ , mOriginAttributes(other->mOriginAttributes)
+ {}
+
+ nsCookieKey(KeyType other)
+ : mBaseDomain(other.mBaseDomain)
+ , mOriginAttributes(other.mOriginAttributes)
+ {}
+
+ ~nsCookieKey()
+ {}
+
+ bool KeyEquals(KeyTypePointer other) const
+ {
+ return mBaseDomain == other->mBaseDomain &&
+ mOriginAttributes == other->mOriginAttributes;
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey)
+ {
+ return &aKey;
+ }
+
+ static PLDHashNumber HashKey(KeyTypePointer aKey)
+ {
+ // TODO: more efficient way to generate hash?
+ nsAutoCString temp(aKey->mBaseDomain);
+ temp.Append('#');
+ nsAutoCString suffix;
+ aKey->mOriginAttributes.CreateSuffix(suffix);
+ temp.Append(suffix);
+ return mozilla::HashString(temp);
+ }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ enum { ALLOW_MEMMOVE = true };
+
+ nsCString mBaseDomain;
+ NeckoOriginAttributes mOriginAttributes;
+};
+
+// Inherit from nsCookieKey so this can be stored in nsTHashTable
+// TODO: why aren't we using nsClassHashTable<nsCookieKey, ArrayType>?
+class nsCookieEntry : public nsCookieKey
+{
+ public:
+ // Hash methods
+ typedef nsTArray< RefPtr<nsCookie> > ArrayType;
+ typedef ArrayType::index_type IndexType;
+
+ explicit nsCookieEntry(KeyTypePointer aKey)
+ : nsCookieKey(aKey)
+ {}
+
+ nsCookieEntry(const nsCookieEntry& toCopy)
+ {
+ // if we end up here, things will break. nsTHashtable shouldn't
+ // allow this, since we set ALLOW_MEMMOVE to true.
+ NS_NOTREACHED("nsCookieEntry copy constructor is forbidden!");
+ }
+
+ ~nsCookieEntry()
+ {}
+
+ inline ArrayType& GetCookies() { return mCookies; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ ArrayType mCookies;
+};
+
+// encapsulates a (key, nsCookie) tuple for temporary storage purposes.
+struct CookieDomainTuple
+{
+ nsCookieKey key;
+ RefPtr<nsCookie> cookie;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+};
+
+// encapsulates in-memory and on-disk DB states, so we can
+// conveniently switch state when entering or exiting private browsing.
+struct DBState final
+{
+ DBState() : cookieCount(0), cookieOldestTime(INT64_MAX), corruptFlag(OK)
+ {
+ }
+
+private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~DBState()
+ {
+ }
+
+public:
+ NS_INLINE_DECL_REFCOUNTING(DBState)
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ // State of the database connection.
+ enum CorruptFlag {
+ OK, // normal
+ CLOSING_FOR_REBUILD, // corruption detected, connection closing
+ REBUILDING // close complete, rebuilding database from memory
+ };
+
+ nsTHashtable<nsCookieEntry> hostTable;
+ uint32_t cookieCount;
+ int64_t cookieOldestTime;
+ nsCOMPtr<nsIFile> cookieFile;
+ nsCOMPtr<mozIStorageConnection> dbConn;
+ nsCOMPtr<mozIStorageAsyncStatement> stmtInsert;
+ nsCOMPtr<mozIStorageAsyncStatement> stmtDelete;
+ nsCOMPtr<mozIStorageAsyncStatement> stmtUpdate;
+ CorruptFlag corruptFlag;
+
+ // Various parts representing asynchronous read state. These are useful
+ // while the background read is taking place.
+ nsCOMPtr<mozIStorageConnection> syncConn;
+ nsCOMPtr<mozIStorageStatement> stmtReadDomain;
+ nsCOMPtr<mozIStoragePendingStatement> pendingRead;
+ // The asynchronous read listener. This is a weak ref (storage has ownership)
+ // since it may need to outlive the DBState's database connection.
+ ReadCookieDBListener* readListener;
+ // An array of (baseDomain, cookie) tuples representing data read in
+ // asynchronously. This is merged into hostTable once read is complete.
+ nsTArray<CookieDomainTuple> hostArray;
+ // A hashset of baseDomains read in synchronously, while the async read is
+ // in flight. This is used to keep track of which data in hostArray is stale
+ // when the time comes to merge.
+ nsTHashtable<nsCookieKey> readSet;
+
+ // DB completion handlers.
+ nsCOMPtr<mozIStorageStatementCallback> insertListener;
+ nsCOMPtr<mozIStorageStatementCallback> updateListener;
+ nsCOMPtr<mozIStorageStatementCallback> removeListener;
+ nsCOMPtr<mozIStorageCompletionCallback> closeListener;
+};
+
+// these constants represent a decision about a cookie based on user prefs.
+enum CookieStatus
+{
+ STATUS_ACCEPTED,
+ STATUS_ACCEPT_SESSION,
+ STATUS_REJECTED,
+ // STATUS_REJECTED_WITH_ERROR indicates the cookie should be rejected because
+ // of an error (rather than something the user can control). this is used for
+ // notification purposes, since we only want to notify of rejections where
+ // the user can do something about it (e.g. whitelist the site).
+ STATUS_REJECTED_WITH_ERROR
+};
+
+// Result codes for TryInitDB() and Read().
+enum OpenDBResult
+{
+ RESULT_OK,
+ RESULT_RETRY,
+ RESULT_FAILURE
+};
+
+/******************************************************************************
+ * nsCookieService:
+ * class declaration
+ ******************************************************************************/
+
+class nsCookieService final : public nsICookieService
+ , public nsICookieManager2
+ , public nsIObserver
+ , public nsSupportsWeakReference
+ , public nsIMemoryReporter
+{
+ private:
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSICOOKIESERVICE
+ NS_DECL_NSICOOKIEMANAGER
+ NS_DECL_NSICOOKIEMANAGER2
+ NS_DECL_NSIMEMORYREPORTER
+
+ nsCookieService();
+ static nsICookieService* GetXPCOMSingleton();
+ nsresult Init();
+
+ /**
+ * Start watching the observer service for messages indicating that an app has
+ * been uninstalled. When an app is uninstalled, we get the cookie service
+ * (thus instantiating it, if necessary) and clear all the cookies for that
+ * app.
+ */
+ static void AppClearDataObserverInit();
+
+ protected:
+ virtual ~nsCookieService();
+
+ void PrefChanged(nsIPrefBranch *aPrefBranch);
+ void InitDBStates();
+ OpenDBResult TryInitDB(bool aDeleteExistingDB);
+ nsresult CreateTable();
+ nsresult CreateTableForSchemaVersion6();
+ nsresult CreateTableForSchemaVersion5();
+ void CloseDBStates();
+ void CleanupCachedStatements();
+ void CleanupDefaultDBConnection();
+ void HandleDBClosed(DBState* aDBState);
+ void HandleCorruptDB(DBState* aDBState);
+ void RebuildCorruptDB(DBState* aDBState);
+ OpenDBResult Read();
+ template<class T> nsCookie* GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes);
+ void AsyncReadComplete();
+ void CancelAsyncRead(bool aPurgeReadSet);
+ void EnsureReadDomain(const nsCookieKey &aKey);
+ void EnsureReadComplete();
+ nsresult NormalizeHost(nsCString &aHost);
+ nsresult GetBaseDomain(nsIURI *aHostURI, nsCString &aBaseDomain, bool &aRequireHostMatch);
+ nsresult GetBaseDomainFromHost(const nsACString &aHost, nsCString &aBaseDomain);
+ nsresult GetCookieStringCommon(nsIURI *aHostURI, nsIChannel *aChannel, bool aHttpBound, char** aCookie);
+ void GetCookieStringInternal(nsIURI *aHostURI, bool aIsForeign, bool aHttpBound, const NeckoOriginAttributes aOriginAttrs, bool aIsPrivate, nsCString &aCookie);
+ nsresult SetCookieStringCommon(nsIURI *aHostURI, const char *aCookieHeader, const char *aServerTime, nsIChannel *aChannel, bool aFromHttp);
+ void SetCookieStringInternal(nsIURI *aHostURI, bool aIsForeign, nsDependentCString &aCookieHeader, const nsCString &aServerTime, bool aFromHttp, const NeckoOriginAttributes &aOriginAttrs, bool aIsPrivate, nsIChannel* aChannel);
+ bool SetCookieInternal(nsIURI *aHostURI, const nsCookieKey& aKey, bool aRequireHostMatch, CookieStatus aStatus, nsDependentCString &aCookieHeader, int64_t aServerTime, bool aFromHttp, nsIChannel* aChannel);
+ void AddInternal(const nsCookieKey& aKey, nsCookie *aCookie, int64_t aCurrentTimeInUsec, nsIURI *aHostURI, const char *aCookieHeader, bool aFromHttp);
+ void RemoveCookieFromList(const nsListIter &aIter, mozIStorageBindingParamsArray *aParamsArray = nullptr);
+ void AddCookieToList(const nsCookieKey& aKey, nsCookie *aCookie, DBState *aDBState, mozIStorageBindingParamsArray *aParamsArray, bool aWriteToDB = true);
+ void UpdateCookieInList(nsCookie *aCookie, int64_t aLastAccessed, mozIStorageBindingParamsArray *aParamsArray);
+ static bool GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, nsASingleFragmentCString::const_char_iterator &aEndIter, nsDependentCSubstring &aTokenString, nsDependentCSubstring &aTokenValue, bool &aEqualsFound);
+ static bool ParseAttributes(nsDependentCString &aCookieHeader, nsCookieAttributes &aCookie);
+ bool RequireThirdPartyCheck();
+ CookieStatus CheckPrefs(nsIURI *aHostURI, bool aIsForeign, const char *aCookieHeader);
+ bool CheckDomain(nsCookieAttributes &aCookie, nsIURI *aHostURI, const nsCString &aBaseDomain, bool aRequireHostMatch);
+ static bool CheckPath(nsCookieAttributes &aCookie, nsIURI *aHostURI);
+ static bool CheckPrefixes(nsCookieAttributes &aCookie, bool aSecureRequest);
+ static bool GetExpiry(nsCookieAttributes &aCookie, int64_t aServerTime, int64_t aCurrentTime);
+ void RemoveAllFromMemory();
+ already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec);
+ bool FindCookie(const nsCookieKey& aKey, const nsAFlatCString &aHost, const nsAFlatCString &aName, const nsAFlatCString &aPath, nsListIter &aIter);
+ bool FindSecureCookie(const nsCookieKey& aKey, nsCookie* aCookie);
+ int64_t FindStaleCookie(nsCookieEntry *aEntry, int64_t aCurrentTime, nsIURI* aSource, mozilla::Maybe<bool> aIsSecure, nsListIter &aIter);
+ void TelemetryForEvictingStaleCookie(nsCookie* aEvicted, int64_t oldestCookieTime);
+ void NotifyRejected(nsIURI *aHostURI);
+ void NotifyThirdParty(nsIURI *aHostURI, bool aAccepted, nsIChannel *aChannel);
+ void NotifyChanged(nsISupports *aSubject, const char16_t *aData);
+ void NotifyPurged(nsICookie2* aCookie);
+ already_AddRefed<nsIArray> CreatePurgeList(nsICookie2* aCookie);
+ void UpdateCookieOldestTime(DBState* aDBState, nsCookie* aCookie);
+
+ nsresult GetCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern& aPattern, const nsCString& aBaseDomain, nsISimpleEnumerator **aEnumerator);
+ nsresult RemoveCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern& aPattern, const nsCString& aBaseDomain);
+
+ /**
+ * This method is a helper that allows calling nsICookieManager::Remove()
+ * with NeckoOriginAttributes parameter.
+ * NOTE: this could be added to a public interface if we happen to need it.
+ */
+ nsresult Remove(const nsACString& aHost, const NeckoOriginAttributes& aAttrs,
+ const nsACString& aName, const nsACString& aPath,
+ bool aBlocked);
+
+ protected:
+ // cached members.
+ nsCOMPtr<nsICookiePermission> mPermissionService;
+ nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil;
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+ nsCOMPtr<nsIIDNService> mIDNService;
+ nsCOMPtr<mozIStorageService> mStorageService;
+
+ // we have two separate DB states: one for normal browsing and one for
+ // private browsing, switching between them on a per-cookie-request basis.
+ // this state encapsulates both the in-memory table and the on-disk DB.
+ // note that the private states' dbConn should always be null - we never
+ // want to be dealing with the on-disk DB when in private browsing.
+ DBState *mDBState;
+ RefPtr<DBState> mDefaultDBState;
+ RefPtr<DBState> mPrivateDBState;
+
+ // cached prefs
+ uint8_t mCookieBehavior; // BEHAVIOR_{ACCEPT, REJECTFOREIGN, REJECT, LIMITFOREIGN}
+ bool mThirdPartySession;
+ bool mLeaveSecureAlone;
+ uint16_t mMaxNumberOfCookies;
+ uint16_t mMaxCookiesPerHost;
+ int64_t mCookiePurgeAge;
+
+ // friends!
+ friend class DBListenerErrorHandler;
+ friend class ReadCookieDBListener;
+ friend class CloseCookieDBListener;
+
+ static nsCookieService* GetSingleton();
+ friend class mozilla::net::CookieServiceParent;
+};
+
+#endif // nsCookieService_h__
diff --git a/netwerk/cookie/nsICookie.idl b/netwerk/cookie/nsICookie.idl
new file mode 100644
index 000000000..e8dadb949
--- /dev/null
+++ b/netwerk/cookie/nsICookie.idl
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * An optional interface for accessing the HTTP or
+ * javascript cookie object
+ */
+
+typedef long nsCookieStatus;
+typedef long nsCookiePolicy;
+
+[scriptable, uuid(adf0db5e-211e-45a3-be14-4486ac430a58)]
+interface nsICookie : nsISupports {
+
+ /**
+ * the name of the cookie
+ */
+ readonly attribute ACString name;
+
+ /**
+ * the cookie value
+ */
+ readonly attribute AUTF8String value;
+
+ /**
+ * true if the cookie is a domain cookie, false otherwise
+ */
+ readonly attribute boolean isDomain;
+
+ /**
+ * the host (possibly fully qualified) of the cookie
+ */
+ readonly attribute AUTF8String host;
+
+ /**
+ * the path pertaining to the cookie
+ */
+ readonly attribute AUTF8String path;
+
+ /**
+ * true if the cookie was transmitted over ssl, false otherwise
+ */
+ readonly attribute boolean isSecure;
+
+ /**
+ * @DEPRECATED use nsICookie2.expiry and nsICookie2.isSession instead.
+ *
+ * expiration time in seconds since midnight (00:00:00), January 1, 1970 UTC.
+ * expires = 0 represents a session cookie.
+ * expires = 1 represents an expiration time earlier than Jan 1, 1970.
+ */
+ readonly attribute uint64_t expires;
+
+ /**
+ * @DEPRECATED status implementation will return STATUS_UNKNOWN in all cases.
+ */
+ const nsCookieStatus STATUS_UNKNOWN=0;
+ const nsCookieStatus STATUS_ACCEPTED=1;
+ const nsCookieStatus STATUS_DOWNGRADED=2;
+ const nsCookieStatus STATUS_FLAGGED=3;
+ const nsCookieStatus STATUS_REJECTED=4;
+ readonly attribute nsCookieStatus status;
+
+ /**
+ * @DEPRECATED policy implementation will return POLICY_UNKNOWN in all cases.
+ */
+ const nsCookiePolicy POLICY_UNKNOWN=0;
+ const nsCookiePolicy POLICY_NONE=1;
+ const nsCookiePolicy POLICY_NO_CONSENT=2;
+ const nsCookiePolicy POLICY_IMPLICIT_CONSENT=3;
+ const nsCookiePolicy POLICY_EXPLICIT_CONSENT=4;
+ const nsCookiePolicy POLICY_NO_II=5;
+ readonly attribute nsCookiePolicy policy;
+
+ /**
+ * The origin attributes for this cookie
+ */
+ [implicit_jscontext]
+ readonly attribute jsval originAttributes;
+};
diff --git a/netwerk/cookie/nsICookie2.idl b/netwerk/cookie/nsICookie2.idl
new file mode 100644
index 000000000..62a97f5bb
--- /dev/null
+++ b/netwerk/cookie/nsICookie2.idl
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsICookie.idl"
+
+/**
+ * Main cookie object interface for use by consumers:
+ * extends nsICookie, a frozen interface for external
+ * access of cookie objects
+ */
+
+[scriptable, uuid(05c420e5-03d0-4c7b-a605-df7ebe5ca326)]
+
+interface nsICookie2 : nsICookie
+{
+
+ /**
+ * the host (possibly fully qualified) of the cookie,
+ * without a leading dot to represent if it is a
+ * domain cookie.
+ */
+ readonly attribute AUTF8String rawHost;
+
+ /**
+ * true if the cookie is a session cookie.
+ * note that expiry time will also be honored
+ * for session cookies (see below); thus, whichever is
+ * the more restrictive of the two will take effect.
+ */
+ readonly attribute boolean isSession;
+
+ /**
+ * the actual expiry time of the cookie, in seconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ *
+ * this is distinct from nsICookie::expires, which
+ * has different and obsolete semantics.
+ */
+ readonly attribute int64_t expiry;
+
+ /**
+ * true if the cookie is an http only cookie
+ */
+ readonly attribute boolean isHttpOnly;
+
+ /**
+ * the creation time of the cookie, in microseconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ */
+ readonly attribute int64_t creationTime;
+
+ /**
+ * the last time the cookie was accessed (i.e. created,
+ * modified, or read by the server), in microseconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ *
+ * note that this time may be approximate.
+ */
+ readonly attribute int64_t lastAccessed;
+
+};
diff --git a/netwerk/cookie/nsICookieManager.idl b/netwerk/cookie/nsICookieManager.idl
new file mode 100644
index 000000000..daea98a4e
--- /dev/null
+++ b/netwerk/cookie/nsICookieManager.idl
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+%{ C++
+namespace mozilla {
+class NeckoOriginAttributes;
+} // mozilla namespace
+%}
+
+[ptr] native NeckoOriginAttributesPtr(mozilla::NeckoOriginAttributes);
+
+interface nsISimpleEnumerator;
+
+[scriptable, function, uuid(20709db8-8dad-4e45-b33e-6e7c761dfc5d)]
+interface nsIPrivateModeCallback : nsISupports
+{
+ void callback();
+};
+
+/**
+ * An optional interface for accessing or removing the cookies
+ * that are in the cookie list
+ */
+
+[scriptable, uuid(AAAB6710-0F2C-11d5-A53B-0010A401EB10)]
+interface nsICookieManager : nsISupports
+{
+
+ /**
+ * Called to remove all cookies from the cookie list
+ */
+ void removeAll();
+
+ /**
+ * Called to enumerate through each cookie in the cookie list.
+ * The objects enumerated over are of type nsICookie
+ */
+ readonly attribute nsISimpleEnumerator enumerator;
+
+ /**
+ * Called to remove an individual cookie from the cookie list, specified
+ * by host, name, and path. If the cookie cannot be found, no exception
+ * is thrown. Typically, the arguments to this method will be obtained
+ * directly from the desired nsICookie object.
+ *
+ * @param aHost The host or domain for which the cookie was set. @see
+ * nsICookieManager2::add for a description of acceptable host
+ * strings. If the target cookie is a domain cookie, a leading
+ * dot must be present.
+ * @param aName The name specified in the cookie
+ * @param aPath The path for which the cookie was set
+ * @param aOriginAttributes The originAttributes of this cookie. This
+ * attribute is optional to avoid breaking add-ons.
+ * In 1 or 2 releases it will be mandatory: bug 1260399.
+ * @param aBlocked Indicates if cookies from this host should be permanently
+ * blocked.
+ *
+ */
+ [implicit_jscontext, optional_argc]
+ void remove(in AUTF8String aHost,
+ in ACString aName,
+ in AUTF8String aPath,
+ in boolean aBlocked,
+ [optional] in jsval aOriginAttributes);
+
+ [notxpcom]
+ nsresult removeNative(in AUTF8String aHost,
+ in ACString aName,
+ in AUTF8String aPath,
+ in boolean aBlocked,
+ in NeckoOriginAttributesPtr aOriginAttributes);
+
+ /**
+ * Set the cookie manager to work on private or non-private cookies for the
+ * duration of the callback.
+ *
+ * @param aIsPrivate True to work on private cookies, false to work on
+ * non-private cookies.
+ * @param aCallback Methods on the cookie manager interface will work on
+ * private or non-private cookies for the duration of this
+ * callback.
+ */
+ void usePrivateMode(in boolean aIsPrivate, in nsIPrivateModeCallback aCallback);
+};
diff --git a/netwerk/cookie/nsICookieManager2.idl b/netwerk/cookie/nsICookieManager2.idl
new file mode 100644
index 000000000..f20618bfa
--- /dev/null
+++ b/netwerk/cookie/nsICookieManager2.idl
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsICookieManager.idl"
+
+interface nsICookie2;
+interface nsIFile;
+
+/**
+ * Additions to the frozen nsICookieManager
+ */
+
+[scriptable, uuid(daf0caa7-b431-4b4d-ba51-08c179bb9dfe)]
+interface nsICookieManager2 : nsICookieManager
+{
+ /**
+ * Add a cookie. nsICookieService is the normal way to do this. This
+ * method is something of a backdoor.
+ *
+ * @param aHost
+ * the host or domain for which the cookie is set. presence of a
+ * leading dot indicates a domain cookie; otherwise, the cookie
+ * is treated as a non-domain cookie (see RFC2109). The host string
+ * will be normalized to ASCII or ACE; any trailing dot will be
+ * stripped. To be a domain cookie, the host must have at least two
+ * subdomain parts (e.g. '.foo.com', not '.com'), otherwise an
+ * exception will be thrown. An empty string is acceptable
+ * (e.g. file:// URI's).
+ * @param aPath
+ * path within the domain for which the cookie is valid
+ * @param aName
+ * cookie name
+ * @param aValue
+ * cookie data
+ * @param aIsSecure
+ * true if the cookie should only be sent over a secure connection.
+ * @param aIsHttpOnly
+ * true if the cookie should only be sent to, and can only be
+ * modified by, an http connection.
+ * @param aIsSession
+ * true if the cookie should exist for the current session only.
+ * see aExpiry.
+ * @param aExpiry
+ * expiration date, in seconds since midnight (00:00:00), January 1,
+ * 1970 UTC. note that expiry time will also be honored for session cookies;
+ * in this way, the more restrictive of the two will take effect.
+ * @param aOriginAttributes The originAttributes of this cookie. This
+ * attribute is optional to avoid breaking add-ons.
+ */
+ [implicit_jscontext, optional_argc]
+ void add(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in ACString aValue,
+ in boolean aIsSecure,
+ in boolean aIsHttpOnly,
+ in boolean aIsSession,
+ in int64_t aExpiry,
+ [optional] in jsval aOriginAttributes);
+
+ [notxpcom]
+ nsresult addNative(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in ACString aValue,
+ in boolean aIsSecure,
+ in boolean aIsHttpOnly,
+ in boolean aIsSession,
+ in int64_t aExpiry,
+ in NeckoOriginAttributesPtr aOriginAttributes);
+
+ /**
+ * Find whether a given cookie already exists.
+ *
+ * @param aCookie
+ * the cookie to look for
+ * @param aOriginAttributes
+ * nsICookie2 contains an originAttributes but if nsICookie2 is
+ * implemented in JS, we can't retrieve its originAttributes because
+ * the getter is marked [implicit_jscontext]. This optional parameter
+ * is a workaround.
+ *
+ * @return true if a cookie was found which matches the host, path, and name
+ * fields of aCookie
+ */
+ [implicit_jscontext, optional_argc]
+ boolean cookieExists(in nsICookie2 aCookie,
+ [optional] in jsval aOriginAttributes);
+
+ [notxpcom]
+ nsresult cookieExistsNative(in nsICookie2 aCookie,
+ in NeckoOriginAttributesPtr aOriginAttributes,
+ out boolean aExists);
+
+ /**
+ * Count how many cookies exist within the base domain of 'aHost'.
+ * Thus, for a host "weather.yahoo.com", the base domain would be "yahoo.com",
+ * and any host or domain cookies for "yahoo.com" and its subdomains would be
+ * counted.
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings.
+ *
+ * @return the number of cookies found.
+ */
+ unsigned long countCookiesFromHost(in AUTF8String aHost);
+
+ /**
+ * Returns an enumerator of cookies that exist within the base domain of
+ * 'aHost'. Thus, for a host "weather.yahoo.com", the base domain would be
+ * "yahoo.com", and any host or domain cookies for "yahoo.com" and its
+ * subdomains would be returned.
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings.
+ * @param aOriginAttributes The originAttributes of cookies that would be
+ * retrived. This attribute is optional to avoid
+ * breaking add-ons.
+ *
+ * @return an nsISimpleEnumerator of nsICookie2 objects.
+ *
+ * @see countCookiesFromHost
+ */
+ [implicit_jscontext, optional_argc]
+ nsISimpleEnumerator getCookiesFromHost(in AUTF8String aHost,
+ [optional] in jsval aOriginAttributes);
+
+ /**
+ * Import an old-style cookie file. Imported cookies will be added to the
+ * existing database. If the database contains any cookies the same as those
+ * being imported (i.e. domain, name, and path match), they will be replaced.
+ *
+ * @param aCookieFile the file to import, usually cookies.txt
+ */
+ void importCookies(in nsIFile aCookieFile);
+
+ /**
+ * Returns an enumerator of all cookies whose origin attributes matches aPattern
+ *
+ * @param aPattern origin attribute pattern in JSON format
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings. This attribute is optional. It will search
+ * all hosts if this attribute is not given.
+ */
+ nsISimpleEnumerator getCookiesWithOriginAttributes(in DOMString aPattern,
+ [optional] in AUTF8String aHost);
+
+ /**
+ * Remove all the cookies whose origin attributes matches aPattern
+ *
+ * @param aPattern origin attribute pattern in JSON format
+ */
+ void removeCookiesWithOriginAttributes(in DOMString aPattern,
+ [optional] in AUTF8String aHost);
+};
diff --git a/netwerk/cookie/nsICookiePermission.idl b/netwerk/cookie/nsICookiePermission.idl
new file mode 100644
index 000000000..fd4a879f9
--- /dev/null
+++ b/netwerk/cookie/nsICookiePermission.idl
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsICookie2;
+interface nsIURI;
+interface nsIChannel;
+
+typedef long nsCookieAccess;
+
+/**
+ * An interface to test for cookie permissions
+ */
+[scriptable, uuid(11ddd4ed-8f5b-40b3-b2a0-27c20ea1c88d)]
+interface nsICookiePermission : nsISupports
+{
+ /**
+ * nsCookieAccess values
+ */
+ const nsCookieAccess ACCESS_DEFAULT = 0;
+ const nsCookieAccess ACCESS_ALLOW = 1;
+ const nsCookieAccess ACCESS_DENY = 2;
+
+ /**
+ * additional values for nsCookieAccess which may not match
+ * nsIPermissionManager. Keep 3-7 available to allow nsIPermissionManager to
+ * add values without colliding. ACCESS_SESSION is not directly returned by
+ * any methods on this interface.
+ */
+ const nsCookieAccess ACCESS_SESSION = 8;
+ const nsCookieAccess ACCESS_ALLOW_FIRST_PARTY_ONLY = 9;
+ const nsCookieAccess ACCESS_LIMIT_THIRD_PARTY = 10;
+
+ /**
+ * setAccess
+ *
+ * this method is called to block cookie access for the given URI. this
+ * may result in other URIs being blocked as well (e.g., URIs which share
+ * the same host name).
+ *
+ * @param aURI
+ * the URI to block
+ * @param aAccess
+ * the new cookie access for the URI.
+ */
+ void setAccess(in nsIURI aURI,
+ in nsCookieAccess aAccess);
+
+ /**
+ * canAccess
+ *
+ * this method is called to test whether or not the given URI/channel may
+ * access the cookie database, either to set or get cookies.
+ *
+ * @param aURI
+ * the URI trying to access cookies
+ * @param aChannel
+ * the channel corresponding to aURI
+ *
+ * @return one of the following nsCookieAccess values:
+ * ACCESS_DEFAULT, ACCESS_ALLOW, ACCESS_DENY, or
+ * ACCESS_ALLOW_FIRST_PARTY_ONLY
+ */
+ nsCookieAccess canAccess(in nsIURI aURI,
+ in nsIChannel aChannel);
+
+ /**
+ * canSetCookie
+ *
+ * this method is called to test whether or not the given URI/channel may
+ * set a specific cookie. this method is always preceded by a call to
+ * canAccess. it may modify the isSession and expiry attributes of the
+ * cookie via the aIsSession and aExpiry parameters, in order to limit
+ * or extend the lifetime of the cookie. this is useful, for instance, to
+ * downgrade a cookie to session-only if it fails to meet certain criteria.
+ *
+ * @param aURI
+ * the URI trying to set the cookie
+ * @param aChannel
+ * the channel corresponding to aURI
+ * @param aCookie
+ * the cookie being added to the cookie database
+ * @param aIsSession
+ * when canSetCookie is invoked, this is the current isSession attribute
+ * of the cookie. canSetCookie may leave this value unchanged to
+ * preserve this attribute of the cookie.
+ * @param aExpiry
+ * when canSetCookie is invoked, this is the current expiry time of
+ * the cookie. canSetCookie may leave this value unchanged to
+ * preserve this attribute of the cookie.
+ *
+ * @return true if the cookie can be set.
+ */
+ boolean canSetCookie(in nsIURI aURI,
+ in nsIChannel aChannel,
+ in nsICookie2 aCookie,
+ inout boolean aIsSession,
+ inout int64_t aExpiry);
+};
+
+%{ C++
+/**
+ * The nsICookiePermission implementation is an XPCOM service registered
+ * under the ContractID:
+ */
+#define NS_COOKIEPERMISSION_CONTRACTID "@mozilla.org/cookie/permission;1"
+%}
diff --git a/netwerk/cookie/nsICookieService.idl b/netwerk/cookie/nsICookieService.idl
new file mode 100644
index 000000000..f876c61b4
--- /dev/null
+++ b/netwerk/cookie/nsICookieService.idl
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIPrompt;
+interface nsIChannel;
+
+/**
+ * nsICookieService
+ *
+ * Provides methods for setting and getting cookies in the context of a
+ * page load. See nsICookieManager for methods to manipulate the cookie
+ * database directly. This separation of interface is mainly historical.
+ *
+ * This service broadcasts the notifications detailed below when the cookie
+ * list is changed, or a cookie is rejected.
+ *
+ * NOTE: observers of these notifications *must* not attempt to change profile
+ * or switch into or out of private browsing mode from within the
+ * observer. Doing so will cause undefined behavior. Mutating the cookie
+ * list (e.g. by calling methods on nsICookieService and friends) is
+ * allowed, but beware that there may be pending notifications you haven't
+ * seen yet -- for instance, a "batch-deleted" notification will likely be
+ * immediately followed by "added". You may check the state of the cookie
+ * list to determine if this is the case.
+ *
+ * topic : "cookie-changed"
+ * broadcast whenever the cookie list changes in some way. see
+ * explanation of data strings below.
+ * subject: see below.
+ * data : "deleted"
+ * a cookie was deleted. the subject is an nsICookie2 representing
+ * the deleted cookie.
+ * "added"
+ * a cookie was added. the subject is an nsICookie2 representing
+ * the added cookie.
+ * "changed"
+ * a cookie was changed. the subject is an nsICookie2 representing
+ * the new cookie. (note that host, path, and name are invariant
+ * for a given cookie; other parameters may change.)
+ * "batch-deleted"
+ * a set of cookies was purged (typically, because they have either
+ * expired or because the cookie list has grown too large). The subject
+ * is an nsIArray of nsICookie2's representing the deleted cookies.
+ * Note that the array could contain a single cookie.
+ * "cleared"
+ * the entire cookie list was cleared. the subject is null.
+ *
+ * topic : "cookie-rejected"
+ * broadcast whenever a cookie was rejected from being set as a
+ * result of user prefs.
+ * subject: an nsIURI interface pointer representing the URI that attempted
+ * to set the cookie.
+ * data : none.
+ *
+ * topic : "third-party-cookie-accepted"
+ * broadcast whenever a third party cookie was accepted
+ * subject: an nsIURI interface pointer representing the URI that attempted
+ * to set the cookie.
+ * data : the referrer, or "?" if unknown
+ *
+ * topic : "third-party-cookie-rejected"
+ * broadcast whenever a third party cookie was rejected
+ * subject: an nsIURI interface pointer representing the URI that attempted
+ * to set the cookie.
+ * data : the referrer, or "?" if unknown
+ */
+[scriptable, uuid(1e94e283-2811-4f43-b947-d22b1549d824)]
+interface nsICookieService : nsISupports
+{
+ /*
+ * Possible values for the "network.cookie.cookieBehavior" preference.
+ */
+ const uint32_t BEHAVIOR_ACCEPT = 0; // allow all cookies
+ const uint32_t BEHAVIOR_REJECT_FOREIGN = 1; // reject all third-party cookies
+ const uint32_t BEHAVIOR_REJECT = 2; // reject all cookies
+ const uint32_t BEHAVIOR_LIMIT_FOREIGN = 3; // reject third-party cookies unless the
+ // eTLD already has at least one cookie
+
+ /*
+ * Possible values for the "network.cookie.lifetimePolicy" preference.
+ */
+ const uint32_t ACCEPT_NORMALLY = 0; // accept normally
+ // Value = 1 is considered the same as 0 (See Bug 606655).
+ const uint32_t ACCEPT_SESSION = 2; // downgrade to session
+ const uint32_t ACCEPT_FOR_N_DAYS = 3; // limit lifetime to N days
+
+ /*
+ * Get the complete cookie string associated with the URI.
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aChannel
+ * the channel used to load the document. this parameter should not
+ * be null, otherwise the cookies will not be returned if third-party
+ * cookies have been disabled by the user. (the channel is used
+ * to determine the originating URI of the document; if it is not
+ * provided, the cookies will be assumed third-party.)
+ *
+ * @return the resulting cookie string
+ */
+ string getCookieString(in nsIURI aURI, in nsIChannel aChannel);
+
+ /*
+ * Get the complete cookie string associated with the URI.
+ *
+ * This function is NOT redundant with getCookieString, as the result
+ * will be different based on httponly (see bug 178993)
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aFirstURI
+ * the URI that the user originally typed in or clicked on to initiate
+ * the load of the document referenced by aURI.
+ * @param aChannel
+ * the channel used to load the document. this parameter should not
+ * be null, otherwise the cookies will not be returned if third-party
+ * cookies have been disabled by the user. (the channel is used
+ * to determine the originating URI of the document; if it is not
+ * provided, the cookies will be assumed third-party.)
+ *
+ * @return the resulting cookie string
+ */
+ string getCookieStringFromHttp(in nsIURI aURI, in nsIURI aFirstURI, in nsIChannel aChannel);
+
+ /*
+ * Set the cookie string associated with the URI.
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aPrompt
+ * the prompt to use for all user-level cookie notifications. This is
+ * presently ignored and can be null. (Prompt information is determined
+ * from the channel if necessary.)
+ * @param aCookie
+ * the cookie string to set.
+ * @param aChannel
+ * the channel used to load the document. this parameter should not
+ * be null, otherwise the cookies will not be set if third-party
+ * cookies have been disabled by the user. (the channel is used
+ * to determine the originating URI of the document; if it is not
+ * provided, the cookies will be assumed third-party.)
+ */
+ void setCookieString(in nsIURI aURI, in nsIPrompt aPrompt, in string aCookie, in nsIChannel aChannel);
+
+ /*
+ * Set the cookie string and expires associated with the URI.
+ *
+ * This function is NOT redundant with setCookieString, as the result
+ * will be different based on httponly (see bug 178993)
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aFirstURI
+ * the URI that the user originally typed in or clicked on to initiate
+ * the load of the document referenced by aURI.
+ * @param aPrompt
+ * the prompt to use for all user-level cookie notifications. This is
+ * presently ignored and can be null. (Prompt information is determined
+ * from the channel if necessary.)
+ * @param aCookie
+ * the cookie string to set.
+ * @param aServerTime
+ * the current time reported by the server, if available. This should
+ * be the string from the Date header in an HTTP response. If the
+ * string is empty or null, server time is assumed to be the current
+ * local time. If provided, it will be used to calculate the expiry
+ * time of the cookie relative to the server's local time.
+ * @param aChannel
+ * the channel used to load the document. this parameter should not
+ * be null, otherwise the cookies will not be set if third-party
+ * cookies have been disabled by the user. (the channel is used
+ * to determine the originating URI of the document; if it is not
+ * provided, the cookies will be assumed third-party.)
+ */
+ void setCookieStringFromHttp(in nsIURI aURI, in nsIURI aFirstURI, in nsIPrompt aPrompt, in string aCookie, in string aServerTime, in nsIChannel aChannel);
+};
diff --git a/netwerk/cookie/test/browser/browser.ini b/netwerk/cookie/test/browser/browser.ini
new file mode 100644
index 000000000..342e14578
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ file_empty.html
+
+[browser_originattributes.js]
diff --git a/netwerk/cookie/test/browser/browser_originattributes.js b/netwerk/cookie/test/browser/browser_originattributes.js
new file mode 100644
index 000000000..617d52e35
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_originattributes.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { classes: Cc, interfaces: Ci } = Components;
+
+const USER_CONTEXTS = ["default", "personal", "work"];
+
+const COOKIE_NAMES = ["cookie0", "cookie1", "cookie2"];
+
+const TEST_URL =
+ "http://example.com/browser/netwerk/cookie/test/browser/file_empty.html";
+
+let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
+
+// opens `uri' in a new tab with the provided userContextId and focuses it.
+// returns the newly opened tab
+function* openTabInUserContext(uri, userContextId) {
+ // open the tab in the correct userContextId
+ let tab = gBrowser.addTab(uri, {userContextId});
+
+ // select tab and make sure its browser is focused
+ gBrowser.selectedTab = tab;
+ tab.ownerDocument.defaultView.focus();
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ // wait for tab load
+ yield BrowserTestUtils.browserLoaded(browser);
+
+ return {tab, browser};
+}
+
+add_task(function* setup() {
+ // make sure userContext is enabled.
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["privacy.userContext.enabled", true]
+ ]}, resolve);
+ });
+});
+
+add_task(function* test() {
+ // load the page in 3 different contexts and set a cookie
+ // which should only be visible in that context
+ for (let userContextId of Object.keys(USER_CONTEXTS)) {
+ // open our tab in the given user context
+ let {tab, browser} = yield* openTabInUserContext(TEST_URL, userContextId);
+
+ yield ContentTask.spawn(browser,
+ {names: COOKIE_NAMES, value: USER_CONTEXTS[userContextId]},
+ function(opts) {
+ for (let name of opts.names) {
+ content.document.cookie = name + "=" + opts.value;
+ }
+ });
+
+ // remove the tab
+ gBrowser.removeTab(tab);
+ }
+
+ let expectedValues = USER_CONTEXTS.slice(0);
+ yield checkCookies(expectedValues, "before removal");
+
+ // remove cookies that belongs to user context id #1
+ cm.removeCookiesWithOriginAttributes(JSON.stringify({userContextId: 1}));
+
+ expectedValues[1] = undefined;
+ yield checkCookies(expectedValues, "after removal");
+});
+
+function *checkCookies(expectedValues, time) {
+ for (let userContextId of Object.keys(expectedValues)) {
+ let cookiesFromTitle = yield* getCookiesFromJS(userContextId);
+ let cookiesFromManager = getCookiesFromManager(userContextId);
+
+ let expectedValue = expectedValues[userContextId];
+ for (let name of COOKIE_NAMES) {
+ is(cookiesFromTitle[name], expectedValue,
+ `User context ${userContextId}: ${name} should be correct from title ${time}`);
+ is(cookiesFromManager[name], expectedValue,
+ `User context ${userContextId}: ${name} should be correct from manager ${time}`);
+ }
+
+ }
+}
+
+function getCookiesFromManager(userContextId) {
+ let cookies = {};
+ let enumerator = cm.getCookiesWithOriginAttributes(JSON.stringify({userContextId}));
+ while (enumerator.hasMoreElements()) {
+ let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
+ cookies[cookie.name] = cookie.value;
+ }
+ return cookies;
+}
+
+function* getCookiesFromJS(userContextId) {
+ let {tab, browser} = yield* openTabInUserContext(TEST_URL, userContextId);
+
+ // get the cookies
+ let cookieString = yield ContentTask.spawn(browser, null, function() {
+ return content.document.cookie;
+ });
+
+ // check each item in the title and validate it meets expectatations
+ let cookies = {};
+ for (let cookie of cookieString.split(";")) {
+ let [name, value] = cookie.trim().split("=");
+ cookies[name] = value;
+ }
+
+ gBrowser.removeTab(tab);
+ return cookies;
+}
diff --git a/netwerk/cookie/test/browser/file_empty.html b/netwerk/cookie/test/browser/file_empty.html
new file mode 100644
index 000000000..5a08c4205
--- /dev/null
+++ b/netwerk/cookie/test/browser/file_empty.html
@@ -0,0 +1,3 @@
+<html><body>
+</body></html>
+
diff --git a/netwerk/cookie/test/unit/test_bug1155169.js b/netwerk/cookie/test/unit/test_bug1155169.js
new file mode 100644
index 000000000..6806ffe6d
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug1155169.js
@@ -0,0 +1,73 @@
+var {utils: Cu, interfaces: Ci, classes: Cc} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const URI = Services.io.newURI("http://example.org/", null, null);
+
+const cs = Cc["@mozilla.org/cookieService;1"]
+ .getService(Ci.nsICookieService);
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Add a new cookie.
+ setCookie("foo=bar", {
+ type: "added", isSession: true, isSecure: false, isHttpOnly: false
+ });
+
+ // Update cookie with isHttpOnly=true.
+ setCookie("foo=bar; HttpOnly", {
+ type: "changed", isSession: true, isSecure: false, isHttpOnly: true
+ });
+
+ // Update cookie with isSecure=true.
+ setCookie("foo=bar; Secure", {
+ type: "changed", isSession: true, isSecure: true, isHttpOnly: false
+ });
+
+ // Update cookie with isSession=false.
+ let expiry = new Date();
+ expiry.setUTCFullYear(expiry.getUTCFullYear() + 2);
+ setCookie(`foo=bar; Expires=${expiry.toGMTString()}`, {
+ type: "changed", isSession: false, isSecure: false, isHttpOnly: false
+ });
+
+ // Reset cookie.
+ setCookie("foo=bar", {
+ type: "changed", isSession: true, isSecure: false, isHttpOnly: false
+ });
+}
+
+function setCookie(value, expected) {
+ function setCookieInternal(value, expected = null) {
+ function observer(subject, topic, data) {
+ if (!expected) {
+ do_throw("no notification expected");
+ return;
+ }
+
+ // Check we saw the right notification.
+ do_check_eq(data, expected.type);
+
+ // Check cookie details.
+ let cookie = subject.QueryInterface(Ci.nsICookie2);
+ do_check_eq(cookie.isSession, expected.isSession);
+ do_check_eq(cookie.isSecure, expected.isSecure);
+ do_check_eq(cookie.isHttpOnly, expected.isHttpOnly);
+ }
+
+ Services.obs.addObserver(observer, "cookie-changed", false);
+ cs.setCookieStringFromHttp(URI, null, null, value, null, null);
+ Services.obs.removeObserver(observer, "cookie-changed");
+ }
+
+ // Check that updating/inserting the cookie works.
+ setCookieInternal(value, expected);
+
+ // Check that we ignore identical cookies.
+ setCookieInternal(value);
+}
diff --git a/netwerk/cookie/test/unit/test_bug1267910.js b/netwerk/cookie/test/unit/test_bug1267910.js
new file mode 100644
index 000000000..93ea5e132
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug1267910.js
@@ -0,0 +1,196 @@
+/*
+ * Bug 1267910 - Add test cases for the backward compatiability and originAttributes
+ * of nsICookieManager2.
+ */
+
+var {utils: Cu, interfaces: Ci, classes: Cc} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const BASE_URL = "http://example.org/";
+
+const COOKIE = {
+ host: BASE_URL,
+ path: "/",
+ name: "test1",
+ value: "yes",
+ isSecure: false,
+ isHttpOnly: false,
+ isSession: true,
+ expiry: 2145934800,
+};
+
+const COOKIE_OA_DEFAULT = {
+ host: BASE_URL,
+ path: "/",
+ name: "test0",
+ value: "yes0",
+ isSecure: false,
+ isHttpOnly: false,
+ isSession: true,
+ expiry: 2145934800,
+ originAttributes: {},
+};
+
+const COOKIE_OA_1 = {
+ host: BASE_URL,
+ path: "/",
+ name: "test1",
+ value: "yes1",
+ isSecure: false,
+ isHttpOnly: false,
+ isSession: true,
+ expiry: 2145934800,
+ originAttributes: {userContextId: 1},
+};
+
+function checkCookie(cookie, cookieObj) {
+ for (let prop of Object.keys(cookieObj)) {
+ if (prop === "originAttributes") {
+ ok(ChromeUtils.isOriginAttributesEqual(cookie[prop], cookieObj[prop]),
+ "Check cookie: " + prop);
+ } else {
+ equal(cookie[prop], cookieObj[prop], "Check cookie: " + prop);
+ }
+ }
+}
+
+function countCookies(enumerator) {
+ let cnt = 0;
+
+ while (enumerator.hasMoreElements()) {
+ cnt++;
+ enumerator.getNext();
+ }
+
+ return cnt;
+}
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ // Enable user context id
+ Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+
+ add_test(test_backward_compatiability);
+ add_test(test_originAttributes);
+
+
+ run_next_test();
+}
+
+/*
+ * Test for backward compatiablility that APIs works correctly without
+ * originAttributes.
+ */
+function test_backward_compatiability() {
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Call Add() to add a cookie without originAttributes
+ Services.cookies.add(COOKIE.host,
+ COOKIE.path,
+ COOKIE.name,
+ COOKIE.value,
+ COOKIE.isSecure,
+ COOKIE.isHttpOnly,
+ COOKIE.isSession,
+ COOKIE.expiry);
+
+ // Call getCookiesFromHost() to get cookies without originAttributes
+ let enumerator = Services.cookies.getCookiesFromHost(BASE_URL);
+
+ ok(enumerator.hasMoreElements(), "Cookies available");
+ let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+
+ checkCookie(foundCookie, COOKIE);
+
+ ok(!enumerator.hasMoreElements(), "We should get only one cookie");
+
+ run_next_test();
+}
+
+/*
+ * Test for originAttributes.
+ */
+function test_originAttributes() {
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Add a cookie for default originAttributes.
+ Services.cookies.add(COOKIE_OA_DEFAULT.host,
+ COOKIE_OA_DEFAULT.path,
+ COOKIE_OA_DEFAULT.name,
+ COOKIE_OA_DEFAULT.value,
+ COOKIE_OA_DEFAULT.isSecure,
+ COOKIE_OA_DEFAULT.isHttpOnly,
+ COOKIE_OA_DEFAULT.isSession,
+ COOKIE_OA_DEFAULT.expiry,
+ COOKIE_OA_DEFAULT.originAttributes);
+
+ // Get cookies for default originAttributes.
+ let enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes);
+
+ // Check that do we get cookie correctly.
+ ok(enumerator.hasMoreElements(), "Cookies available");
+ let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+ checkCookie(foundCookie, COOKIE_OA_DEFAULT);
+
+ // We should only get one cookie.
+ ok(!enumerator.hasMoreElements(), "We should get only one cookie");
+
+ // Get cookies for originAttributes with user context id 1.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes);
+
+ // Check that we will not get cookies if the originAttributes is different.
+ ok(!enumerator.hasMoreElements(), "No cookie should be here");
+
+ // Add a cookie for originAttributes with user context id 1.
+ Services.cookies.add(COOKIE_OA_1.host,
+ COOKIE_OA_1.path,
+ COOKIE_OA_1.name,
+ COOKIE_OA_1.value,
+ COOKIE_OA_1.isSecure,
+ COOKIE_OA_1.isHttpOnly,
+ COOKIE_OA_1.isSession,
+ COOKIE_OA_1.expiry,
+ COOKIE_OA_1.originAttributes);
+
+ // Get cookies for originAttributes with user context id 1.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes);
+
+ // Check that do we get cookie correctly.
+ ok(enumerator.hasMoreElements(), "Cookies available");
+ foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+ checkCookie(foundCookie, COOKIE_OA_1);
+
+ // We should only get one cookie.
+ ok(!enumerator.hasMoreElements(), "We should get only one cookie");
+
+ // Check that add a cookie will not affect cookies in different originAttributes.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes);
+ equal(countCookies(enumerator), 1, "We should get only one cookie for default originAttributes");
+
+ // Remove a cookie for originAttributes with user context id 1.
+ Services.cookies.remove(COOKIE_OA_1.host, COOKIE_OA_1.name, COOKIE_OA_1.path,
+ false, COOKIE_OA_1.originAttributes);
+
+ // Check that remove will not affect cookies in default originAttributes.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes);
+ equal(countCookies(enumerator), 1, "Get one cookie for default originAttributes.");
+
+ // Check that should be no cookie for originAttributes with user context id 1.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes);
+ equal(countCookies(enumerator), 0, "No cookie shold be here");
+
+ // Remove a cookie for default originAttributes.
+ Services.cookies.remove(COOKIE_OA_DEFAULT.host, COOKIE_OA_DEFAULT.name, COOKIE_OA_DEFAULT.path,
+ false, COOKIE_OA_DEFAULT.originAttributes);
+
+ // Check remove() works correctly for default originAttributes.
+ enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes);
+ equal(countCookies(enumerator), 0, "No cookie shold be here");
+
+ run_next_test();
+}
diff --git a/netwerk/cookie/test/unit/test_bug643051.js b/netwerk/cookie/test/unit/test_bug643051.js
new file mode 100644
index 000000000..d6695054e
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug643051.js
@@ -0,0 +1,29 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+
+ let uri = NetUtil.newURI("http://example.org/");
+
+ let set = "foo=bar\nbaz=foo";
+ let expected = "foo=bar; baz=foo";
+ cs.setCookieStringFromHttp(uri, null, null, set, null, null);
+
+ let actual = cs.getCookieStringFromHttp(uri, null, null);
+ do_check_eq(actual, expected);
+
+ uri = NetUtil.newURI("http://example.com/");
+ cs.setCookieString(uri, null, set, null);
+
+ expected = "foo=bar";
+ actual = cs.getCookieString(uri, null, null);
+ do_check_eq(actual, expected);
+}
+
diff --git a/netwerk/cookie/test/unit/test_eviction.js b/netwerk/cookie/test/unit/test_eviction.js
new file mode 100644
index 000000000..7f693ee94
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_eviction.js
@@ -0,0 +1,296 @@
+var {utils: Cu, interfaces: Ci, classes: Cc} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const BASE_HOSTNAMES = ["example.org", "example.co.uk"];
+const SUBDOMAINS = ["", "pub.", "www.", "other."];
+
+const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+const cm = cs.QueryInterface(Ci.nsICookieManager2);
+
+function run_test() {
+ var tests = [];
+ Services.prefs.setIntPref("network.cookie.staleThreshold", 0);
+ for (var host of BASE_HOSTNAMES) {
+ var base = SUBDOMAINS[0] + host;
+ var sub = SUBDOMAINS[1] + host;
+ var other = SUBDOMAINS[2] + host;
+ var another = SUBDOMAINS[3] + host;
+ tests.push([host, test_basic_eviction.bind(this, base, sub, other, another)]);
+ add_task(function* a() {
+ var t = tests.splice(0, 1)[0];
+ do_print('testing with host ' + t[0]);
+ yield t[1]();
+ cm.removeAll();
+ });
+ tests.push([host, test_domain_or_path_matches_not_both.bind(this, base, sub, other, another)]);
+ add_task(function*() {
+ var t = tests.splice(0, 1)[0];
+ do_print('testing with host ' + t[0]);
+ yield t[1]();
+ cm.removeAll();
+ });
+ }
+ add_task(function*() {
+ yield test_localdomain();
+ cm.removeAll();
+ });
+
+ add_task(function*() {
+ yield test_path_prefix();
+ });
+
+ run_next_test();
+}
+
+// Verify that cookies that share a path prefix with the URI path are still considered
+// candidates for eviction, since the paths do not actually match.
+function* test_path_prefix() {
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 2);
+
+ const BASE_URI = Services.io.newURI("http://example.org/", null, null);
+ const BASE_BAR = Services.io.newURI("http://example.org/bar/", null, null);
+ const BASE_BARBAR = Services.io.newURI("http://example.org/barbar/", null, null);
+
+ yield setCookie("session_first", null, null, null, BASE_URI);
+ yield setCookie("session_second", null, "/bar", null, BASE_BAR);
+ verifyCookies(['session_first', 'session_second'], BASE_URI);
+
+ yield setCookie("session_third", null, "/barbar", null, BASE_BARBAR);
+ verifyCookies(['session_first', 'session_third'], BASE_URI);
+}
+
+// Verify that subdomains of localhost are treated as separate hosts and aren't considered
+// candidates for eviction.
+function* test_localdomain() {
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 2);
+
+ const BASE_URI = Services.io.newURI("http://localhost", null, null);
+ const BASE_BAR = Services.io.newURI("http://localhost/bar", null, null);
+ const OTHER_URI = Services.io.newURI("http://other.localhost", null, null);
+ const OTHER_BAR = Services.io.newURI("http://other.localhost/bar", null, null);
+
+ yield setCookie("session_no_path", null, null, null, BASE_URI);
+ yield setCookie("session_bar_path", null, "/bar", null, BASE_BAR);
+
+ yield setCookie("session_no_path", null, null, null, OTHER_URI);
+ yield setCookie("session_bar_path", null, "/bar", null, OTHER_BAR);
+
+ verifyCookies(['session_no_path',
+ 'session_bar_path'], BASE_URI);
+ verifyCookies(['session_no_path',
+ 'session_bar_path'], OTHER_URI);
+
+ yield setCookie("session_another_no_path", null, null, null, BASE_URI);
+ verifyCookies(['session_no_path',
+ 'session_another_no_path'], BASE_URI);
+
+ yield setCookie("session_another_no_path", null, null, null, OTHER_URI);
+ verifyCookies(['session_no_path',
+ 'session_another_no_path'], OTHER_URI);
+}
+
+// Ensure that cookies are still considered candidates for eviction if either the domain
+// or path matches, but not both.
+function* test_domain_or_path_matches_not_both(base_host,
+ subdomain_host,
+ other_subdomain_host,
+ another_subdomain_host) {
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 2);
+
+ const BASE_URI = Services.io.newURI("http://" + base_host, null, null);
+ const PUB_FOO_PATH = Services.io.newURI("http://" + subdomain_host + "/foo/", null, null);
+ const WWW_BAR_PATH = Services.io.newURI("http://" + other_subdomain_host + "/bar/", null, null);
+ const OTHER_BAR_PATH = Services.io.newURI("http://" + another_subdomain_host + "/bar/", null, null);
+ const PUB_BAR_PATH = Services.io.newURI("http://" + subdomain_host + "/bar/", null, null);
+ const WWW_FOO_PATH = Services.io.newURI("http://" + other_subdomain_host + "/foo/", null, null);
+
+ yield setCookie("session_pub_with_foo_path", subdomain_host, "/foo", null, PUB_FOO_PATH);
+ yield setCookie("session_www_with_bar_path", other_subdomain_host, "/bar", null, WWW_BAR_PATH);
+ verifyCookies(['session_pub_with_foo_path',
+ 'session_www_with_bar_path'], BASE_URI);
+
+ yield setCookie("session_pub_with_bar_path", subdomain_host, "/bar", null, PUB_BAR_PATH);
+ verifyCookies(['session_www_with_bar_path',
+ 'session_pub_with_bar_path'], BASE_URI);
+
+ yield setCookie("session_other_with_bar_path", another_subdomain_host, "/bar", null, OTHER_BAR_PATH);
+ verifyCookies(['session_pub_with_bar_path',
+ 'session_other_with_bar_path'], BASE_URI);
+}
+
+function* test_basic_eviction(base_host, subdomain_host, other_subdomain_host) {
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 5);
+
+ const BASE_URI = Services.io.newURI("http://" + base_host, null, null);
+ const SUBDOMAIN_URI = Services.io.newURI("http://" + subdomain_host, null, null);
+ const OTHER_SUBDOMAIN_URI = Services.io.newURI("http://" + other_subdomain_host, null, null);
+ const FOO_PATH = Services.io.newURI("http://" + base_host + "/foo/", null, null);
+ const BAR_PATH = Services.io.newURI("http://" + base_host + "/bar/", null, null);
+ const ALL_SUBDOMAINS = '.' + base_host;
+ const OTHER_SUBDOMAIN = other_subdomain_host;
+
+ // Initialize the set of cookies with a mix of non-session cookies with no path,
+ // and session cookies with explicit paths. Any subsequent cookies added will cause
+ // existing cookies to be evicted.
+ yield setCookie("non_session_non_path_non_domain", null, null, 100000, BASE_URI);
+ yield setCookie("non_session_non_path_subdomain", ALL_SUBDOMAINS, null, 100000, SUBDOMAIN_URI);
+ yield setCookie("session_non_path_pub_domain", OTHER_SUBDOMAIN, null, null, OTHER_SUBDOMAIN_URI);
+ yield setCookie("session_foo_path", null, "/foo", null, FOO_PATH);
+ yield setCookie("session_bar_path", null, "/bar", null, BAR_PATH);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'session_non_path_pub_domain',
+ 'session_foo_path',
+ 'session_bar_path'], BASE_URI);
+
+ // Ensure that cookies set for the / path appear more recent.
+ cs.getCookieString(OTHER_SUBDOMAIN_URI, null)
+ verifyCookies(['non_session_non_path_non_domain',
+ 'session_foo_path',
+ 'session_bar_path',
+ 'non_session_non_path_subdomain',
+ 'session_non_path_pub_domain'], BASE_URI);
+
+ // Evict oldest session cookie that does not match example.org/foo (session_bar_path)
+ yield setCookie("session_foo_path_2", null, "/foo", null, FOO_PATH);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'session_foo_path',
+ 'non_session_non_path_subdomain',
+ 'session_non_path_pub_domain',
+ 'session_foo_path_2'], BASE_URI);
+
+ // Evict oldest session cookie that does not match example.org/bar (session_foo_path)
+ yield setCookie("session_bar_path_2", null, "/bar", null, BAR_PATH);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'session_non_path_pub_domain',
+ 'session_foo_path_2',
+ 'session_bar_path_2'], BASE_URI);
+
+ // Evict oldest session cookie that does not match example.org/ (session_non_path_pub_domain)
+ yield setCookie("non_session_non_path_non_domain_2", null, null, 100000, BASE_URI);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'session_foo_path_2',
+ 'session_bar_path_2',
+ 'non_session_non_path_non_domain_2'], BASE_URI);
+
+ // Evict oldest session cookie that does not match example.org/ (session_foo_path_2)
+ yield setCookie("session_non_path_non_domain_3", null, null, null, BASE_URI);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'session_bar_path_2',
+ 'non_session_non_path_non_domain_2',
+ 'session_non_path_non_domain_3'], BASE_URI);
+
+ // Evict oldest session cookie; all such cookies match example.org/bar (session_bar_path_2)
+ // note: this new cookie doesn't have an explicit path, but empty paths inherit the
+ // request's path
+ yield setCookie("non_session_bar_path_non_domain", null, null, 100000, BAR_PATH);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'non_session_non_path_non_domain_2',
+ 'session_non_path_non_domain_3',
+ 'non_session_bar_path_non_domain'], BASE_URI);
+
+ // Evict oldest session cookie, even though it matches pub.example.org (session_non_path_non_domain_3)
+ yield setCookie("non_session_non_path_pub_domain", null, null, 100000, OTHER_SUBDOMAIN_URI);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'non_session_non_path_non_domain_2',
+ 'non_session_bar_path_non_domain',
+ 'non_session_non_path_pub_domain'], BASE_URI);
+
+ // All session cookies have been evicted.
+ // Evict oldest non-session non-domain-matching cookie (non_session_non_path_pub_domain)
+ yield setCookie("non_session_bar_path_non_domain_2", null, '/bar', 100000, BAR_PATH);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'non_session_non_path_non_domain_2',
+ 'non_session_bar_path_non_domain',
+ 'non_session_bar_path_non_domain_2'], BASE_URI);
+
+ // Evict oldest non-session non-path-matching cookie (non_session_bar_path_non_domain)
+ yield setCookie("non_session_non_path_non_domain_4", null, null, 100000, BASE_URI);
+ verifyCookies(['non_session_non_path_non_domain',
+ 'non_session_non_path_subdomain',
+ 'non_session_non_path_non_domain_2',
+ 'non_session_bar_path_non_domain_2',
+ 'non_session_non_path_non_domain_4'], BASE_URI);
+
+ // At this point all remaining cookies are non-session cookies, have a path of /,
+ // and either don't have a domain or have one that matches subdomains.
+ // They will therefore be evicted from oldest to newest if all new cookies added share
+ // similar characteristics.
+}
+
+// Verify that the given cookie names exist, and are ordered from least to most recently accessed
+function verifyCookies(names, uri) {
+ do_check_eq(cm.countCookiesFromHost(uri.host), names.length);
+ let cookies = cm.getCookiesFromHost(uri.host, {});
+ let actual_cookies = [];
+ while (cookies.hasMoreElements()) {
+ let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2);
+ actual_cookies.push(cookie);
+ }
+ if (names.length != actual_cookies.length) {
+ let left = names.filter(function(n) {
+ return actual_cookies.findIndex(function(c) {
+ return c.name == n;
+ }) == -1;
+ });
+ let right = actual_cookies.filter(function(c) {
+ return names.findIndex(function(n) {
+ return c.name == n;
+ }) == -1;
+ }).map(function(c) { return c.name });
+ if (left.length) {
+ do_print("unexpected cookies: " + left);
+ }
+ if (right.length) {
+ do_print("expected cookies: " + right);
+ }
+ }
+ do_check_eq(names.length, actual_cookies.length);
+ actual_cookies.sort(function(a, b) {
+ if (a.lastAccessed < b.lastAccessed)
+ return -1;
+ if (a.lastAccessed > b.lastAccessed)
+ return 1;
+ return 0;
+ });
+ for (var i = 0; i < names.length; i++) {
+ do_check_eq(names[i], actual_cookies[i].name);
+ do_check_eq(names[i].startsWith('session'), actual_cookies[i].isSession);
+ }
+}
+
+var lastValue = 0
+function* setCookie(name, domain, path, maxAge, url) {
+ let value = name + "=" + ++lastValue;
+ var s = 'setting cookie ' + value;
+ if (domain) {
+ value += "; Domain=" + domain;
+ s += ' (d=' + domain + ')';
+ }
+ if (path) {
+ value += "; Path=" + path;
+ s += ' (p=' + path + ')';
+ }
+ if (maxAge) {
+ value += "; Max-Age=" + maxAge;
+ s += ' (non-session)';
+ } else {
+ s += ' (session)';
+ }
+ s += ' for ' + url.spec;
+ do_print(s);
+ cs.setCookieStringFromHttp(url, null, null, value, null, null);
+ return new Promise(function(resolve) {
+ // Windows XP has low precision timestamps that cause our cookie eviction
+ // algorithm to produce different results from other platforms. We work around
+ // this by ensuring that there's a clear gap between each cookie update.
+ do_timeout(10, resolve);
+ })
+}
diff --git a/netwerk/cookie/test/unit/test_parser_0001.js b/netwerk/cookie/test/unit/test_parser_0001.js
new file mode 100644
index 000000000..f885972a0
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_parser_0001.js
@@ -0,0 +1,29 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function inChildProcess() {
+ return Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess())
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+
+ let uri = NetUtil.newURI("http://example.org/");
+
+ let set = "foo=bar";
+ cs.setCookieStringFromHttp(uri, null, null, set, null, null);
+
+ let expected = "foo=bar";
+ let actual = cs.getCookieStringFromHttp(uri, null, null);
+ do_check_eq(actual, expected);
+}
+
diff --git a/netwerk/cookie/test/unit/test_parser_0019.js b/netwerk/cookie/test/unit/test_parser_0019.js
new file mode 100644
index 000000000..7811b01fe
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_parser_0019.js
@@ -0,0 +1,29 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function inChildProcess() {
+ return Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime)
+ .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess())
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
+
+ let uri = NetUtil.newURI("http://example.org/");
+
+ let set = "foo=b;max-age=3600, c=d;path=/";
+ cs.setCookieStringFromHttp(uri, null, null, set, null, null);
+
+ let expected = "foo=b";
+ let actual = cs.getCookieStringFromHttp(uri, null, null);
+ do_check_eq(actual, expected);
+}
+
diff --git a/netwerk/cookie/test/unit/xpcshell.ini b/netwerk/cookie/test/unit/xpcshell.ini
new file mode 100644
index 000000000..f9c4093cf
--- /dev/null
+++ b/netwerk/cookie/test/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+
+[test_bug643051.js]
+[test_bug1155169.js]
+[test_bug1267910.js]
+[test_parser_0001.js]
+[test_parser_0019.js]
+[test_eviction.js] \ No newline at end of file
diff --git a/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js
new file mode 100644
index 000000000..988c8d196
--- /dev/null
+++ b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js
@@ -0,0 +1,9 @@
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ run_test_in_child("../unit/test_parser_0001.js");
+}
diff --git a/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js
new file mode 100644
index 000000000..535ac6e34
--- /dev/null
+++ b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js
@@ -0,0 +1,9 @@
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ run_test_in_child("../unit/test_parser_0019.js");
+}
diff --git a/netwerk/cookie/test/unit_ipc/xpcshell.ini b/netwerk/cookie/test/unit_ipc/xpcshell.ini
new file mode 100644
index 000000000..922443490
--- /dev/null
+++ b/netwerk/cookie/test/unit_ipc/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head =
+tail =
+skip-if = toolkit == 'android'
+support-files =
+ !/netwerk/cookie/test/unit/test_parser_0001.js
+ !/netwerk/cookie/test/unit/test_parser_0019.js
+
+[test_ipc_parser_0001.js]
+[test_ipc_parser_0019.js]