| // 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 "modules/cookie_store/CookieStore.h" |
| |
| #include <utility> |
| |
| #include "bindings/core/v8/ScriptPromiseResolver.h" |
| #include "core/dom/DOMException.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "modules/EventModules.h" |
| #include "modules/EventTargetModules.h" |
| #include "modules/cookie_store/CookieChangeEvent.h" |
| #include "modules/cookie_store/CookieListItem.h" |
| #include "modules/cookie_store/CookieStoreGetOptions.h" |
| #include "modules/cookie_store/CookieStoreSetOptions.h" |
| #include "modules/serviceworkers/ServiceWorkerGlobalScope.h" |
| #include "platform/bindings/ScriptState.h" |
| #include "platform/heap/Handle.h" |
| #include "platform/weborigin/KURL.h" |
| #include "platform/weborigin/SecurityOrigin.h" |
| #include "platform/wtf/Functional.h" |
| #include "platform/wtf/Optional.h" |
| #include "platform/wtf/Time.h" |
| #include "platform/wtf/text/WTFString.h" |
| #include "services/network/public/mojom/restricted_cookie_manager.mojom-blink.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Returns null if and only if an exception is thrown. |
| network::mojom::blink::CookieManagerGetOptionsPtr ToBackendOptions( |
| const String& name, // Value of the "name" positional argument. |
| 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() == "startsWith") { |
| 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 (name.IsNull()) { |
| 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; |
| } |
| } else { |
| if (options.hasName()) { |
| exception_state.ThrowTypeError( |
| "Cookie name specified both as an argument and as an option"); |
| return nullptr; |
| } |
| backend_options->name = name; |
| } |
| |
| return backend_options; |
| } |
| |
| // Returns null if and only if an exception is thrown. |
| network::mojom::blink::CanonicalCookiePtr ToCanonicalCookie( |
| const KURL& cookie_url, |
| const String& name, // Value of the "name" positional argument. |
| const String& value, // Value of the "value" positional argument. |
| bool for_deletion, // True for CookieStore.delete, false for set. |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| auto canonical_cookie = network::mojom::blink::CanonicalCookie::New(); |
| |
| if (name.IsNull()) { |
| if (!options.hasName()) { |
| exception_state.ThrowTypeError("Unspecified cookie name"); |
| return nullptr; |
| } |
| canonical_cookie->name = options.name(); |
| } else { |
| if (options.hasName()) { |
| exception_state.ThrowTypeError( |
| "Cookie name specified both as an argument and as an option"); |
| return nullptr; |
| } |
| canonical_cookie->name = name; |
| } |
| |
| if (for_deletion) { |
| DCHECK(value.IsNull()); |
| if (options.hasValue()) { |
| exception_state.ThrowTypeError( |
| "Cookie value is meaningless when deleting"); |
| return nullptr; |
| } |
| canonical_cookie->value = g_empty_string; |
| |
| if (options.hasExpires()) { |
| exception_state.ThrowTypeError( |
| "Cookie expiration time is meaningless when deleting"); |
| return nullptr; |
| } |
| canonical_cookie->expiry = WTF::Time::Min(); |
| } else { |
| if (value.IsNull()) { |
| if (!options.hasValue()) { |
| exception_state.ThrowTypeError("Unspecified cookie value"); |
| return nullptr; |
| } |
| canonical_cookie->value = options.value(); |
| } else { |
| if (options.hasValue()) { |
| exception_state.ThrowTypeError( |
| "Cookie value specified both as an argument and as an option"); |
| return nullptr; |
| } |
| canonical_cookie->value = value; |
| } |
| |
| if (options.hasExpires()) |
| canonical_cookie->expiry = WTF::Time::FromJavaTime(options.expires()); |
| // The expires option is not set in CookieStoreSetOptions for session |
| // cookies. This is represented by a null expiry field in CanonicalCookie. |
| } |
| |
| if (options.hasDomain()) { |
| // TODO(crbug.com/729800): Checks and exception throwing. |
| canonical_cookie->domain = options.domain(); |
| } else { |
| // TODO(crbug.com/729800): Correct value? |
| canonical_cookie->domain = cookie_url.Host(); |
| } |
| |
| if (options.hasPath()) { |
| canonical_cookie->path = options.path(); |
| } else { |
| canonical_cookie->path = String("/"); |
| } |
| |
| bool is_secure_origin = SecurityOrigin::IsSecure(cookie_url); |
| if (options.hasSecure()) { |
| canonical_cookie->secure = options.secure(); |
| } else { |
| canonical_cookie->secure = is_secure_origin; |
| } |
| |
| if (name.StartsWith("__Secure-") || name.StartsWith("__Host-")) { |
| if (!canonical_cookie->secure) { |
| exception_state.ThrowTypeError( |
| "__Secure- and __Host- cookies must be secure"); |
| return nullptr; |
| } |
| if (!is_secure_origin) { |
| exception_state.ThrowTypeError( |
| "__Secure- and __Host- cookies must be written from secure origin"); |
| return nullptr; |
| } |
| } |
| |
| canonical_cookie->httponly = options.httpOnly(); |
| return canonical_cookie; |
| } |
| |
| void ToCookieListItem( |
| const network::mojom::blink::CanonicalCookiePtr& canonical_cookie, |
| bool is_deleted, // True for the information from a cookie deletion event. |
| CookieListItem& cookie) { |
| cookie.setName(canonical_cookie->name); |
| if (!is_deleted) |
| cookie.setValue(canonical_cookie->value); |
| } |
| |
| 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(); |
| } |
| |
| const 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(); |
| } |
| |
| } // anonymous namespace |
| |
| CookieStore::~CookieStore() = default; |
| |
| ScriptPromise CookieStore::getAll(ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return getAll(script_state, WTF::String(), options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::getAll(ScriptState* script_state, |
| const String& name, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return DoRead(script_state, name, options, |
| &CookieStore::GetAllForUrlToGetAllResult, exception_state); |
| } |
| |
| ScriptPromise CookieStore::get(ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return get(script_state, WTF::String(), options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::get(ScriptState* script_state, |
| const String& name, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return DoRead(script_state, name, options, |
| &CookieStore::GetAllForUrlToGetResult, exception_state); |
| } |
| |
| ScriptPromise CookieStore::has(ScriptState* script_state, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return has(script_state, WTF::String(), options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::has(ScriptState* script_state, |
| const String& name, |
| const CookieStoreGetOptions& options, |
| ExceptionState& exception_state) { |
| return DoRead(script_state, name, options, |
| &CookieStore::GetAllForUrlToHasResult, exception_state); |
| } |
| |
| ScriptPromise CookieStore::set(ScriptState* script_state, |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| return set(script_state, WTF::String(), WTF::String(), options, |
| exception_state); |
| } |
| |
| ScriptPromise CookieStore::set(ScriptState* script_state, |
| const String& name, |
| const String& value, |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| return DoWrite(script_state, name, value, options, false /* is_deletion */, |
| exception_state); |
| } |
| |
| ScriptPromise CookieStore::Delete(ScriptState* script_state, |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| return Delete(script_state, WTF::String(), options, exception_state); |
| } |
| |
| ScriptPromise CookieStore::Delete(ScriptState* script_state, |
| const String& name, |
| const CookieStoreSetOptions& options, |
| ExceptionState& exception_state) { |
| return DoWrite(script_state, name, WTF::String(), options, |
| true /* is_deletion */, exception_state); |
| } |
| |
| 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( |
| network::mojom::blink::CanonicalCookiePtr backend_cookie, |
| network::mojom::blink::CookieChangeCause change_cause) { |
| HeapVector<CookieListItem> changed, deleted; |
| |
| switch (change_cause) { |
| case ::network::mojom::blink::CookieChangeCause::INSERTED: |
| case ::network::mojom::blink::CookieChangeCause::EXPLICIT: { |
| CookieListItem& cookie = changed.emplace_back(); |
| ToCookieListItem(backend_cookie, false /* is_deleted */, cookie); |
| break; |
| } |
| case ::network::mojom::blink::CookieChangeCause::UNKNOWN_DELETION: |
| case ::network::mojom::blink::CookieChangeCause::EXPIRED: |
| case ::network::mojom::blink::CookieChangeCause::EVICTED: |
| case ::network::mojom::blink::CookieChangeCause::EXPIRED_OVERWRITE: { |
| CookieListItem& cookie = deleted.emplace_back(); |
| ToCookieListItem(backend_cookie, true /* is_deleted */, cookie); |
| break; |
| } |
| |
| case ::network::mojom::blink::CookieChangeCause::OVERWRITE: |
| // A cookie overwrite causes an OVERWRITE (meaning the old cookie was |
| // deleted) and an INSERTED. |
| break; |
| } |
| |
| 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) |
| : ContextLifecycleObserver(execution_context), |
| backend_(std::move(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 String& name, |
| const CookieStoreGetOptions& options, |
| DoReadBackendResultConverter backend_result_converter, |
| ExceptionState& exception_state) { |
| network::mojom::blink::CookieManagerGetOptionsPtr backend_options = |
| ToBackendOptions(name, options, exception_state); |
| if (backend_options.is_null()) |
| return ScriptPromise(); // ToBackendOptions has thrown an exception. |
| |
| if (!backend_) { |
| exception_state.ThrowDOMException(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, |
| Vector<network::mojom::blink::CanonicalCookiePtr> backend_cookies) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| HeapVector<CookieListItem> cookies; |
| cookies.ReserveInitialCapacity(backend_cookies.size()); |
| for (const auto& canonical_cookie : backend_cookies) { |
| CookieListItem& cookie = cookies.emplace_back(); |
| ToCookieListItem(canonical_cookie, false /* is_deleted */, cookie); |
| } |
| |
| resolver->Resolve(std::move(cookies)); |
| } |
| |
| // static |
| void CookieStore::GetAllForUrlToGetResult( |
| ScriptPromiseResolver* resolver, |
| Vector<network::mojom::blink::CanonicalCookiePtr> 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& canonical_cookie = backend_cookies.front(); |
| CookieListItem cookie; |
| ToCookieListItem(canonical_cookie, false /* is_deleted */, cookie); |
| resolver->Resolve(cookie); |
| } |
| |
| // static |
| void CookieStore::GetAllForUrlToHasResult( |
| ScriptPromiseResolver* resolver, |
| Vector<network::mojom::blink::CanonicalCookiePtr> backend_cookies) { |
| ScriptState* script_state = resolver->GetScriptState(); |
| if (!script_state->ContextIsValid()) |
| return; |
| |
| resolver->Resolve(!backend_cookies.IsEmpty()); |
| } |
| |
| ScriptPromise CookieStore::DoWrite(ScriptState* script_state, |
| const String& name, |
| const String& value, |
| const CookieStoreSetOptions& options, |
| bool is_deletion, |
| ExceptionState& exception_state) { |
| network::mojom::blink::CanonicalCookiePtr canonical_cookie = |
| ToCanonicalCookie(default_cookie_url_, name, value, is_deletion, options, |
| exception_state); |
| if (canonical_cookie.is_null()) |
| return ScriptPromise(); // ToCanonicalCookie has thrown an exception. |
| |
| if (!backend_) { |
| exception_state.ThrowDOMException(kInvalidStateError, |
| "CookieStore backend went away"); |
| return ScriptPromise(); |
| } |
| |
| ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state); |
| backend_->SetCanonicalCookie( |
| std::move(canonical_cookie), 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( |
| kUnknownError, "An unknown error occured while writing the cookie.")); |
| return; |
| } |
| resolver->Resolve(); |
| } |
| |
| 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 |