// Copyright 2024 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/web_applications/commands/install_app_from_verified_manifest_command.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/containers/fixed_flat_set.h"
#include "base/containers/flat_tree.h"
#include "base/functional/bind.h"
#include "base/strings/to_string.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/commands/command_metrics.h"
#include "chrome/browser/web_applications/jobs/finalize_install_or_update_job.h"
#include "chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.h"
#include "chrome/browser/web_applications/jobs/parse_manifest_from_string_job.h"
#include "chrome/browser/web_applications/locks/shared_web_contents_lock.h"
#include "chrome/browser/web_applications/locks/shared_web_contents_with_app_lock.h"
#include "chrome/browser/web_applications/locks/web_app_lock_manager.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_icon_operations.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_params.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/web_contents.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"

namespace web_app {

namespace {

// TODO(crbug.com/40273612): Figure out why tests fail when removing
// 127.0.0.1 even though that was added only for Lacros testing and hence
// shouldn't be needed anymore.
constexpr auto kHostAllowlist = base::MakeFixedFlatSet<std::string_view>(
    {"googleusercontent.com", "gstatic.com", "youtube.com",
     "127.0.0.1" /*FOR TESTING*/});

}  // namespace

InstallAppFromVerifiedManifestCommand::InstallAppFromVerifiedManifestCommand(
    webapps::WebappInstallSource install_source,
    GURL document_url,
    GURL verified_manifest_url,
    std::string verified_manifest_contents,
    webapps::AppId expected_id,
    bool is_diy_app,
    std::optional<WebAppInstallParams> install_params,
    OnceInstallCallback callback)
    : WebAppCommand<SharedWebContentsLock,
                    const webapps::AppId&,
                    webapps::InstallResultCode>(
          "InstallAppFromVerifiedManifestCommand",
          SharedWebContentsLockDescription(),
          std::move(callback),
          /*args_for_shutdown=*/
          std::make_tuple(webapps::AppId(),
                          webapps::InstallResultCode::
                              kCancelledOnWebAppProviderShuttingDown)),
      install_source_(install_source),
      document_url_(std::move(document_url)),
      verified_manifest_url_(std::move(verified_manifest_url)),
      verified_manifest_contents_(std::move(verified_manifest_contents)),
      expected_id_(std::move(expected_id)),
      is_diy_app_(is_diy_app),
      install_params_(std::move(install_params)) {
  if (install_params_) {
    // Not every `install_params` option has an effect, check that unused params
    // are not set.
    CHECK_EQ(install_params->install_state,
             proto::InstallState::INSTALLED_WITH_OS_INTEGRATION);
    CHECK(install_params->fallback_start_url.is_empty());
    CHECK(!install_params->fallback_app_name.has_value());
  }

  GetMutableDebugValue().Set("document_url", document_url_.spec());
  GetMutableDebugValue().Set("verified_manifest_url",
                             verified_manifest_url_.spec());
  GetMutableDebugValue().Set("expected_id", expected_id_);
  GetMutableDebugValue().Set("verified_manifest_contents",
                             verified_manifest_contents_);
  GetMutableDebugValue().Set("has_install_params", !!install_params_);
}

InstallAppFromVerifiedManifestCommand::
    ~InstallAppFromVerifiedManifestCommand() = default;

void InstallAppFromVerifiedManifestCommand::StartWithLock(
    std::unique_ptr<SharedWebContentsLock> lock) {
  web_contents_lock_ = std::move(lock);

  data_retriever_ =
      web_contents_lock_->web_contents_manager().CreateDataRetriever();

  parse_job_ = std::make_unique<ParseManifestFromStringJob>(
      web_contents_lock_->web_contents_manager(),
      web_contents_lock_->shared_web_contents(), document_url_,
      verified_manifest_url_,
      std::move(verified_manifest_contents_),  // Consumed by parse_job_.
      *GetMutableDebugValue().EnsureDict("parse_manifest_from_string_job"),
      base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnManifestParsed,
                     weak_ptr_factory_.GetWeakPtr()));
  parse_job_->Start();
}

void InstallAppFromVerifiedManifestCommand::OnManifestParsed(
    blink::mojom::ManifestPtr manifest) {
  parse_job_.reset();

  if (!manifest) {
    Abort(CommandResult::kFailure,
          webapps::InstallResultCode::kNotValidManifestForWebApp);
    return;
  }

  auto icon_url_modifications = [](IconUrlSizeSet& icon_urls) {
    base::EraseIf(icon_urls, [](const IconUrlWithSize& url_with_size) {
      for (const auto& allowed_host : kHostAllowlist) {
        const GURL& icon_url = url_with_size.url;
        if (icon_url.DomainIs(allowed_host)) {
          // Found a match, don't erase this url!
          return false;
        }
      }
      // No matches, erase this url!
      return true;
    });
  };

  // This is needed to differentiate between icons being generated and no icon
  // urls left to download.
  WebAppInstallInfoConstructOptions construct_options;
  construct_options.bypass_icon_generation_if_no_url = true;

  manifest_to_install_info_job_ =
      ManifestToWebAppInstallInfoJob::CreateAndStart(
          *manifest, *data_retriever_.get(), /*background_installation=*/true,
          install_source_,
          web_contents_lock_->shared_web_contents().GetWeakPtr(),
          icon_url_modifications, GetMutableDebugValue(),
          base::BindOnce(&InstallAppFromVerifiedManifestCommand::
                             OnInstallInfoParsedFromManifest,
                         weak_ptr_factory_.GetWeakPtr()),
          construct_options);
}

void InstallAppFromVerifiedManifestCommand::OnInstallInfoParsedFromManifest(
    std::unique_ptr<WebAppInstallInfo> install_info) {
  CHECK(install_info);
  web_app_info_ = std::move(install_info);

  if (web_app_info_->icon_bitmaps.empty()) {
    // Abort if there are no icons to download, so we can distinguish this case
    // from having icons but failing to download them.
    Abort(CommandResult::kFailure,
          webapps::InstallResultCode::kNoValidIconsInManifest);
    return;
  }

  if (web_app_info_->is_generated_icon) {
    // PopulateProductIcons() inside the ManifestToWebAppInstallInfoJob sets
    // `is_generated_icon` if it had to generate a product icon due a lack of
    // successfully downloaded product icons. In this case, abort the
    // installation and report the error.
    Abort(CommandResult::kFailure,
          webapps::InstallResultCode::kIconDownloadingFailed);
    return;
  }

  web_app_info_->user_display_mode = mojom::UserDisplayMode::kStandalone;
  web_app_info_->is_diy_app = is_diy_app_;

  if (install_params_) {
    // TODO(crbug.com/354981650): Remove this call.
    ApplyParamsToWebAppInstallInfo(*install_params_, *web_app_info_);
  }

  webapps::AppId app_id =
      GenerateAppIdFromManifestId(web_app_info_->manifest_id());

  if (app_id != expected_id_) {
    Abort(CommandResult::kFailure,
          webapps::InstallResultCode::kExpectedAppIdCheckFailed);
    return;
  }

  app_lock_ = std::make_unique<SharedWebContentsWithAppLock>();
  command_manager()->lock_manager().UpgradeAndAcquireLock(
      std::move(web_contents_lock_), *app_lock_, {app_id},
      base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnAppLockAcquired,
                     weak_ptr_factory_.GetWeakPtr()));
}

void InstallAppFromVerifiedManifestCommand::OnAppLockAcquired() {
  CHECK(app_lock_);
  CHECK(app_lock_->IsGranted());
  FinalizeJobOptions finalize_options(install_source_);
  finalize_options.add_to_quick_launch_bar = false;
  finalize_options.overwrite_existing_manifest_fields = false;

  if (install_params_) {
    ApplyParamsToFinalizeOptions(*install_params_, finalize_options);
  }

  // TODO(crbug.com/40197834): apply host_allowlist instead of disabling origin
  // association validate for all origins.
  finalize_options.skip_origin_association_validation = true;

  install_job_ = std::make_unique<FinalizeInstallOrUpdateJob>(
      *Profile::FromBrowserContext(
          app_lock_->shared_web_contents().GetBrowserContext()),
      app_lock_.get(), app_lock_.get(), *web_app_info_, finalize_options);

  install_job_->Start(
      base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnInstallFinalized,
                     weak_ptr_factory_.GetWeakPtr()));
}

void InstallAppFromVerifiedManifestCommand::OnInstallFinalized(
    const webapps::AppId& app_id,
    webapps::InstallResultCode code) {
  install_job_.reset();
  GetMutableDebugValue().Set("error_code", base::ToString(code));
  RecordInstallMetrics(InstallCommand::kInstallAppFromVerifiedManifest,
                       WebAppType::kCraftedApp, code, install_source_);
  CompleteAndSelfDestruct(webapps::IsSuccess(code) ? CommandResult::kSuccess
                                                   : CommandResult::kFailure,
                          app_id, code);
}

void InstallAppFromVerifiedManifestCommand::Abort(
    CommandResult result,
    webapps::InstallResultCode code) {
  GetMutableDebugValue().Set("error_code", base::ToString(code));
  RecordInstallMetrics(InstallCommand::kInstallAppFromVerifiedManifest,
                       WebAppType::kCraftedApp, code, install_source_);
  CompleteAndSelfDestruct(result, webapps::AppId(), code);
}

}  // namespace web_app
