| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/gcm/gcm_api.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "chrome/browser/extensions/extension_gcm_app_handler.h" |
| #include "chrome/browser/gcm/gcm_profile_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/extensions/api/gcm.h" |
| #include "components/gcm_driver/common/gcm_message.h" |
| #include "components/gcm_driver/gcm_client.h" |
| #include "components/gcm_driver/gcm_driver.h" |
| #include "components/gcm_driver/gcm_profile_service.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/extension.h" |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| namespace { |
| |
| const size_t kMaximumGcmMessageSize = 4096; // in bytes. |
| const char kCollapseKey[] = "collapse_key"; |
| const char kGoogDotRestrictedPrefix[] = "goog."; |
| const char kGoogleRestrictedPrefix[] = "google"; |
| |
| // Error messages. |
| const char kInvalidParameter[] = |
| "Function was called with invalid parameters."; |
| const char kGCMDisabled[] = "GCM is currently disabled."; |
| const char kAsyncOperationPending[] = |
| "Asynchronous operation is pending."; |
| const char kNetworkError[] = "Network error occurred."; |
| const char kServerError[] = "Server error occurred."; |
| const char kTtlExceeded[] = "Time-to-live exceeded."; |
| const char kUnknownError[] = "Unknown error occurred."; |
| |
| const char* GcmResultToError(gcm::GCMClient::Result result) { |
| switch (result) { |
| case gcm::GCMClient::SUCCESS: |
| return ""; |
| case gcm::GCMClient::INVALID_PARAMETER: |
| return kInvalidParameter; |
| case gcm::GCMClient::GCM_DISABLED: |
| return kGCMDisabled; |
| case gcm::GCMClient::ASYNC_OPERATION_PENDING: |
| return kAsyncOperationPending; |
| case gcm::GCMClient::NETWORK_ERROR: |
| return kNetworkError; |
| case gcm::GCMClient::SERVER_ERROR: |
| return kServerError; |
| case gcm::GCMClient::TTL_EXCEEDED: |
| return kTtlExceeded; |
| case gcm::GCMClient::UNKNOWN_ERROR: |
| return kUnknownError; |
| default: |
| NOTREACHED() << "Unexpected value of result cannot be converted: " |
| << result; |
| } |
| } |
| |
| bool IsMessageKeyValid(const std::string& key) { |
| std::string lower = base::ToLowerASCII(key); |
| return !key.empty() && |
| key.compare(0, std::size(kCollapseKey) - 1, kCollapseKey) != 0 && |
| lower.compare(0, std::size(kGoogleRestrictedPrefix) - 1, |
| kGoogleRestrictedPrefix) != 0 && |
| lower.compare(0, std::size(kGoogDotRestrictedPrefix), |
| kGoogDotRestrictedPrefix) != 0; |
| } |
| |
| } // namespace |
| |
| namespace extensions { |
| |
| bool GcmApiFunction::PreRunValidation(std::string* error) { |
| return IsGcmApiEnabled(error); |
| } |
| |
| bool GcmApiFunction::IsGcmApiEnabled(std::string* error) const { |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| |
| // GCM is not supported in incognito mode. |
| if (profile->IsOffTheRecord()) { |
| *error = "GCM is not supported in incognito mode."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| gcm::GCMDriver* GcmApiFunction::GetGCMDriver() const { |
| return gcm::GCMProfileServiceFactory::GetForProfile( |
| Profile::FromBrowserContext(browser_context()))->driver(); |
| } |
| |
| GcmRegisterFunction::GcmRegisterFunction() = default; |
| |
| GcmRegisterFunction::~GcmRegisterFunction() = default; |
| |
| ExtensionFunction::ResponseAction GcmRegisterFunction::Run() { |
| std::optional<api::gcm::Register::Params> params = |
| api::gcm::Register::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // This server API was deprecated by Firebase in 2019. Don't bother trying to |
| // implement register() on Android - it will just return an error. |
| // TODO(crbug.com/421235963): Consider deprecating on other platforms. |
| return RespondNow(Error(GcmResultToError(gcm::GCMClient::UNKNOWN_ERROR))); |
| #else |
| GetGCMDriver()->Register( |
| extension()->id(), params->sender_ids, |
| base::BindOnce(&GcmRegisterFunction::CompleteFunctionWithResult, this)); |
| |
| // Register() might have returned synchronously. |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| void GcmRegisterFunction::CompleteFunctionWithResult( |
| const std::string& registration_id, |
| gcm::GCMClient::Result gcm_result) { |
| base::Value::List result; |
| result.Append(registration_id); |
| |
| const bool succeeded = gcm::GCMClient::SUCCESS == gcm_result; |
| Respond(succeeded |
| ? ArgumentList(std::move(result)) |
| // TODO(lazyboy): We shouldn't be using |result| in case of error. |
| : ErrorWithArgumentsDoNotUse(std::move(result), |
| GcmResultToError(gcm_result))); |
| } |
| |
| GcmUnregisterFunction::GcmUnregisterFunction() = default; |
| |
| GcmUnregisterFunction::~GcmUnregisterFunction() = default; |
| |
| ExtensionFunction::ResponseAction GcmUnregisterFunction::Run() { |
| #if BUILDFLAG(IS_ANDROID) |
| // This server API was deprecated by Firebase in 2019. Don't bother trying to |
| // implement register() on Android - it will just return an error. |
| // TODO(crbug.com/421235963): Consider deprecating on other platforms. |
| return RespondNow(Error(GcmResultToError(gcm::GCMClient::UNKNOWN_ERROR))); |
| #else |
| GetGCMDriver()->Unregister( |
| extension()->id(), |
| base::BindOnce(&GcmUnregisterFunction::CompleteFunctionWithResult, this)); |
| |
| // Unregister might have responded already (synchronously). |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| void GcmUnregisterFunction::CompleteFunctionWithResult( |
| gcm::GCMClient::Result result) { |
| const bool succeeded = gcm::GCMClient::SUCCESS == result; |
| Respond(succeeded ? NoArguments() : Error(GcmResultToError(result))); |
| } |
| |
| GcmSendFunction::GcmSendFunction() = default; |
| |
| GcmSendFunction::~GcmSendFunction() = default; |
| |
| ExtensionFunction::ResponseAction GcmSendFunction::Run() { |
| std::optional<api::gcm::Send::Params> params = |
| api::gcm::Send::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| EXTENSION_FUNCTION_VALIDATE( |
| ValidateMessageData(params->message.data.additional_properties)); |
| |
| gcm::OutgoingMessage outgoing_message; |
| outgoing_message.id = params->message.message_id; |
| outgoing_message.data = params->message.data.additional_properties; |
| if (params->message.time_to_live) |
| outgoing_message.time_to_live = *params->message.time_to_live; |
| |
| GetGCMDriver()->Send( |
| extension()->id(), params->message.destination_id, outgoing_message, |
| base::BindOnce(&GcmSendFunction::CompleteFunctionWithResult, this)); |
| |
| // Send might have already responded synchronously. |
| return did_respond() ? AlreadyResponded() : RespondLater(); |
| } |
| |
| void GcmSendFunction::CompleteFunctionWithResult( |
| const std::string& message_id, |
| gcm::GCMClient::Result gcm_result) { |
| base::Value::List result; |
| result.Append(message_id); |
| |
| const bool succeeded = gcm::GCMClient::SUCCESS == gcm_result; |
| Respond(succeeded |
| ? ArgumentList(std::move(result)) |
| // TODO(lazyboy): We shouldn't be using |result| in case of error. |
| : ErrorWithArgumentsDoNotUse(std::move(result), |
| GcmResultToError(gcm_result))); |
| } |
| |
| bool GcmSendFunction::ValidateMessageData(const gcm::MessageData& data) const { |
| size_t total_size = 0u; |
| for (const auto& [key, value] : data) { |
| total_size += key.size() + value.size(); |
| |
| if (!IsMessageKeyValid(key) || kMaximumGcmMessageSize < key.size() || |
| kMaximumGcmMessageSize < value.size() || |
| kMaximumGcmMessageSize < total_size) |
| return false; |
| } |
| |
| return total_size != 0; |
| } |
| |
| GcmJsEventRouter::GcmJsEventRouter(Profile* profile) : profile_(profile) { |
| } |
| |
| GcmJsEventRouter::~GcmJsEventRouter() = default; |
| |
| void GcmJsEventRouter::OnMessage(const std::string& app_id, |
| const gcm::IncomingMessage& message) { |
| api::gcm::OnMessage::Message message_arg; |
| message_arg.data.additional_properties = message.data; |
| if (!message.sender_id.empty()) |
| message_arg.from = message.sender_id; |
| if (!message.collapse_key.empty()) { |
| message_arg.collapse_key = message.collapse_key; |
| } |
| |
| std::unique_ptr<Event> event( |
| new Event(events::GCM_ON_MESSAGE, api::gcm::OnMessage::kEventName, |
| api::gcm::OnMessage::Create(message_arg), profile_)); |
| EventRouter::Get(profile_) |
| ->DispatchEventToExtension(app_id, std::move(event)); |
| } |
| |
| void GcmJsEventRouter::OnMessagesDeleted(const std::string& app_id) { |
| std::unique_ptr<Event> event(new Event( |
| events::GCM_ON_MESSAGES_DELETED, api::gcm::OnMessagesDeleted::kEventName, |
| api::gcm::OnMessagesDeleted::Create(), profile_)); |
| EventRouter::Get(profile_) |
| ->DispatchEventToExtension(app_id, std::move(event)); |
| } |
| |
| void GcmJsEventRouter::OnSendError( |
| const std::string& app_id, |
| const gcm::GCMClient::SendErrorDetails& send_error_details) { |
| api::gcm::OnSendError::Error error; |
| error.message_id = send_error_details.message_id; |
| error.error_message = GcmResultToError(send_error_details.result); |
| error.details.additional_properties = send_error_details.additional_data; |
| |
| std::unique_ptr<Event> event( |
| new Event(events::GCM_ON_SEND_ERROR, api::gcm::OnSendError::kEventName, |
| api::gcm::OnSendError::Create(error), profile_)); |
| EventRouter::Get(profile_) |
| ->DispatchEventToExtension(app_id, std::move(event)); |
| } |
| |
| } // namespace extensions |