chromium/chrome/browser/ui/webui/ash/parent_access/parent_access_ui_handler_impl.cc

// Copyright 2021 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/ui/webui/ash/parent_access/parent_access_ui_handler_impl.h"

#include <optional>
#include <string>
#include <utility>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/system/sys_info.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/webui/ash/parent_access/parent_access_callback.pb.h"
#include "chrome/browser/ui/webui/ash/parent_access/parent_access_dialog.h"
#include "chrome/browser/ui/webui/ash/parent_access/parent_access_metrics_utils.h"
#include "chrome/browser/ui/webui/ash/parent_access/parent_access_ui.mojom.h"
#include "components/google/core/common/google_util.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_fetcher.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "google_apis/gaia/gaia_constants.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "url/gurl.h"

namespace ash {

namespace {
constexpr char kParentAccessDefaultURL[] =
    "https://families.google.com/parentaccess";
constexpr char kParentAccessSwitch[] = "parent-access-url";

// Returns the caller id to be used for the web widget.  The caller id is
// mapped from the flow type.   When new flow types are added to
// ParentAccessParams, a new case statement should be added here.
std::string GetCallerId(
    parent_access_ui::mojom::ParentAccessParams::FlowType flow_type) {
  switch (flow_type) {
    case parent_access_ui::mojom::ParentAccessParams::FlowType::kWebsiteAccess:
      return "39454505";
    case parent_access_ui::mojom::ParentAccessParams::FlowType::
        kExtensionAccess:
      return "12367dff";
      // NOTE:  Do not add default case here, to ensure that adding new flow
      // types to ParentAccessParams forces this case statement to be updated.
  }
}
}  // namespace

void ParentAccessUiHandlerImpl::RecordParentAccessWidgetError(
    ParentAccessUiHandlerImpl::ParentAccessWidgetError error) {
  if (delegate_) {
    base::UmaHistogramEnumeration(
        parent_access::GetHistogramTitleForFlowType(
            parent_access::kParentAccessWidgetErrorHistogramBase,
            params_->flow_type),
        error);
  }

  // Always record metric for "all" flow type.
  base::UmaHistogramEnumeration(
      parent_access::GetHistogramTitleForFlowType(
          parent_access::kParentAccessWidgetErrorHistogramBase, std::nullopt),
      error);
}

ParentAccessUiHandlerImpl::ParentAccessUiHandlerImpl(
    mojo::PendingReceiver<parent_access_ui::mojom::ParentAccessUiHandler>
        receiver,
    signin::IdentityManager* identity_manager,
    ParentAccessUiHandlerDelegate* delegate)
    : identity_manager_(identity_manager),
      delegate_(delegate),
      receiver_(this, std::move(receiver)),
      params_(delegate_ ? delegate_->CloneParentAccessParams() : nullptr) {
  // ParentAccess state is only tracked when a dialog is created. i.e. not when
  // chrome://parent-access is directly accessed.
  if (delegate_) {
    state_tracker_ = std::make_unique<ParentAccessStateTracker>(
        params_->flow_type, params_->is_disabled);
  }
}

ParentAccessUiHandlerImpl::~ParentAccessUiHandlerImpl() = default;

void ParentAccessUiHandlerImpl::GetOauthToken(GetOauthTokenCallback callback) {
  signin::ScopeSet scopes;
  scopes.insert(GaiaConstants::kParentApprovalOAuth2Scope);
  scopes.insert(GaiaConstants::kProgrammaticChallengeOAuth2Scope);

  if (oauth2_access_token_fetcher_) {
    // Only one GetOauthToken call can happen at a time.
    std::move(callback).Run(
        parent_access_ui::mojom::GetOauthTokenStatus::kOnlyOneFetchAtATime, "");
    return;
  }

  oauth2_access_token_fetcher_ =
      identity_manager_->CreateAccessTokenFetcherForAccount(
          identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync),
          "parent_access", scopes,
          base::BindOnce(&ParentAccessUiHandlerImpl::OnAccessTokenFetchComplete,
                         weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
          signin::AccessTokenFetcher::Mode::kImmediate);
}

void ParentAccessUiHandlerImpl::OnAccessTokenFetchComplete(
    GetOauthTokenCallback callback,
    GoogleServiceAuthError error,
    signin::AccessTokenInfo access_token_info) {
  oauth2_access_token_fetcher_.reset();
  if (error.state() != GoogleServiceAuthError::NONE) {
    DLOG(ERROR) << "ParentAccessUiHandlerImpl: OAuth2 token request failed. "
                << error.state() << ": " << error.ToString();
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::kOAuthError);
    std::move(callback).Run(
        parent_access_ui::mojom::GetOauthTokenStatus::kError,
        "" /* No token */);
    return;
  }
  std::move(callback).Run(
      parent_access_ui::mojom::GetOauthTokenStatus::kSuccess,
      access_token_info.token);
}

void ParentAccessUiHandlerImpl::GetParentAccessParams(
    GetParentAccessParamsCallback callback) {
  if (!delegate_) {
    LOG(ERROR) << "Delegate not available in ParentAccessUiHandler - WebUI was "
                  "probably created without a dialog";
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::
            kDelegateNotAvailable);
    std::move(callback).Run(parent_access_ui::mojom::ParentAccessParams::New());
    return;
  }
  std::move(callback).Run(params_->Clone());
  return;
}

void ParentAccessUiHandlerImpl::OnParentAccessDone(
    parent_access_ui::mojom::ParentAccessResult result,
    OnParentAccessDoneCallback callback) {
  if (!delegate_) {
    LOG(ERROR) << "Delegate not available in ParentAccessUiHandler - WebUI was "
                  "probably created without a dialog";
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::
            kDelegateNotAvailable);
    std::move(callback).Run();
    return;
  }
  switch (result) {
    case parent_access_ui::mojom::ParentAccessResult::kApproved:
      DCHECK(parent_access_token_);
      if (state_tracker_) {
        state_tracker_->OnWebUiStateChanged(
            ParentAccessStateTracker::FlowResult::kAccessApproved);
      }
      delegate_->SetApproved(
          parent_access_token_->token(),
          // Only keep the seconds, not the nanoseconds.
          base::Time::FromSecondsSinceUnixEpoch(
              parent_access_token_->expire_time().seconds()));
      break;
    case parent_access_ui::mojom::ParentAccessResult::kDeclined:
      if (state_tracker_) {
        state_tracker_->OnWebUiStateChanged(
            ParentAccessStateTracker::FlowResult::kAccessDeclined);
      }
      delegate_->SetDeclined();
      break;
    case parent_access_ui::mojom::ParentAccessResult::kCanceled:
      delegate_->SetCanceled();
      break;
    case parent_access_ui::mojom::ParentAccessResult::kError:
      if (state_tracker_) {
        state_tracker_->OnWebUiStateChanged(
            ParentAccessStateTracker::FlowResult::kError);
      }
      delegate_->SetError();
      break;
    case parent_access_ui::mojom::ParentAccessResult::kDisabled:
      delegate_->SetDisabled();
      break;
  }

  std::move(callback).Run();
}

void ParentAccessUiHandlerImpl::GetParentAccessUrl(
    GetParentAccessUrlCallback callback) {
  if (!delegate_) {
    LOG(ERROR) << "Delegate not available in ParentAccessUiHandler - WebUI was "
                  "probably created without a dialog";
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::
            kDelegateNotAvailable);
    std::move(callback).Run("");
    return;
  }

  std::string platform_version = base::SysInfo::OperatingSystemVersion();
  std::string language_code =
      google_util::GetGoogleLocale(g_browser_process->GetApplicationLocale());

  std::string url;
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(kParentAccessSwitch)) {
    url = command_line->GetSwitchValueASCII(kParentAccessSwitch);
  } else {
    url = kParentAccessDefaultURL;
    DCHECK(GURL(url).DomainIs("google.com"));
  }

  const GURL base_url(url);
  GURL::Replacements replacements;
  std::string query_string = base::StringPrintf(
      "callerid=%s&hl=%s&platform_version=%s&cros-origin=chrome://"
      "parent-access",
      GetCallerId(params_->flow_type).c_str(), language_code.c_str(),
      platform_version.c_str());
  replacements.SetQueryStr(query_string);
  const GURL result = base_url.ReplaceComponents(replacements);
  DCHECK(result.is_valid()) << "Invalid URL \"" << url << "\" for switch \""
                            << kParentAccessSwitch << "\"";
  std::move(callback).Run(result.spec());
}

void ParentAccessUiHandlerImpl::OnBeforeScreenDone(
    OnBeforeScreenDoneCallback callback) {
  if (state_tracker_) {
    state_tracker_->OnWebUiStateChanged(
        ParentAccessStateTracker::FlowResult::kParentAuthentication);
  }
  std::move(callback).Run();
}

const kids::platform::parentaccess::client::proto::ParentAccessToken*
ParentAccessUiHandlerImpl::GetParentAccessTokenForTest() {
  return parent_access_token_.get();
}

void ParentAccessUiHandlerImpl::OnParentAccessCallbackReceived(
    const std::string& encoded_parent_access_callback_proto,
    OnParentAccessCallbackReceivedCallback callback) {
  std::string decoded_parent_access_callback;
  parent_access_ui::mojom::ParentAccessServerMessagePtr message =
      parent_access_ui::mojom::ParentAccessServerMessage::New();
  if (!base::Base64Decode(encoded_parent_access_callback_proto,
                          &decoded_parent_access_callback)) {
    LOG(ERROR) << "ParentAccessHandler::ParentAccessResult: Error decoding "
                  "parent_access_result from base64";
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::kDecodingError);

    message->type =
        parent_access_ui::mojom::ParentAccessServerMessageType::kError;
    std::move(callback).Run(std::move(message));
    return;
  }

  kids::platform::parentaccess::client::proto::ParentAccessCallback
      parent_access_callback;
  if (!parent_access_callback.ParseFromString(decoded_parent_access_callback)) {
    LOG(ERROR) << "ParentAccessHandler::ParentAccessResult: Error parsing "
                  "decoded_parent_access_result to proto";
    RecordParentAccessWidgetError(
        ParentAccessUiHandlerImpl::ParentAccessWidgetError::kParsingError);

    message->type =
        parent_access_ui::mojom::ParentAccessServerMessageType::kError;
    std::move(callback).Run(std::move(message));
    return;
  }

  switch (parent_access_callback.callback_case()) {
    case kids::platform::parentaccess::client::proto::ParentAccessCallback::
        CallbackCase::kOnParentVerified:
      message->type = parent_access_ui::mojom::ParentAccessServerMessageType::
          kParentVerified;
      if (state_tracker_) {
        state_tracker_->OnWebUiStateChanged(
            ParentAccessStateTracker::FlowResult::kApproval);
      }
      if (parent_access_callback.on_parent_verified()
              .verification_proof_case() ==
          kids::platform::parentaccess::client::proto::OnParentVerified::
              VerificationProofCase::kParentAccessToken) {
        DCHECK(!parent_access_token_);
        parent_access_token_ = std::make_unique<
            kids::platform::parentaccess::client::proto::ParentAccessToken>();
        parent_access_token_->CopyFrom(
            parent_access_callback.on_parent_verified().parent_access_token());
      }
      std::move(callback).Run(std::move(message));
      break;
    default:
      VLOG(0) << "ParentAccessHandler::OnParentAccessCallback: Unknown type of "
                 "callback received and ignored: "
              << parent_access_callback.callback_case();
      RecordParentAccessWidgetError(
          ParentAccessUiHandlerImpl::ParentAccessWidgetError::kUnknownCallback);
      message->type =
          parent_access_ui::mojom::ParentAccessServerMessageType::kIgnore;
      std::move(callback).Run(std::move(message));
      break;
  }
}

}  // namespace ash