| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Implements the Chrome Extensions Cookies API. |
| |
| #include "chrome/browser/extensions/api/cookies/cookies_api.h" |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_writer.h" |
| #include "base/lazy_instance.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/extensions/api/cookies/cookies_helpers.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/window_controller_list.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/extensions/api/cookies.h" |
| #include "components/safe_browsing/buildflags.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_api_frame_id_map.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/stack_frame.h" |
| #include "net/cookies/canonical_cookie.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(SAFE_BROWSING_AVAILABLE) |
| #include "chrome/browser/safe_browsing/extension_telemetry/cookies_get_all_signal.h" |
| #include "chrome/browser/safe_browsing/extension_telemetry/cookies_get_signal.h" |
| #include "chrome/browser/safe_browsing/extension_telemetry/extension_telemetry_service.h" |
| #include "chrome/browser/safe_browsing/extension_telemetry/extension_telemetry_service_factory.h" |
| #endif |
| |
| using content::BrowserThread; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Keys |
| constexpr char kCauseKey[] = "cause"; |
| constexpr char kCookieKey[] = "cookie"; |
| constexpr char kRemovedKey[] = "removed"; |
| |
| // Cause Constants |
| constexpr char kEvictedChangeCause[] = "evicted"; |
| constexpr char kExpiredChangeCause[] = "expired"; |
| constexpr char kExpiredOverwriteChangeCause[] = "expired_overwrite"; |
| constexpr char kExplicitChangeCause[] = "explicit"; |
| constexpr char kOverwriteChangeCause[] = "overwrite"; |
| |
| // Errors |
| constexpr char kCookieSetFailedError[] = |
| "Failed to parse or set cookie named \"*\"."; |
| constexpr char kInvalidStoreIdError[] = "Invalid cookie store id: \"*\"."; |
| constexpr char kInvalidUrlError[] = "Invalid url: \"*\"."; |
| constexpr char kNoHostPermissionsError[] = |
| "No host permissions for cookies at url: \"*\"."; |
| |
| bool CheckHostPermissions(const Extension* extension, |
| const GURL& url, |
| std::string* error) { |
| if (!extension->permissions_data()->HasHostPermission(url)) { |
| *error = |
| ErrorUtils::FormatErrorMessage(kNoHostPermissionsError, url.spec()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool ParseUrl(const Extension* extension, |
| const std::string& url_string, |
| GURL* url, |
| bool check_host_permissions, |
| std::string* error) { |
| *url = GURL(url_string); |
| if (!url->is_valid()) { |
| *error = ErrorUtils::FormatErrorMessage(kInvalidUrlError, url_string); |
| return false; |
| } |
| // Check against host permissions if needed. |
| if (check_host_permissions && !CheckHostPermissions(extension, *url, error)) { |
| return false; |
| } |
| return true; |
| } |
| |
| network::mojom::CookieManager* ParseStoreCookieManager( |
| content::BrowserContext* function_context, |
| bool include_incognito, |
| std::string* store_id, |
| std::string* error) { |
| Profile* function_profile = Profile::FromBrowserContext(function_context); |
| Profile* store_profile = nullptr; |
| if (!store_id->empty()) { |
| store_profile = cookies_helpers::ChooseProfileFromStoreId( |
| *store_id, function_profile, include_incognito); |
| if (!store_profile) { |
| *error = ErrorUtils::FormatErrorMessage(kInvalidStoreIdError, *store_id); |
| return nullptr; |
| } |
| } else { |
| store_profile = function_profile; |
| *store_id = cookies_helpers::GetStoreIdFromProfile(store_profile); |
| } |
| |
| return store_profile->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess(); |
| } |
| |
| } // namespace |
| |
| CookiesEventRouter::CookieChangeListener::CookieChangeListener( |
| CookiesEventRouter* router, |
| bool otr) |
| : router_(router), otr_(otr) {} |
| CookiesEventRouter::CookieChangeListener::~CookieChangeListener() = default; |
| |
| void CookiesEventRouter::CookieChangeListener::OnCookieChange( |
| const net::CookieChangeInfo& change) { |
| router_->OnCookieChange(otr_, change); |
| } |
| |
| CookiesEventRouter::CookiesEventRouter(content::BrowserContext* context) |
| : profile_(Profile::FromBrowserContext(context)), |
| profile_observation_(this), |
| otr_profile_observation_(this) { |
| MaybeStartListening(); |
| profile_observation_.Observe(profile_); |
| } |
| |
| CookiesEventRouter::~CookiesEventRouter() = default; |
| |
| void CookiesEventRouter::OnCookieChange(bool otr, |
| const net::CookieChangeInfo& change) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // There is no way to represent non-serializable |
| // partition keys in JS so return to prevent a crash. |
| if (change.cookie.IsPartitioned() && |
| !change.cookie.PartitionKey()->IsSerializeable()) { |
| return; |
| } |
| base::Value::List args; |
| base::Value::Dict dict; |
| dict.Set( |
| kRemovedKey, |
| change.cause != net::CookieChangeCause::INSERTED && |
| change.cause != net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE); |
| |
| Profile* profile = |
| otr ? profile_->GetPrimaryOTRProfile(/*create_if_needed=*/false) |
| : profile_->GetOriginalProfile(); |
| // TODO(407373848): OTR profile must exist when the cookie change event |
| // arrived. |
| CHECK(profile); |
| |
| api::cookies::Cookie cookie = cookies_helpers::CreateCookie( |
| change.cookie, cookies_helpers::GetStoreIdFromProfile(profile)); |
| dict.Set(kCookieKey, cookie.ToValue()); |
| |
| // Map the internal cause to an external string. |
| std::string cause_dict_entry; |
| switch (change.cause) { |
| // Report an inserted cookie as an "explicit" change cause. All other causes |
| // only make sense for deletions. |
| case net::CookieChangeCause::INSERTED: |
| case net::CookieChangeCause::EXPLICIT: |
| case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE: |
| cause_dict_entry = kExplicitChangeCause; |
| break; |
| |
| case net::CookieChangeCause::OVERWRITE: |
| cause_dict_entry = kOverwriteChangeCause; |
| break; |
| |
| case net::CookieChangeCause::EXPIRED: |
| cause_dict_entry = kExpiredChangeCause; |
| break; |
| |
| case net::CookieChangeCause::EVICTED: |
| cause_dict_entry = kEvictedChangeCause; |
| break; |
| |
| case net::CookieChangeCause::EXPIRED_OVERWRITE: |
| cause_dict_entry = kExpiredOverwriteChangeCause; |
| break; |
| |
| case net::CookieChangeCause::UNKNOWN_DELETION: |
| NOTREACHED(); |
| } |
| dict.Set(kCauseKey, cause_dict_entry); |
| |
| args.Append(std::move(dict)); |
| |
| DispatchEvent(profile, events::COOKIES_ON_CHANGED, |
| api::cookies::OnChanged::kEventName, std::move(args), |
| cookies_helpers::GetURLFromCanonicalCookie(change.cookie)); |
| } |
| |
| void CookiesEventRouter::OnOffTheRecordProfileCreated(Profile* off_the_record) { |
| // When an off-the-record spinoff of |profile_| is created, start listening |
| // for cookie changes there. The OTR receiver should never be bound, since |
| // there wasn't previously an OTR profile. |
| // TODO(crbug.com/417228685): Clank allows for multiple OTR profiles, unlike |
| // desktop Chrome. Extensions APIs may have built-in assumptions that there |
| // will only be one OTR profile. We need to determine how this will be handled |
| // in Desktop Android. |
| if (!off_the_record->IsPrimaryOTRProfile()) { |
| return; |
| } |
| |
| DCHECK(!otr_receiver_.is_bound()); |
| otr_profile_observation_.Observe(off_the_record); |
| BindToCookieManager(&otr_receiver_, off_the_record); |
| } |
| |
| void CookiesEventRouter::OnProfileWillBeDestroyed(Profile* profile) { |
| Profile* original_profile = profile_->GetOriginalProfile(); |
| Profile* otr_profile = |
| original_profile->HasPrimaryOTRProfile() |
| ? original_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true) |
| : nullptr; |
| if (profile == otr_profile) { |
| otr_profile_observation_.Reset(); |
| otr_receiver_.reset(); |
| } |
| } |
| |
| void CookiesEventRouter::MaybeStartListening() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| |
| Profile* original_profile = profile_->GetOriginalProfile(); |
| Profile* otr_profile = |
| original_profile->HasPrimaryOTRProfile() |
| ? original_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true) |
| : nullptr; |
| |
| if (!receiver_.is_bound()) { |
| BindToCookieManager(&receiver_, original_profile); |
| } |
| |
| if (!otr_receiver_.is_bound() && otr_profile) { |
| otr_profile_observation_.Observe(otr_profile); |
| BindToCookieManager(&otr_receiver_, otr_profile); |
| } |
| } |
| |
| void CookiesEventRouter::BindToCookieManager( |
| mojo::Receiver<network::mojom::CookieChangeListener>* receiver, |
| Profile* profile) { |
| network::mojom::CookieManager* cookie_manager = |
| profile->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess(); |
| if (!cookie_manager) |
| return; |
| |
| cookie_manager->AddGlobalChangeListener(receiver->BindNewPipeAndPassRemote()); |
| receiver->set_disconnect_handler( |
| base::BindOnce(&CookiesEventRouter::OnConnectionError, |
| base::Unretained(this), receiver)); |
| } |
| |
| void CookiesEventRouter::OnConnectionError( |
| mojo::Receiver<network::mojom::CookieChangeListener>* receiver) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| receiver->reset(); |
| MaybeStartListening(); |
| } |
| |
| void CookiesEventRouter::DispatchEvent(content::BrowserContext* context, |
| events::HistogramValue histogram_value, |
| const std::string& event_name, |
| base::Value::List event_args, |
| const GURL& cookie_domain) { |
| EventRouter* router = context ? EventRouter::Get(context) : nullptr; |
| if (!router) |
| return; |
| auto event = std::make_unique<Event>(histogram_value, event_name, |
| std::move(event_args), context); |
| event->event_url = cookie_domain; |
| router->BroadcastEvent(std::move(event)); |
| } |
| |
| CookiesGetFunction::CookiesGetFunction() = default; |
| CookiesGetFunction::~CookiesGetFunction() = default; |
| |
| ExtensionFunction::ResponseAction CookiesGetFunction::Run() { |
| parsed_args_ = api::cookies::Get::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(parsed_args_); |
| |
| // Read/validate input parameters. |
| std::string error; |
| if (!ParseUrl(extension(), parsed_args_->details.url, &url_, true, &error)) |
| return RespondNow(Error(std::move(error))); |
| |
| std::string store_id = parsed_args_->details.store_id.value_or(std::string()); |
| network::mojom::CookieManager* cookie_manager = ParseStoreCookieManager( |
| browser_context(), include_incognito_information(), &store_id, &error); |
| if (!cookie_manager) |
| return RespondNow(Error(std::move(error))); |
| |
| if (parsed_args_->details.partition_key.has_value() && |
| !parsed_args_->details.partition_key->has_cross_site_ancestor |
| .has_value() && |
| parsed_args_->details.partition_key->top_level_site.has_value()) { |
| base::expected<bool, std::string> cross_site_ancestor = |
| cookies_helpers::CalculateHasCrossSiteAncestor( |
| parsed_args_->details.url, parsed_args_->details.partition_key); |
| if (!cross_site_ancestor.has_value()) { |
| return RespondNow(Error(std::move(cross_site_ancestor.error()))); |
| } |
| parsed_args_->details.partition_key->has_cross_site_ancestor = |
| cross_site_ancestor.value(); |
| } |
| |
| base::expected<std::optional<net::CookiePartitionKey>, std::string> |
| partition_key = cookies_helpers::ToNetCookiePartitionKey( |
| parsed_args_->details.partition_key); |
| if (!partition_key.has_value()) { |
| return RespondNow(Error(std::move(partition_key.error()))); |
| } |
| |
| if (!parsed_args_->details.store_id) |
| parsed_args_->details.store_id = store_id; |
| |
| DCHECK(!url_.is_empty() && url_.is_valid()); |
| cookies_helpers::GetCookieListFromManager( |
| cookie_manager, url_, |
| net::CookiePartitionKeyCollection(std::move(partition_key).value()), |
| base::BindOnce(&CookiesGetFunction::GetCookieListCallback, this)); |
| |
| // Extension telemetry signal intercept |
| NotifyExtensionTelemetry(); |
| |
| // Will finish asynchronously. |
| return RespondLater(); |
| } |
| |
| void CookiesGetFunction::GetCookieListCallback( |
| const net::CookieAccessResultList& cookie_list, |
| const net::CookieAccessResultList& excluded_cookies) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| for (const net::CookieWithAccessResult& cookie_with_access_result : |
| cookie_list) { |
| if (!cookies_helpers:: |
| CanonicalCookiePartitionKeyMatchesApiCookiePartitionKey( |
| parsed_args_->details.partition_key, |
| cookie_with_access_result.cookie.PartitionKey())) { |
| continue; |
| } |
| |
| // Return the first matching cookie. Relies on the fact that the |
| // CookieManager interface returns them in canonical order (longest path, |
| // then earliest creation time). |
| if (cookie_with_access_result.cookie.Name() == parsed_args_->details.name) { |
| api::cookies::Cookie api_cookie = cookies_helpers::CreateCookie( |
| cookie_with_access_result.cookie, *parsed_args_->details.store_id); |
| Respond(ArgumentList(api::cookies::Get::Results::Create(api_cookie))); |
| return; |
| } |
| } |
| |
| // The cookie doesn't exist; return null. |
| Respond(WithArguments(base::Value())); |
| } |
| |
| void CookiesGetFunction::NotifyExtensionTelemetry() { |
| // TODO(crbug.com/371423073): Support telemetry on Android. |
| #if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(SAFE_BROWSING_AVAILABLE) |
| auto* telemetry_service = |
| safe_browsing::ExtensionTelemetryServiceFactory::GetForProfile( |
| Profile::FromBrowserContext(browser_context())); |
| |
| if (!telemetry_service || !telemetry_service->enabled()) { |
| return; |
| } |
| |
| auto cookies_get_signal = std::make_unique<safe_browsing::CookiesGetSignal>( |
| extension_id(), parsed_args_->details.name, |
| parsed_args_->details.store_id.value_or(std::string()), |
| parsed_args_->details.url, js_callstack().value_or(StackTrace())); |
| telemetry_service->AddSignal(std::move(cookies_get_signal)); |
| #endif |
| } |
| |
| CookiesGetAllFunction::CookiesGetAllFunction() = default; |
| |
| CookiesGetAllFunction::~CookiesGetAllFunction() = default; |
| |
| ExtensionFunction::ResponseAction CookiesGetAllFunction::Run() { |
| parsed_args_ = api::cookies::GetAll::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(parsed_args_); |
| |
| std::string error; |
| if (parsed_args_->details.url && |
| !ParseUrl(extension(), *parsed_args_->details.url, &url_, false, |
| &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| std::string store_id = parsed_args_->details.store_id.value_or(std::string()); |
| network::mojom::CookieManager* cookie_manager = ParseStoreCookieManager( |
| browser_context(), include_incognito_information(), &store_id, &error); |
| if (!cookie_manager) |
| return RespondNow(Error(std::move(error))); |
| |
| // make sure user input is valid |
| base::expected<std::optional<net::CookiePartitionKey>, std::string> |
| partition_key = cookies_helpers::ToNetCookiePartitionKey( |
| parsed_args_->details.partition_key); |
| if (!partition_key.has_value()) { |
| return RespondNow(Error(std::move(partition_key.error()))); |
| } |
| |
| if (!parsed_args_->details.store_id) |
| parsed_args_->details.store_id = store_id; |
| |
| net::CookiePartitionKeyCollection cookie_partition_key_collection = |
| cookies_helpers::CookiePartitionKeyCollectionFromApiPartitionKey( |
| parsed_args_->details.partition_key); |
| |
| DCHECK(url_.is_empty() || url_.is_valid()); |
| if (url_.is_empty()) { |
| cookies_helpers::GetAllCookiesFromManager( |
| cookie_manager, |
| base::BindOnce(&CookiesGetAllFunction::GetAllCookiesCallback, this)); |
| } else { |
| cookies_helpers::GetCookieListFromManager( |
| cookie_manager, url_, cookie_partition_key_collection, |
| base::BindOnce(&CookiesGetAllFunction::GetCookieListCallback, this)); |
| } |
| |
| // Extension telemetry signal intercept |
| NotifyExtensionTelemetry(); |
| |
| return RespondLater(); |
| } |
| |
| void CookiesGetAllFunction::GetAllCookiesCallback( |
| const net::CookieList& cookie_list) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (extension()) { |
| net::CookiePartitionKeyCollection cookie_partition_key_collection = |
| cookies_helpers::CookiePartitionKeyCollectionFromApiPartitionKey( |
| parsed_args_->details.partition_key); |
| std::vector<api::cookies::Cookie> match_vector; |
| cookies_helpers::AppendMatchingCookiesFromCookieListToVector( |
| cookie_list, &parsed_args_->details, extension(), &match_vector, |
| cookie_partition_key_collection); |
| |
| Respond(ArgumentList(api::cookies::GetAll::Results::Create(match_vector))); |
| } else { |
| // TODO(devlin): When can |extension()| be null for this function? |
| Respond(NoArguments()); |
| } |
| } |
| |
| void CookiesGetAllFunction::GetCookieListCallback( |
| const net::CookieAccessResultList& cookie_list, |
| const net::CookieAccessResultList& excluded_cookies) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (extension()) { |
| std::vector<api::cookies::Cookie> match_vector; |
| cookies_helpers::AppendMatchingCookiesFromCookieAccessResultListToVector( |
| cookie_list, &parsed_args_->details, extension(), &match_vector); |
| |
| Respond(ArgumentList(api::cookies::GetAll::Results::Create(match_vector))); |
| } else { |
| // TODO(devlin): When can |extension()| be null for this function? |
| Respond(NoArguments()); |
| } |
| } |
| |
| void CookiesGetAllFunction::NotifyExtensionTelemetry() { |
| // TODO(crbug.com/371423073): Support telemetry on Android. |
| #if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(SAFE_BROWSING_AVAILABLE) |
| auto* telemetry_service = |
| safe_browsing::ExtensionTelemetryServiceFactory::GetForProfile( |
| Profile::FromBrowserContext(browser_context())); |
| |
| if (!telemetry_service || !telemetry_service->enabled()) { |
| return; |
| } |
| |
| auto cookies_get_all_signal = |
| std::make_unique<safe_browsing::CookiesGetAllSignal>( |
| extension_id(), parsed_args_->details.domain.value_or(std::string()), |
| parsed_args_->details.name.value_or(std::string()), |
| parsed_args_->details.path.value_or(std::string()), |
| parsed_args_->details.secure, |
| parsed_args_->details.store_id.value_or(std::string()), |
| parsed_args_->details.url.value_or(std::string()), |
| parsed_args_->details.session, js_callstack().value_or(StackTrace())); |
| telemetry_service->AddSignal(std::move(cookies_get_all_signal)); |
| #endif |
| } |
| |
| CookiesSetFunction::CookiesSetFunction() |
| : state_(NO_RESPONSE), success_(false) {} |
| |
| CookiesSetFunction::~CookiesSetFunction() = default; |
| |
| ExtensionFunction::ResponseAction CookiesSetFunction::Run() { |
| parsed_args_ = api::cookies::Set::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(parsed_args_); |
| |
| // Read/validate input parameters. |
| std::string error; |
| if (!ParseUrl(extension(), parsed_args_->details.url, &url_, true, &error)) |
| return RespondNow(Error(std::move(error))); |
| |
| std::string store_id = parsed_args_->details.store_id.value_or(std::string()); |
| network::mojom::CookieManager* cookie_manager = ParseStoreCookieManager( |
| browser_context(), include_incognito_information(), &store_id, &error); |
| if (!cookie_manager) |
| return RespondNow(Error(std::move(error))); |
| |
| // cookies.set api allows for an partitionKey with a `top_level_site` present |
| // but no value for `has_cross_site_ancestor`. If that is the case, the |
| // browser will calculate the value for `has_cross_site_ancestor`. |
| std::optional<extensions::api::cookies::CookiePartitionKey> api_partition_key; |
| if (parsed_args_->details.partition_key.has_value()) { |
| api_partition_key = parsed_args_->details.partition_key->Clone(); |
| if (!api_partition_key->has_cross_site_ancestor.has_value() && |
| api_partition_key->top_level_site.has_value()) { |
| base::expected<bool, std::string> cross_site_ancestor = |
| cookies_helpers::CalculateHasCrossSiteAncestor( |
| parsed_args_->details.url, api_partition_key); |
| if (!cross_site_ancestor.has_value()) { |
| return RespondNow(Error(std::move(cross_site_ancestor.error()))); |
| } |
| api_partition_key->has_cross_site_ancestor = cross_site_ancestor.value(); |
| } |
| } |
| |
| if (!cookies_helpers::ValidateCrossSiteAncestor(parsed_args_->details.url, |
| api_partition_key, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| base::expected<std::optional<net::CookiePartitionKey>, std::string> |
| net_partition_key = |
| cookies_helpers::ToNetCookiePartitionKey(api_partition_key); |
| if (!net_partition_key.has_value()) { |
| return RespondNow(Error(std::move(net_partition_key.error()))); |
| } |
| |
| if (!parsed_args_->details.store_id) |
| parsed_args_->details.store_id = store_id; |
| |
| base::Time expiration_time; |
| if (parsed_args_->details.expiration_date) { |
| // Time::FromSecondsSinceUnixEpoch converts double time 0 to empty Time |
| // object. So we need to do special handling here. |
| expiration_time = (*parsed_args_->details.expiration_date == 0) |
| ? base::Time::UnixEpoch() |
| : base::Time::FromSecondsSinceUnixEpoch( |
| *parsed_args_->details.expiration_date); |
| } |
| |
| net::CookieSameSite same_site = net::CookieSameSite::UNSPECIFIED; |
| switch (parsed_args_->details.same_site) { |
| case api::cookies::SameSiteStatus::kNoRestriction: |
| same_site = net::CookieSameSite::NO_RESTRICTION; |
| break; |
| case api::cookies::SameSiteStatus::kLax: |
| same_site = net::CookieSameSite::LAX_MODE; |
| break; |
| case api::cookies::SameSiteStatus::kStrict: |
| same_site = net::CookieSameSite::STRICT_MODE; |
| break; |
| // This is the case if the optional sameSite property is given as |
| // "unspecified": |
| case api::cookies::SameSiteStatus::kUnspecified: |
| // This is the case if the optional sameSite property is left out: |
| case api::cookies::SameSiteStatus::kNone: |
| same_site = net::CookieSameSite::UNSPECIFIED; |
| break; |
| } |
| |
| std::unique_ptr<net::CanonicalCookie> cc( |
| net::CanonicalCookie::CreateSanitizedCookie( |
| url_, // |
| parsed_args_->details.name.value_or(std::string()), // |
| parsed_args_->details.value.value_or(std::string()), // |
| parsed_args_->details.domain.value_or(std::string()), // |
| parsed_args_->details.path.value_or(std::string()), // |
| /*creation_time=*/base::Time(), // |
| expiration_time, // |
| /*last_access_time=*/base::Time(), // |
| parsed_args_->details.secure.value_or(false), // |
| parsed_args_->details.http_only.value_or(false), // |
| same_site, // |
| net::COOKIE_PRIORITY_DEFAULT, // |
| net_partition_key.value(), // |
| /*status=*/nullptr)); |
| if (!cc) { |
| // Return error through callbacks so that the proper error message |
| // is generated. |
| success_ = false; |
| state_ = SET_COMPLETED; |
| GetCookieListCallback(net::CookieAccessResultList(), |
| net::CookieAccessResultList()); |
| return AlreadyResponded(); |
| } |
| |
| // Dispatch the setter, immediately followed by the getter. This |
| // plus FIFO ordering on the cookie_manager_ pipe means that no |
| // other extension function will affect the get result. |
| net::CookieOptions options; |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| DCHECK(!url_.is_empty() && url_.is_valid()); |
| cookie_manager->SetCanonicalCookie( |
| *cc, url_, options, |
| base::BindOnce(&CookiesSetFunction::SetCanonicalCookieCallback, this)); |
| cookies_helpers::GetCookieListFromManager( |
| cookie_manager, url_, |
| net::CookiePartitionKeyCollection(std::move(net_partition_key).value()), |
| base::BindOnce(&CookiesSetFunction::GetCookieListCallback, this)); |
| |
| // Will finish asynchronously. |
| return RespondLater(); |
| } |
| |
| void CookiesSetFunction::SetCanonicalCookieCallback( |
| net::CookieAccessResult set_cookie_result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK_EQ(NO_RESPONSE, state_); |
| state_ = SET_COMPLETED; |
| success_ = set_cookie_result.status.IsInclude(); |
| } |
| |
| void CookiesSetFunction::GetCookieListCallback( |
| const net::CookieAccessResultList& cookie_list, |
| const net::CookieAccessResultList& excluded_cookies) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK_EQ(SET_COMPLETED, state_); |
| state_ = GET_COMPLETED; |
| |
| if (!success_) { |
| std::string name = parsed_args_->details.name.value_or(std::string()); |
| Respond(Error(ErrorUtils::FormatErrorMessage(kCookieSetFailedError, name))); |
| return; |
| } |
| |
| std::optional<ResponseValue> value; |
| for (const net::CookieWithAccessResult& cookie_with_access_result : |
| cookie_list) { |
| // Return the first matching cookie. Relies on the fact that the |
| // CookieMonster returns them in canonical order (longest path, then |
| // earliest creation time). |
| |
| if (!extensions::cookies_helpers:: |
| CanonicalCookiePartitionKeyMatchesApiCookiePartitionKey( |
| parsed_args_->details.partition_key, |
| cookie_with_access_result.cookie.PartitionKey())) { |
| continue; |
| } |
| |
| std::string name = parsed_args_->details.name.value_or(std::string()); |
| |
| if (cookie_with_access_result.cookie.Name() == name) { |
| api::cookies::Cookie api_cookie = cookies_helpers::CreateCookie( |
| cookie_with_access_result.cookie, *parsed_args_->details.store_id); |
| value.emplace( |
| ArgumentList(api::cookies::Set::Results::Create(api_cookie))); |
| break; |
| } |
| } |
| |
| Respond(value ? std::move(*value) : NoArguments()); |
| } |
| |
| CookiesRemoveFunction::CookiesRemoveFunction() = default; |
| |
| CookiesRemoveFunction::~CookiesRemoveFunction() = default; |
| |
| ExtensionFunction::ResponseAction CookiesRemoveFunction::Run() { |
| parsed_args_ = api::cookies::Remove::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(parsed_args_); |
| |
| // Read/validate input parameters. |
| std::string error; |
| if (!ParseUrl(extension(), parsed_args_->details.url, &url_, true, &error)) |
| return RespondNow(Error(std::move(error))); |
| |
| std::string store_id = parsed_args_->details.store_id.value_or(std::string()); |
| network::mojom::CookieManager* cookie_manager = ParseStoreCookieManager( |
| browser_context(), include_incognito_information(), &store_id, &error); |
| if (!cookie_manager) |
| return RespondNow(Error(std::move(error))); |
| |
| base::expected<std::optional<net::CookiePartitionKey>, std::string> |
| partition_key = cookies_helpers::ToNetCookiePartitionKey( |
| parsed_args_->details.partition_key); |
| if (!partition_key.has_value()) { |
| return RespondNow(Error(std::move(partition_key.error()))); |
| } |
| |
| if (!parsed_args_->details.store_id) |
| parsed_args_->details.store_id = store_id; |
| |
| network::mojom::CookieDeletionFilterPtr filter( |
| network::mojom::CookieDeletionFilter::New()); |
| |
| filter->cookie_partition_key_collection = |
| net::CookiePartitionKeyCollection(std::move(partition_key).value()); |
| filter->url = url_; |
| filter->cookie_name = parsed_args_->details.name; |
| cookie_manager->DeleteCookies( |
| std::move(filter), |
| base::BindOnce(&CookiesRemoveFunction::RemoveCookieCallback, this)); |
| |
| // Will return asynchronously. |
| return RespondLater(); |
| } |
| |
| void CookiesRemoveFunction::RemoveCookieCallback(uint32_t /* num_deleted */) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Build the callback result |
| api::cookies::Remove::Results::Details details; |
| details.name = parsed_args_->details.name; |
| details.url = url_.spec(); |
| details.store_id = *parsed_args_->details.store_id; |
| if (parsed_args_->details.partition_key) { |
| details.partition_key = parsed_args_->details.partition_key->Clone(); |
| } |
| |
| Respond(ArgumentList(api::cookies::Remove::Results::Create(details))); |
| } |
| |
| CookiesGetPartitionKeyFunction::CookiesGetPartitionKeyFunction() = default; |
| |
| CookiesGetPartitionKeyFunction::~CookiesGetPartitionKeyFunction() = default; |
| |
| ExtensionFunction::ResponseAction CookiesGetPartitionKeyFunction::Run() { |
| parsed_args_ = api::cookies::GetPartitionKey::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(parsed_args_); |
| |
| content::RenderFrameHost* render_frame_host = nullptr; |
| content::WebContents* web_contents = nullptr; |
| std::optional<int> frame_id = parsed_args_->details.frame_id; |
| std::optional<ExtensionApiFrameIdMap::DocumentId> document_id; |
| std::optional<int> tab_id = parsed_args_->details.tab_id; |
| |
| if (parsed_args_->details.document_id.has_value()) { |
| document_id = ExtensionApiFrameIdMap::DocumentIdFromString( |
| *parsed_args_->details.document_id); |
| render_frame_host = |
| ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId( |
| document_id.value()); |
| web_contents = content::WebContents::FromRenderFrameHost(render_frame_host); |
| } else if (tab_id.has_value()) { |
| if (!frame_id.has_value()) { |
| // Default to main frame if no frame is provided. |
| frame_id = 0; |
| } |
| |
| if (!ExtensionTabUtil::GetTabById(tab_id.value(), browser_context(), |
| include_incognito_information(), |
| &web_contents) || |
| !web_contents) { |
| return RespondNow(Error("Invalid `tabId`.")); |
| } |
| render_frame_host = ExtensionApiFrameIdMap::GetRenderFrameHostById( |
| web_contents, frame_id.value()); |
| } else if (frame_id.has_value()) { |
| if (frame_id.value() == 0) { |
| return RespondNow( |
| Error("`frameId` may not be 0 if no `tabId` is present.")); |
| } |
| |
| render_frame_host = |
| ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByFrameId( |
| frame_id.value()); |
| } else { |
| return RespondNow( |
| Error("Either `documentId` or `tabId` must be specified.")); |
| } |
| |
| if (!render_frame_host) { |
| return RespondNow(document_id.has_value() ? Error("Invalid `documentId`.") |
| : Error("Invalid `frameId`.")); |
| } |
| |
| // If both document_id and tab_id are provided, make sure they match. |
| if (document_id.has_value() && tab_id.has_value()) { |
| if (ExtensionTabUtil::GetTabId(web_contents) != tab_id.value() || |
| ExtensionApiFrameIdMap::GetFrameId(render_frame_host) != |
| frame_id.value()) { |
| return RespondNow( |
| Error("Provided `tabId` and `frameId` do not match the frame.")); |
| } |
| } |
| |
| base::expected<net::CookiePartitionKey::SerializedCookiePartitionKey, |
| std::string> |
| serialized_key = net::CookiePartitionKey::Serialize( |
| render_frame_host->GetStorageKey().ToCookiePartitionKey()); |
| if (!serialized_key.has_value()) { |
| return RespondNow(Error("PartitionKey requested is not serializable.")); |
| } |
| |
| std::string error; |
| if (!CheckHostPermissions(extension(), GURL(serialized_key->TopLevelSite()), |
| &error) || |
| !CheckHostPermissions(extension(), |
| render_frame_host->GetLastCommittedURL(), &error)) { |
| return RespondNow(Error(error)); |
| } |
| |
| api::cookies::CookiePartitionKey partition_key; |
| partition_key.has_cross_site_ancestor = |
| serialized_key->has_cross_site_ancestor(); |
| partition_key.top_level_site = serialized_key->TopLevelSite(); |
| |
| api::cookies::GetPartitionKey::Results::Details details = |
| api::cookies::GetPartitionKey::Results::Details(); |
| details.partition_key = partition_key.Clone(); |
| return RespondNow(WithArguments(details.ToValue())); |
| } |
| |
| ExtensionFunction::ResponseAction CookiesGetAllCookieStoresFunction::Run() { |
| Profile* original_profile = Profile::FromBrowserContext(browser_context()); |
| DCHECK(original_profile); |
| base::Value::List original_tab_ids; |
| Profile* incognito_profile = nullptr; |
| base::Value::List incognito_tab_ids; |
| if (include_incognito_information() && |
| original_profile->HasPrimaryOTRProfile()) { |
| incognito_profile = |
| original_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true); |
| } |
| DCHECK(original_profile != incognito_profile); |
| |
| // Iterate through all browser instances, and for each browser, |
| // add its tab IDs to either the regular or incognito tab ID list depending |
| // whether the browser is regular or incognito. |
| for (WindowController* window : *WindowControllerList::GetInstance()) { |
| if (window->profile() == original_profile) { |
| cookies_helpers::AppendToTabIdList(window, original_tab_ids); |
| } else if (window->profile() == incognito_profile) { |
| cookies_helpers::AppendToTabIdList(window, incognito_tab_ids); |
| } |
| } |
| // Return a list of all cookie stores with at least one open tab. |
| std::vector<api::cookies::CookieStore> cookie_stores; |
| if (!original_tab_ids.empty()) { |
| cookie_stores.push_back(cookies_helpers::CreateCookieStore( |
| original_profile, std::move(original_tab_ids))); |
| } |
| if (incognito_profile && !incognito_tab_ids.empty()) { |
| cookie_stores.push_back(cookies_helpers::CreateCookieStore( |
| incognito_profile, std::move(incognito_tab_ids))); |
| } |
| return RespondNow(ArgumentList( |
| api::cookies::GetAllCookieStores::Results::Create(cookie_stores))); |
| } |
| |
| CookiesAPI::CookiesAPI(content::BrowserContext* context) |
| : browser_context_(context) { |
| EventRouter::Get(browser_context_) |
| ->RegisterObserver(this, api::cookies::OnChanged::kEventName); |
| } |
| |
| CookiesAPI::~CookiesAPI() = default; |
| |
| void CookiesAPI::Shutdown() { |
| EventRouter::Get(browser_context_)->UnregisterObserver(this); |
| } |
| |
| static base::LazyInstance<BrowserContextKeyedAPIFactory<CookiesAPI>>:: |
| DestructorAtExit g_cookies_api_factory = LAZY_INSTANCE_INITIALIZER; |
| |
| // static |
| BrowserContextKeyedAPIFactory<CookiesAPI>* CookiesAPI::GetFactoryInstance() { |
| return g_cookies_api_factory.Pointer(); |
| } |
| |
| void CookiesAPI::OnListenerAdded(const EventListenerInfo& details) { |
| DCHECK(!cookies_event_router_); |
| cookies_event_router_ = |
| std::make_unique<CookiesEventRouter>(browser_context_); |
| EventRouter::Get(browser_context_)->UnregisterObserver(this); |
| } |
| |
| } // namespace extensions |