// 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/check_is_test.h"
#include "base/containers/contains.h"
#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/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/browser/web_contents/web_app_url_loader.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/url_util.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/blink/public/mojom/manifest/manifest_manager.mojom.h"
#include "url/url_constants.h"
namespace web_app {
namespace {
// TODO(crbug.com/40273612): Find a better way to do Lacros testing so that we
// don't have to pass localhost into the allowlist. Allowlisted host must be
// from a Google server.
constexpr auto kHostAllowlist = base::MakeFixedFlatSet<std::string_view>(
{"googleusercontent.com", "gstatic.com", "youtube.com",
"127.0.0.1" /*FOR TESTING*/});
bool HasRequiredManifestFields(const blink::mojom::ManifestPtr& manifest) {
if (!manifest->has_valid_specified_start_url) {
return false;
}
if (!manifest->short_name.has_value() && !manifest->name.has_value()) {
return false;
}
return true;
}
} // 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);
url_loader_ = web_contents_lock_->web_contents_manager().CreateUrlLoader();
data_retriever_ =
web_contents_lock_->web_contents_manager().CreateDataRetriever();
url_loader_->LoadUrl(
GURL(url::kAboutBlankURL), &web_contents_lock_->shared_web_contents(),
webapps::WebAppUrlLoader::UrlComparison::kExact,
base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnAboutBlankLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallAppFromVerifiedManifestCommand::OnAboutBlankLoaded(
webapps::WebAppUrlLoaderResult result) {
// The shared web contents must have been reset to about:blank before command
// execution.
DCHECK_EQ(web_contents_lock_->shared_web_contents().GetURL(),
GURL(url::kAboutBlankURL));
web_contents_lock_->shared_web_contents()
.GetPrimaryMainFrame()
->GetRemoteInterfaces()
->GetInterface(manifest_manager_.BindNewPipeAndPassReceiver());
manifest_manager_.set_disconnect_handler(
base::BindOnce(&InstallAppFromVerifiedManifestCommand::Abort,
weak_ptr_factory_.GetWeakPtr(), CommandResult::kFailure,
webapps::InstallResultCode::kWebContentsDestroyed));
manifest_manager_->ParseManifestFromString(
document_url_, verified_manifest_url_, verified_manifest_contents_,
base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnManifestParsed,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallAppFromVerifiedManifestCommand::OnManifestParsed(
blink::mojom::ManifestPtr manifest) {
// Note that most errors during parsing (e.g. errors to do with parsing a
// particular field) are silently ignored. As long as the manifest is valid
// JSON and contains a valid start_url and name, installation will proceed.
if (blink::IsEmptyManifest(manifest) ||
!HasRequiredManifestFields(manifest)) {
Abort(CommandResult::kFailure,
webapps::InstallResultCode::kNotValidManifestForWebApp);
return;
}
if (manifest->manifest_url != verified_manifest_url_) {
mojo::ReportBadMessage("Returned manifest has incorrect manifest URL");
Abort(CommandResult::kFailure,
webapps::InstallResultCode::kNotValidManifestForWebApp);
return;
}
GetMutableDebugValue().Set("manifest_parsed", true);
web_app_info_ =
std::make_unique<WebAppInstallInfo>(manifest->id, manifest->start_url);
web_app_info_->user_display_mode = mojom::UserDisplayMode::kStandalone;
web_app_info_->is_diy_app = is_diy_app_;
UpdateWebAppInfoFromManifest(*manifest, web_app_info_.get());
if (install_params_) {
// TODO(crbug.com/354981650): Remove this call.
ApplyParamsToWebAppInstallInfo(*install_params_, *web_app_info_);
}
IconUrlSizeSet icon_urls = GetValidIconUrlsToDownload(*web_app_info_);
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;
});
if (icon_urls.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;
}
data_retriever_->GetIcons(
&web_contents_lock_->shared_web_contents(), std::move(icon_urls),
/*skip_page_favicons=*/true,
/*fail_all_if_any_fail=*/false,
base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnIconsRetrieved,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallAppFromVerifiedManifestCommand::OnIconsRetrieved(
IconsDownloadedResult result,
IconsMap icons_map,
DownloadedIconsHttpResults icons_http_results) {
DCHECK(web_app_info_);
PopulateProductIcons(web_app_info_.get(), &icons_map);
if (web_app_info_->is_generated_icon) {
// PopulateProductIcons 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;
}
PopulateOtherIcons(web_app_info_.get(), icons_map);
webapps::AppId app_id =
GenerateAppIdFromManifestId(web_app_info_->manifest_id());
if (app_id != expected_id_) {
Abort(CommandResult::kFailure,
webapps::InstallResultCode::kExpectedAppIdCheckFailed);
return;
}
command_manager()->lock_manager().UpgradeAndAcquireLock(
std::move(web_contents_lock_), {app_id},
base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnAppLockAcquired,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallAppFromVerifiedManifestCommand::OnAppLockAcquired(
std::unique_ptr<SharedWebContentsWithAppLock> app_lock) {
app_lock_ = std::move(app_lock);
WebAppInstallFinalizer::FinalizeOptions 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;
app_lock_->install_finalizer().FinalizeInstall(
*web_app_info_, finalize_options,
base::BindOnce(&InstallAppFromVerifiedManifestCommand::OnInstallFinalized,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallAppFromVerifiedManifestCommand::OnInstallFinalized(
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
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));
CompleteAndSelfDestruct(result, webapps::AppId(), code);
}
} // namespace web_app