chromium/chrome/browser/sharing/sms/sms_fetch_request_handler.cc

// Copyright 2019 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/sharing/sms/sms_fetch_request_handler.h"

#include <string>

#include "base/android/jni_string.h"
#include "base/check.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/sharing_message/proto/sms_fetch_message_test_proto3_optional.pb.h"
#include "components/sharing_message/sharing_device_source.h"
#include "components/sharing_message/sharing_target_device_info.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/sms_fetcher.h"
#include "third_party/protobuf/src/google/protobuf/repeated_field.h"
#include "url/gurl.h"
#include "url/origin.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/SmsFetcherMessageHandler_jni.h"

namespace {
// To mitigate the overlapping of the notification for SMS and the one for
// user permission, we postpone showing the latter to make sure it's always
// visible to users.
static constexpr base::TimeDelta kNotificationDelay = base::Seconds(1);

bool DoesMatchOriginList(const std::vector<std::u16string>& origins,
                         const content::OriginList& origin_list) {
  if (origins.size() != origin_list.size())
    return false;

  for (size_t i = 0; i < origins.size(); ++i) {
    if (origins[i] != url_formatter::FormatOriginForSecurityDisplay(
                          origin_list[i],
                          url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)) {
      return false;
    }
  }
  return true;
}

}  // namespace

SmsFetchRequestHandler::SmsFetchRequestHandler(
    SharingDeviceSource* device_source,
    content::SmsFetcher* fetcher)
    : device_source_(device_source), fetcher_(fetcher) {}

SmsFetchRequestHandler::~SmsFetchRequestHandler() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_SmsFetcherMessageHandler_reset(env);
}

void SmsFetchRequestHandler::OnMessage(
    components_sharing_message::SharingMessage message,
    SharingMessageHandler::DoneCallback done_callback) {
  DCHECK(message.has_sms_fetch_request());

  std::optional<SharingTargetDeviceInfo> device =
      device_source_->GetDeviceByGuid(message.sender_guid());
  const std::string& client_name =
      device ? device->client_name() : message.sender_device_name();

  // Empty client_name means that the message is from an unsupported version of
  // Chrome.
  base::UmaHistogramBoolean("Sharing.SmsFetcherClientNameIsEmpty",
                            client_name.empty());
  if (client_name.empty())
    return;

  const google::protobuf::RepeatedPtrField<std::string>& origin_strings =
      message.sms_fetch_request().origins();
  if (origin_strings.empty())
    return;

  std::vector<url::Origin> origin_list;
  for (const std::string& origin_string : origin_strings)
    origin_list.push_back(url::Origin::Create(GURL(origin_string)));

  auto request = std::make_unique<Request>(
      this, fetcher_, origin_list, client_name, std::move(done_callback));
  requests_.insert(std::move(request));
}

void SmsFetchRequestHandler::RemoveRequest(Request* request) {
  requests_.erase(request);
}

void SmsFetchRequestHandler::AskUserPermission(
    const content::OriginList& origin_list,
    const std::string& one_time_code,
    const std::string& client_name) {
  JNIEnv* env = base::android::AttachCurrentThread();
  DCHECK(origin_list.size() == 1 || origin_list.size() == 2);

  base::android::ScopedJavaLocalRef<jstring> embedded_origin;
  base::android::ScopedJavaLocalRef<jstring> top_origin;
  if (origin_list.size() == 2) {
    embedded_origin = base::android::ConvertUTF16ToJavaString(
        env,
        url_formatter::FormatOriginForSecurityDisplay(
            origin_list[0], url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS));
    top_origin = base::android::ConvertUTF16ToJavaString(
        env,
        url_formatter::FormatOriginForSecurityDisplay(
            origin_list[1], url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS));
  } else {
    top_origin = base::android::ConvertUTF16ToJavaString(
        env,
        url_formatter::FormatOriginForSecurityDisplay(
            origin_list[0], url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS));
  }

  // If there is a notification from a previous request on screen this will
  // overwrite that one with the new origin. In most cases where there's only
  // one pending origin, the request will be removed when |SmsRetrieverClient|
  // times out which would triggers |Request::OnFailure|.
  // TODO(crbug.com/40153007): We should improve the infrastructure to be able
  // to handle failures when there are multiple pending origins simultaneously.
  Java_SmsFetcherMessageHandler_showNotification(
      env, one_time_code, top_origin, embedded_origin, client_name,
      reinterpret_cast<intptr_t>(this));
}

void SmsFetchRequestHandler::OnConfirm(JNIEnv* env,
                                       jstring j_top_origin,
                                       jstring j_embedded_origin) {
  std::vector<std::u16string> origins;
  if (j_embedded_origin) {
    std::u16string embedded_origin =
        base::android::ConvertJavaStringToUTF16(env, j_embedded_origin);
    origins.push_back(embedded_origin);
  }
  std::u16string top_origin =
      base::android::ConvertJavaStringToUTF16(env, j_top_origin);
  origins.push_back(top_origin);
  auto* request = GetRequest(origins);
  DCHECK(request);
  request->SendSuccessMessage();
}

void SmsFetchRequestHandler::OnDismiss(JNIEnv* env,
                                       jstring j_top_origin,
                                       jstring j_embedded_origin) {
  std::vector<std::u16string> origins;
  if (j_embedded_origin) {
    std::u16string embedded_origin =
        base::android::ConvertJavaStringToUTF16(env, j_embedded_origin);
    origins.push_back(embedded_origin);
  }
  std::u16string top_origin =
      base::android::ConvertJavaStringToUTF16(env, j_top_origin);
  origins.push_back(top_origin);
  auto* request = GetRequest(origins);
  DCHECK(request);
  // TODO(crbug.com/40103792): We should have a separate catergory for this type
  // of failure.
  request->SendFailureMessage(FailureType::kPromptCancelled);
}

SmsFetchRequestHandler::Request* SmsFetchRequestHandler::GetRequest(
    const std::vector<std::u16string>& origins) {
  // If the request is made from a cross-origin iframe, the origin_list consists
  // of the embedded frame origin and then the top frame origin.
  for (auto& request : requests_) {
    if (DoesMatchOriginList(origins, request->origin_list()))
      return request.get();
  }
  return nullptr;
}

base::WeakPtr<SmsFetchRequestHandler> SmsFetchRequestHandler::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

SmsFetchRequestHandler::Request::Request(
    SmsFetchRequestHandler* handler,
    content::SmsFetcher* fetcher,
    const std::vector<url::Origin>& origin_list,
    const std::string& client_name,
    SharingMessageHandler::DoneCallback respond_callback)
    : handler_(handler),
      fetcher_(fetcher),
      origin_list_(origin_list),
      client_name_(client_name),
      respond_callback_(std::move(respond_callback)) {
  fetcher_->Subscribe(origin_list_, *this);
}

SmsFetchRequestHandler::Request::~Request() {
  fetcher_->Unsubscribe(origin_list_, this);
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_SmsFetcherMessageHandler_dismissNotification(env);
}

void SmsFetchRequestHandler::Request::OnReceive(
    const content::OriginList& origin_list,
    const std::string& one_time_code,
    content::SmsFetcher::UserConsent consent_requirement) {
  DCHECK(origin_list_ == origin_list);
  one_time_code_ = one_time_code;

  // Postpones asking for user permission to make sure that the notification is
  // not covered by the SMS notification.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&SmsFetchRequestHandler::AskUserPermission,
                     handler_->GetWeakPtr(), origin_list, one_time_code,
                     client_name_),
      kNotificationDelay);
}

void SmsFetchRequestHandler::Request::SendSuccessMessage() {
  auto response =
      std::make_unique<components_sharing_message::ResponseMessage>();
  for (const auto& origin : origin_list_)
    response->mutable_sms_fetch_response()->add_origins(origin.Serialize());
  response->mutable_sms_fetch_response()->set_one_time_code(one_time_code_);

  std::move(respond_callback_).Run(std::move(response));
  handler_->RemoveRequest(this);
}

void SmsFetchRequestHandler::Request::SendFailureMessage(
    FailureType failure_type) {
  auto response =
      std::make_unique<components_sharing_message::ResponseMessage>();
  response->mutable_sms_fetch_response()->set_failure_type(
      static_cast<components_sharing_message::SmsFetchResponse::FailureType>(
          failure_type));

  std::move(respond_callback_).Run(std::move(response));
  handler_->RemoveRequest(this);
}

void SmsFetchRequestHandler::Request::OnFailure(FailureType failure_type) {
  SendFailureMessage(failure_type);
}