blob: 6ec68017676404fbfdf09d3480eb74c8f5c621eb [file] [log] [blame]
// 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/web_install_service_impl.h"
#include <optional>
#include <vector>
#include "base/functional/bind.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/commands/web_install_from_url_command.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/browser/web_applications/web_contents/web_app_data_retriever.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "components/permissions/permission_request.h"
#include "components/webapps/browser/banners/app_banner_manager.h"
#include "components/webapps/browser/banners/installable_web_app_check_result.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/installable/installable_params.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_request_description.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "third_party/blink/public/mojom/web_install/web_install.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace web_app {
namespace {
using PermissionStatus = blink::mojom::PermissionStatus;
} // namespace
WebInstallServiceImpl::WebInstallServiceImpl(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<blink::mojom::WebInstallService> receiver)
: content::DocumentService<blink::mojom::WebInstallService>(
render_frame_host,
std::move(receiver)),
frame_routing_id_(render_frame_host.GetGlobalId()) {}
WebInstallServiceImpl::~WebInstallServiceImpl() = default;
// static
void WebInstallServiceImpl::CreateIfAllowed(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::WebInstallService> receiver) {
CHECK(render_frame_host);
// This class is created only on the primary main frame.
if (!render_frame_host->IsInPrimaryMainFrame()) {
receiver.reset();
return;
}
// TODO(crbug.com/402547563): Installing web apps is not supported from
// off-the-record profiles.
// This check stops the ServiceImpl from being
// created in Incognito mode. (To exclude Guest mode too, switch to
// AreWebAppsUserInstallable). It may need to be removed depending where the
// auto rejection is implemented.
if (!AreWebAppsEnabled(Profile::FromBrowserContext(
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetBrowserContext()))) {
receiver.reset();
return;
}
if (!render_frame_host->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) {
receiver.reset();
return;
}
new WebInstallServiceImpl(*render_frame_host, std::move(receiver));
}
void WebInstallServiceImpl::Install(blink::mojom::InstallOptionsPtr options,
InstallCallback callback) {
GURL install_target;
const GURL current_url = render_frame_host().GetLastCommittedURL();
// `options` is null if the 0-parameter signature was called.
if (!options) {
// No parameters means we want to install the current document.
install_target = current_url;
} else {
install_target = GURL(options->install_url);
}
install_options_ = std::move(options);
// Do not allow installation of file:// or chrome:// urls.
if (!install_target.SchemeIsHTTPOrHTTPS()) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
// TODO(crbug.com/402547563): Installing web apps is not supported from
// off-the-record profiles.
// Initiate installation of the current document.
// TODO(crbug.com/407473727): Treat install(self) and install(self, self) as
// background installs, but skip the permissions checking code. Tests will
// also likely need updating.
if (install_target == current_url) {
TryInstallCurrentDocument(std::move(callback));
// Current document installs don't require the permissions checking code.
return;
}
auto* rfh = content::RenderFrameHost::FromID(frame_routing_id_);
if (!rfh) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
// Verify that the calling app has the Web Install permissions policy set.
if (!rfh->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kWebAppInstallation)) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
RequestWebInstallPermission(
base::BindOnce(&WebInstallServiceImpl::OnPermissionDecided,
weak_ptr_factory_.GetWeakPtr(), install_target,
install_options_->manifest_id, std::move(callback)));
}
void WebInstallServiceImpl::TryInstallCurrentDocument(
InstallCallback callback) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host());
auto* provider = WebAppProvider::GetForWebContents(web_contents);
// TODO(crbug.com/402547563): Installing web apps is not supported from
// off-the-record profiles.
// As of now, WebInstallServiceImpl is only created if `AreWebAppsEnabled` for
// the current browsing context (see `CreateIfAllowed`), so the provider
// should always be available. If this changes, this check can be
// reevaluated.
CHECK(provider);
// Check if the current document is already installed.
const webapps::AppId* app_id =
web_app::WebAppTabHelper::GetAppId(web_contents);
if (app_id) {
// TODO(crbug.com/402547565) Use IntentPickerBubbleView to launch the app if
// already installed. Add test cases when this is implemented.
} else {
// The current document is not installed yet. Retrieve the manifest to
// perform id validation checks.
std::unique_ptr<WebAppDataRetriever> data_retriever =
provider->web_contents_manager().CreateDataRetriever();
webapps::InstallableParams params;
params.installable_criteria =
webapps::InstallableCriteria::kValidManifestWithIcons;
data_retriever->CheckInstallabilityAndRetrieveManifest(
web_contents,
base::BindOnce(&WebInstallServiceImpl::
OnDidRetrieveManifestForCurrentDocumentInstall,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
provider),
params);
}
}
void WebInstallServiceImpl::OnDidRetrieveManifestForCurrentDocumentInstall(
InstallCallback callback,
WebAppProvider* provider,
blink::mojom::ManifestPtr opt_manifest,
bool valid_manifest_for_web_app,
webapps::InstallableStatusCode error_code) {
// If for some reason a valid manifest was not found, cancel with the generic
// abort error.
if (!opt_manifest || !valid_manifest_for_web_app) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
// Ensure that the manifest is from the same trusted origin as the current
// document.
if (!origin().IsSameOriginWith(opt_manifest->id)) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
// The manifest must have a developer-specified id if navigator.install was
// invoked without a `manifest_id` (ie. the 0 or 1 parameter version).
bool manifest_must_have_id =
!install_options_ || !install_options_->manifest_id;
if (manifest_must_have_id && !opt_manifest->has_custom_id) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kDataError,
GURL());
return;
}
// navigator.install was invoked with a manifest_id, so the current document
// is not required to have a developer-specified id. However, the id passed to
// navigator.install must match the current document's computed id.
if (!manifest_must_have_id &&
install_options_->manifest_id.value() != opt_manifest->id) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kDataError,
GURL());
return;
}
provider->ui_manager().TriggerInstallDialog(
content::WebContents::FromRenderFrameHost(&render_frame_host()),
webapps::WebappInstallSource::WEB_INSTALL,
base::BindOnce(&WebInstallServiceImpl::OnAppInstalled,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebInstallServiceImpl::RequestWebInstallPermission(
base::OnceCallback<void(const std::vector<PermissionStatus>&)> callback) {
content::BrowserContext* const browser_context =
render_frame_host().GetBrowserContext();
if (!browser_context) {
// TODO(crbug.com/381282538): Technically this isn't correct since
// permission wasn't denied. Same for the if check below. Update to a more
// appropriate error.
std::move(callback).Run(
std::vector<PermissionStatus>({PermissionStatus::DENIED}));
return;
}
content::PermissionController* const permission_controller =
browser_context->GetPermissionController();
if (!permission_controller) {
std::move(callback).Run(
std::vector<PermissionStatus>({PermissionStatus::DENIED}));
return;
}
// Check if the permission status is already set.
content::PermissionResult permission_status =
permission_controller->GetPermissionResultForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::WEB_APP_INSTALLATION),
&render_frame_host());
switch (permission_status.status) {
case PermissionStatus::GRANTED:
std::move(callback).Run(
std::vector<PermissionStatus>({PermissionStatus::GRANTED}));
return;
case PermissionStatus::DENIED:
std::move(callback).Run(
std::vector<PermissionStatus>({PermissionStatus::DENIED}));
return;
case PermissionStatus::ASK:
break;
}
GURL requesting_origin = origin().GetURL();
// User gesture requirement is enforced in NavigatorWebInstall::InstallImpl.
permission_controller->RequestPermissionsFromCurrentDocument(
&render_frame_host(),
content::PermissionRequestDescription(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::WEB_APP_INSTALLATION),
/*user_gesture=*/true, requesting_origin),
std::move(callback));
}
void WebInstallServiceImpl::OnPermissionDecided(
const GURL& install_target,
const std::optional<GURL>& manifest_id,
InstallCallback callback,
const std::vector<PermissionStatus>& permission_status) {
CHECK_EQ(permission_status.size(), 1u);
if (permission_status[0] != PermissionStatus::GRANTED) {
std::move(callback).Run(blink::mojom::WebInstallServiceResult::kAbortError,
GURL());
return;
}
auto* profile =
Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
auto* provider = WebAppProvider::GetForWebApps(profile);
provider->scheduler().InstallAppFromUrl(
install_target, manifest_id,
base::BindOnce(&WebInstallServiceImpl::OnAppInstalled,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebInstallServiceImpl::OnAppInstalled(InstallCallback callback,
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
// Results to report for generic failures.
blink::mojom::WebInstallServiceResult install_result =
blink::mojom::WebInstallServiceResult::kAbortError;
webapps::ManifestId manifest_id_result;
if (webapps::IsSuccess(code)) {
install_result = blink::mojom::WebInstallServiceResult::kSuccess;
auto* profile =
Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
auto* provider = WebAppProvider::GetForWebApps(profile);
CHECK(provider);
manifest_id_result =
provider->registrar_unsafe().GetComputedManifestId(app_id);
CHECK(!manifest_id_result.is_empty());
} else if (code == webapps::InstallResultCode::kNoCustomManifestId ||
code == webapps::InstallResultCode::kManifestIdMismatch) {
install_result = blink::mojom::WebInstallServiceResult::kDataError;
}
std::move(callback).Run(install_result, manifest_id_result);
}
} // namespace web_app