| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_store.h" |
| |
| #include <utility> |
| |
| #include "base/optional.h" |
| #include "services/network/public/mojom/restricted_cookie_manager.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/frame/use_counter.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_change_event.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_list_item.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_store_delete_options.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_store_get_options.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_store_set_extra_options.h" |
| #include "third_party/blink/renderer/modules/cookie_store/cookie_store_set_options.h" |
| #include "third_party/blink/renderer/modules/event_modules.h" |
| #include "third_party/blink/renderer/modules/event_target_modules.h" |
| #include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h" |
| #include "third_party/blink/renderer/modules/service_worker/service_worker_registration.h" |
| #include "third_party/blink/renderer/platform/bindings/script_state.h" |
| #include "third_party/blink/renderer/platform/heap/handle.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| #include "third_party/blink/renderer/platform/wtf/time.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Returns null if and only if an exception is thrown. |
| network::mojom::blink::CookieManagerGetOptionsPtr ToBackendOptions( |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| auto backend_options = network::mojom::blink::CookieManagerGetOptions::New(); |
| |
| // TODO(crbug.com/729800): Handle the url option. |
| |
| if (options.matchType() == "starts-with") { |
| backend_options->match_type = |
| network::mojom::blink::CookieMatchType::STARTS_WITH; |
| } else { |
| DCHECK_EQ(options.matchType(), WTF::String("equals")); |
| backend_options->match_type = |
| network::mojom::blink::CookieMatchType::EQUALS; |
| } |
| |
| if (options.hasName()) { |
| backend_options->name = options.name(); |
| } else { |
| // No name provided. Use a filter that matches all cookies. This overrides |
| // a user-provided matchType. |
| backend_options->match_type = |
| network::mojom::blink::CookieMatchType::STARTS_WITH; |
| backend_options->name = g_empty_string; |
| } |
| |
| return backend_options; |
| } |
| |
| // Returns no value if and only if an exception is thrown. |
| base::Optional<WebCanonicalCookie> ToWebCanonicalCookie( |
| const KURL& cookie_url, |
| const CookieStoreSetExtraOptions& options, |
| ExceptionState& exception_state) { |
| const String& name = options.name(); |
| const String& value = options.value(); |
| if (name.IsEmpty() && value.Contains('=')) { |
| exception_state.ThrowTypeError( |
| "Cookie value cannot contain '=' if the name is empty"); |
| return base::nullopt; |
| } |
| |
| WTF::Time expires = options.hasExpires() |
| ? WTF::Time::FromJavaTime(options.expires()) |
| : WTF::Time(); |
| |
| String cookie_url_host = cookie_url.Host(); |
| String domain; |
| if (options.hasDomain()) { |
| // The leading dot (".") from the domain attribute is stripped in the |
| // Set-Cookie header, for compatibility. This API doesn't have compatibility |
| // constraints, so reject the edge case outright. |
| if (options.domain().StartsWith(".")) { |
| exception_state.ThrowTypeError("Cookie domain cannot start with \".\""); |
| return base::nullopt; |
| } |
| |
| domain = String(".") + options.domain(); |
| if (!cookie_url_host.EndsWith(domain) && |
| cookie_url_host != options.domain()) { |
| exception_state.ThrowTypeError( |
| "Cookie domain must domain-match current host"); |
| return base::nullopt; |
| } |
| } else { |
| // The absence of "domain" implies a host-only cookie. |
| domain = cookie_url_host; |
| } |
| |
| // The cookie store API is only exposed on secure origins. If this changes: |
| // 1) The secure option must default to false for insecure origins. |
| // 2) Only secure origins can set the "secure" option to true. |
| DCHECK(SecurityOrigin::IsSecure(cookie_url)); |
| |
| const bool secure = options.secure(); |
| if (!secure && (name.StartsWith("__Secure-") || name.StartsWith("__Host-"))) { |
| exception_state.ThrowTypeError( |
| "__Secure- and __Host- cookies must be secure"); |
| } |
| |
| network::mojom::CookieSameSite same_site; |
| if (options.sameSite() == "strict") { |
| same_site = network::mojom::CookieSameSite::STRICT_MODE; |
| } else if (options.sameSite() == "lax") { |
| same_site = network::mojom::CookieSameSite::LAX_MODE; |
| } else { |
| DCHECK_EQ(options.sameSite(), "unrestricted"); |
| same_site = network::mojom::CookieSameSite::NO_RESTRICTION; |
| } |
| |
| return WebCanonicalCookie::Create( |
| name, value, domain, options.path(), WTF::Time() /*creation*/, expires, |
| WTF::Time() /*last_access*/, secure, options.httpOnly(), same_site, |
| WebCanonicalCookie::kDefaultPriority); |
| } |
| |
| // Returns null if and only if an exception is thrown. |
| blink::mojom::blink::CookieChangeSubscriptionPtr ToBackendSubscription( |
| const KURL& default_cookie_url, |
| const CookieStoreGetOptions& subscription, |
| ExceptionState& exception_state) { |
| auto backend_subscription = |
| blink::mojom::blink::CookieChangeSubscription::New(); |
| |
| if (subscription.hasURL()) { |
| KURL subscription_url(default_cookie_url, subscription.url()); |
| // TODO(crbug.com/729800): Check that the URL is under default_cookie_url. |
| backend_subscription->url = subscription_url; |
| } else { |
| backend_subscription->url = default_cookie_url; |
| } |
| |
| if (subscription.matchType() == "starts-with") { |
| backend_subscription->match_type = |
| network::mojom::blink::CookieMatchType::STARTS_WITH; |
| } else { |
| DCHECK_EQ(subscription.matchType(), WTF::String("equals")); |
| backend_subscription->match_type = |
| network::mojom::blink::CookieMatchType::EQUALS; |
| } |
| |
| if (subscription.hasName()) { |
| backend_subscription->name = subscription.name(); |
| } else { |
| // No name provided. Use a filter that matches all cookies. This overrides |
| // a user-provided matchType. |
| backend_subscription->match_type = |
| network::mojom::blink::CookieMatchType::STARTS_WITH; |
| backend_subscription->name = g_empty_string; |
| } |
| |
| return backend_subscription; |
| } |
| |
| void ToCookieChangeSubscription( |
| const blink::mojom::blink::CookieChangeSubscription& backend_subscription, |
| CookieStoreGetOptions& subscription) { |
| subscription.setURL(backend_subscription.url); |
| |
| if (backend_subscription.match_type != |
| network::mojom::blink::CookieMatchType::STARTS_WITH || |
| !backend_subscription.name.IsEmpty()) { |
| subscription.setName(backend_subscription.name); |
| } |
| |
| switch (backend_subscription.match_type) { |
| case network::mojom::blink::CookieMatchType::STARTS_WITH: |
| subscription.setMatchType(WTF::String("starts-with")); |
| break; |
| case network::mojom::blink::CookieMatchType::EQUALS: |
| subscription.setMatchType(WTF::String("equals")); |
| break; |
| } |
| |
| subscription.setURL(backend_subscription.url); |
| } |
| |
| const KURL& DefaultCookieURL(ExecutionContext* execution_context) { |
| DCHECK(execution_context); |
| |
| if (execution_context->IsDocument()) { |
| Document* document = ToDocument(execution_context); |
| return document->CookieURL(); |
| } |
| |
| DCHECK(execution_context->IsServiceWorkerGlobalScope()); |
| ServiceWorkerGlobalScope* scope = |
| ToServiceWorkerGlobalScope(execution_context); |
| return scope->Url(); |
| } |
| |
| KURL DefaultSiteForCookies(ExecutionContext* execution_context) { |
| DCHECK(execution_context); |
| |
| if (execution_context->IsDocument()) { |
| Document* document = ToDocument(execution_context); |
| return document->SiteForCookies(); |
| } |
| |
| DCHECK(execution_context->IsServiceWorkerGlobalScope()); |
| ServiceWorkerGlobalScope* scope = |
| ToServiceWorkerGlobalScope(execution_context); |
| return scope->Url(); |
| } |
| |
| } // namespace |
| |
| CookieStore::~CookieStore() = default; |
| |
| ScriptPromise CookieStore::getAll(ScriptState* script_state, |
| const String& name, |
| ExceptionState& exception_state) { |
| CookieStoreGetOptions options; |
| options.setName(name); |
| return getAll(script_state, options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::getAll(ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| return DoRead(script_state, options, &CookieStore::GetAllForUrlToGetAllResult, |
| exception_state); |
| } |
| |
| ScriptPromise CookieStore::get(ScriptState* script_state, |
| const String& name, |
| ExceptionState& exception_state) { |
| CookieStoreGetOptions options; |
| options.setName(name); |
| return get(script_state, options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::get(ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| return DoRead(script_state, options, &CookieStore::GetAllForUrlToGetResult, |
| exception_state); |
| } |
| |
| ScriptPromise CookieStore::set(ScriptState* script_state, |
| const String& name, |
| const String& value, |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| CookieStoreSetExtraOptions set_options; |
| set_options.setName(name); |
| set_options.setValue(value); |
| if (options.hasExpires()) |
| set_options.setExpires(options.expires()); |
| set_options.setDomain(options.domain()); |
| set_options.setPath(options.path()); |
| set_options.setSecure(options.secure()); |
| set_options.setHttpOnly(options.httpOnly()); |
| set_options.setSameSite(options.sameSite()); |
| return set(script_state, set_options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::set(ScriptState* script_state, |
| const CookieStoreSetExtraOptions& options, |
| ExceptionState& exception_state) { |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| return DoWrite(script_state, options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::Delete(ScriptState* script_state, |
| const String& name, |
| ExceptionState& exception_state) { |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| CookieStoreSetExtraOptions set_options; |
| set_options.setName(name); |
| set_options.setValue(g_empty_string); |
| set_options.setExpires(0); |
| return DoWrite(script_state, set_options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::Delete(ScriptState* script_state, |
| const CookieStoreDeleteOptions& options, |
| ExceptionState& exception_state) { |
| CookieStoreSetExtraOptions set_options; |
| set_options.setName(options.name()); |
| set_options.setValue(g_empty_string); |
| set_options.setExpires(0); |
| set_options.setDomain(options.domain()); |
| set_options.setPath(options.path()); |
| set_options.setSecure(options.secure()); |
| set_options.setSameSite(options.sameSite()); |
| return DoWrite(script_state, set_options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::subscribeToChanges( |
| ScriptState* script_state, |
| const HeapVector<CookieStoreGetOptions>& subscriptions, |
| ExceptionState& exception_state) { |
| DCHECK(GetExecutionContext()->IsServiceWorkerGlobalScope()); |
| |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| Vector<blink::mojom::blink::CookieChangeSubscriptionPtr> |
| backend_subscriptions; |
| backend_subscriptions.ReserveInitialCapacity(subscriptions.size()); |
| for (const CookieStoreGetOptions& subscription : subscriptions) { |
| blink::mojom::blink::CookieChangeSubscriptionPtr backend_subscription = |
| ToBackendSubscription(default_cookie_url_, subscription, |
| exception_state); |
| if (backend_subscription.is_null()) { |
| DCHECK(exception_state.HadException()); |
| return ScriptPromise(); |
| } |
| backend_subscriptions.emplace_back(std::move(backend_subscription)); |
| } |
| |
| if (!subscription_backend_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "CookieStore backend went away"); |
| return ScriptPromise(); |
| } |
| |
| ServiceWorkerGlobalScope* scope = |
| ToServiceWorkerGlobalScope(GetExecutionContext()); |
| |
| if (!scope->IsInstalling()) { |
| exception_state.ThrowTypeError("Outside the installation phase"); |
| return ScriptPromise(); |
| } |
| |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| int64_t service_worker_registration_id = |
| scope->registration()->WebRegistration()->RegistrationId(); |
| subscription_backend_->AppendSubscriptions( |
| service_worker_registration_id, std::move(backend_subscriptions), |
| WTF::Bind(&CookieStore::OnSubscribeToCookieChangesResult, |
| WrapPersistent(resolver))); |
| return resolver->Promise(); |
| } |
| |
| ScriptPromise CookieStore::getChangeSubscriptions( |
| ScriptState* script_state, |
| ExceptionState& exception_state) { |
| DCHECK(GetExecutionContext()->IsServiceWorkerGlobalScope()); |
| |
| UseCounter::Count(CurrentExecutionContext(script_state->GetIsolate()), |
| WebFeature::kCookieStoreAPI); |
| |
| if (!subscription_backend_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "CookieStore backend went away"); |
| return ScriptPromise(); |
| } |
| |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| ServiceWorkerGlobalScope* scope = |
| ToServiceWorkerGlobalScope(GetExecutionContext()); |
| int64_t service_worker_registration_id = |
| scope->registration()->WebRegistration()->RegistrationId(); |
| subscription_backend_->GetSubscriptions( |
| service_worker_registration_id, |
| WTF::Bind(&CookieStore::OnGetCookieChangeSubscriptionResult, |
| WrapPersistent(resolver))); |
| return resolver->Promise(); |
| } |
| |
| void CookieStore::ContextDestroyed(ExecutionContext* execution_context) { |
| StopObserving(); |
| backend_.reset(); |
| } |
| |
| const AtomicString& CookieStore::InterfaceName() const { |
| return EventTargetNames::CookieStore; |
| } |
| |
| ExecutionContext* CookieStore::GetExecutionContext() const { |
| return ContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| void CookieStore::RemoveAllEventListeners() { |
| EventTargetWithInlineData::RemoveAllEventListeners(); |
| DCHECK(!HasEventListeners()); |
| StopObserving(); |
| } |
| |
| void CookieStore::OnCookieChange( |
| const WebCanonicalCookie& backend_cookie, |
| network::mojom::blink::CookieChangeCause change_cause) { |
| HeapVector<CookieListItem> changed, deleted; |
| CookieChangeEvent::ToEventInfo(backend_cookie, change_cause, changed, |
| deleted); |
| if (changed.IsEmpty() && deleted.IsEmpty()) { |
| // The backend only reported OVERWRITE events, which are dropped. |
| return; |
| } |
| DispatchEvent(CookieChangeEvent::Create( |
| EventTypeNames::change, std::move(changed), std::move(deleted))); |
| } |
| |
| void CookieStore::AddedEventListener( |
| const AtomicString& event_type, |
| RegisteredEventListener& registered_listener) { |
| EventTargetWithInlineData::AddedEventListener(event_type, |
| registered_listener); |
| StartObserving(); |
| } |
| |
| void CookieStore::RemovedEventListener( |
| const AtomicString& event_type, |
| const RegisteredEventListener& registered_listener) { |
| EventTargetWithInlineData::RemovedEventListener(event_type, |
| registered_listener); |
| if (!HasEventListeners()) |
| StopObserving(); |
| } |
| |
| CookieStore::CookieStore( |
| ExecutionContext* execution_context, |
| network::mojom::blink::RestrictedCookieManagerPtr backend, |
| blink::mojom::blink::CookieStorePtr subscription_backend) |
| : ContextLifecycleObserver(execution_context), |
| backend_(std::move(backend)), |
| subscription_backend_(std::move(subscription_backend)), |
| change_listener_binding_(this), |
| default_cookie_url_(DefaultCookieURL(execution_context)), |
| default_site_for_cookies_(DefaultSiteForCookies(execution_context)) { |
| DCHECK(backend_); |
| } |
| |
| ScriptPromise CookieStore::DoRead( |
| ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| DoReadBackendResultConverter backend_result_converter, |
| ExceptionState& exception_state) { |
| network::mojom::blink::CookieManagerGetOptionsPtr backend_options = |
| ToBackendOptions(options, exception_state); |
| if (backend_options.is_null()) { |
| DCHECK(exception_state.HadException()); |
| return ScriptPromise(); |
| } |
| |
| if (!backend_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "CookieStore backend went away"); |
| return ScriptPromise(); |
| } |
| |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| backend_->GetAllForUrl( |
| default_cookie_url_, default_site_for_cookies_, |
| std::move(backend_options), |
| WTF::Bind(backend_result_converter, WrapPersistent(resolver))); |
| return resolver->Promise(); |
| } |
| |
| // static |
| void CookieStore::GetAllForUrlToGetAllResult( |
| ScriptPromiseResolver* resolver, |
| const Vector<WebCanonicalCookie>& backend_cookies) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| HeapVector<CookieListItem> cookies; |
| cookies.ReserveInitialCapacity(backend_cookies.size()); |
| for (const auto& backend_cookie : backend_cookies) { |
| CookieListItem& cookie = cookies.emplace_back(); |
| CookieChangeEvent::ToCookieListItem(backend_cookie, false /* is_deleted */, |
| cookie); |
| } |
| |
| resolver->Resolve(std::move(cookies)); |
| } |
| |
| // static |
| void CookieStore::GetAllForUrlToGetResult( |
| ScriptPromiseResolver* resolver, |
| const Vector<WebCanonicalCookie>& backend_cookies) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| if (backend_cookies.IsEmpty()) { |
| resolver->Resolve(v8::Null(script_state->GetIsolate())); |
| return; |
| } |
| |
| const auto& backend_cookie = backend_cookies.front(); |
| CookieListItem cookie; |
| CookieChangeEvent::ToCookieListItem(backend_cookie, false /* is_deleted */, |
| cookie); |
| resolver->Resolve(cookie); |
| } |
| |
| ScriptPromise CookieStore::DoWrite(ScriptState* script_state, |
| const CookieStoreSetExtraOptions& options, |
| ExceptionState& exception_state) { |
| base::Optional<WebCanonicalCookie> canonical_cookie = |
| ToWebCanonicalCookie(default_cookie_url_, options, exception_state); |
| if (!canonical_cookie) { |
| DCHECK(exception_state.HadException()); |
| return ScriptPromise(); |
| } |
| |
| if (!backend_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "CookieStore backend went away"); |
| return ScriptPromise(); |
| } |
| |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| backend_->SetCanonicalCookie( |
| std::move(canonical_cookie.value()), default_cookie_url_, |
| default_site_for_cookies_, |
| WTF::Bind(&CookieStore::OnSetCanonicalCookieResult, |
| WrapPersistent(resolver))); |
| return resolver->Promise(); |
| } |
| |
| // static |
| void CookieStore::OnSetCanonicalCookieResult(ScriptPromiseResolver* resolver, |
| bool backend_success) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| if (!backend_success) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kUnknownError, |
| "An unknown error occured while writing the cookie.")); |
| return; |
| } |
| resolver->Resolve(); |
| } |
| |
| // static |
| void CookieStore::OnSubscribeToCookieChangesResult( |
| ScriptPromiseResolver* resolver, |
| bool backend_success) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| if (!backend_success) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kUnknownError, |
| "An unknown error occured while subscribing to cookie changes.")); |
| return; |
| } |
| resolver->Resolve(); |
| } |
| |
| // static |
| void CookieStore::OnGetCookieChangeSubscriptionResult( |
| ScriptPromiseResolver* resolver, |
| Vector<blink::mojom::blink::CookieChangeSubscriptionPtr> backend_result, |
| bool backend_success) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| if (!backend_success) { |
| resolver->Reject(DOMException::Create( |
| DOMExceptionCode::kUnknownError, |
| "An unknown error occured while reading cookie change subscriptions.")); |
| return; |
| } |
| |
| HeapVector<CookieStoreGetOptions> subscriptions; |
| subscriptions.ReserveInitialCapacity(backend_result.size()); |
| for (const auto& backend_subscription : backend_result) { |
| CookieStoreGetOptions& subscription = subscriptions.emplace_back(); |
| ToCookieChangeSubscription(*backend_subscription, subscription); |
| } |
| |
| resolver->Resolve(std::move(subscriptions)); |
| } |
| |
| void CookieStore::StartObserving() { |
| if (change_listener_binding_ || !backend_) |
| return; |
| |
| network::mojom::blink::CookieChangeListenerPtr change_listener; |
| change_listener_binding_.Bind(mojo::MakeRequest(&change_listener)); |
| backend_->AddChangeListener(default_cookie_url_, default_site_for_cookies_, |
| std::move(change_listener), {}); |
| } |
| |
| void CookieStore::StopObserving() { |
| if (!change_listener_binding_.is_bound()) |
| return; |
| change_listener_binding_.Close(); |
| } |
| |
| } // namespace blink |