// Copyright 2023 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/apps/app_service/app_install/app_install_navigation_throttle.h"
#include <memory>
#include <optional>
#include <string_view>
#include "base/containers/contains.h"
#include "base/functional/callback.h"
#include "chrome/browser/apps/app_service/app_install/app_install_service.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/url_constants.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "url/url_util.h"
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_tracker.h"
// TODO(crbug.com/40251079): Remove circular includes.
#include "chrome/browser/ui/browser_finder.h" // nogncheck
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "base/unguessable_token.h"
#include "ui/gfx/native_widget_types.h"
#endif
static_assert(BUILDFLAG(IS_CHROMEOS));
namespace apps {
namespace {
using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
constexpr char kAppInstallParentWindowFound[] =
"Apps.AppInstallParentWindowFound";
#endif
constexpr std::string_view kAppInstallHost = "install-app";
constexpr std::string_view kAppInstallPath = "//install-app";
constexpr std::string_view kAppInstallPackageIdParam = "package_id";
constexpr std::string_view kAppInstallSourceParam = "source";
constexpr size_t kMaxDecodeLength = 2048;
AppInstallSurface SourceParamToAppInstallSurface(std::string_view source) {
if (base::EqualsCaseInsensitiveASCII(source, "showoff")) {
return AppInstallSurface::kAppInstallUriShowoff;
}
if (base::EqualsCaseInsensitiveASCII(source, "mall")) {
return AppInstallSurface::kAppInstallUriMall;
}
if (base::EqualsCaseInsensitiveASCII(source, "getit")) {
return AppInstallSurface::kAppInstallUriGetit;
}
if (base::EqualsCaseInsensitiveASCII(source, "launcher")) {
return AppInstallSurface::kAppInstallUriLauncher;
}
if (base::EqualsCaseInsensitiveASCII(source, "peripherals")) {
return AppInstallSurface::kAppInstallUriPeripherals;
}
return AppInstallSurface::kAppInstallUriUnknown;
}
// Retrieves an identifier to the window we are anchoring to.
std::optional<AppInstallService::WindowIdentifier> GetAnchorWindow(
content::WebContents* web_contents,
AppServiceProxy* proxy) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
return web_contents->GetTopLevelNativeWindow();
#else
static_assert(BUILDFLAG(IS_CHROMEOS_LACROS));
Browser* browser = chrome::FindBrowserWithTab(web_contents);
if (!browser) {
return std::nullopt;
}
CHECK(proxy->BrowserAppInstanceTracker());
const BrowserWindowInstance* browser_window =
proxy->BrowserAppInstanceTracker()->GetBrowserWindowInstance(browser);
const BrowserAppInstance* browser_app =
proxy->BrowserAppInstanceTracker()->GetAppInstance(browser);
if (browser_window) {
base::UmaHistogramBoolean(kAppInstallParentWindowFound, true);
return browser_window->id;
}
if (browser_app) {
base::UmaHistogramBoolean(kAppInstallParentWindowFound, true);
return browser_app->id;
}
// Unexpected to reach here, as the origin of the install dialog must be a
// browser or app window. However, BrowserAppInstanceTracker is operating on
// async window changes over crosapi, so the anchor window might not be
// tracked at this point. In this case the dialog will not be anchored to any
// parent.
DLOG(WARNING) << "App Install Dialog parent not found.";
base::UmaHistogramBoolean(kAppInstallParentWindowFound, false);
return std::nullopt;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
bool IsNavigationUserInitiated(content::NavigationHandle* handle) {
if (!handle->IsRendererInitiated()) {
return true;
}
switch (handle->GetNavigationInitiatorActivationAndAdStatus()) {
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kDidNotStartWithTransientActivation:
return false;
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kStartedWithTransientActivationFromNonAd:
case blink::mojom::NavigationInitiatorActivationAndAdStatus::
kStartedWithTransientActivationFromAd:
return true;
}
}
} // namespace
// static
base::OnceCallback<void(bool created)>&
AppInstallNavigationThrottle::MaybeCreateCallbackForTesting() {
static base::NoDestructor<base::OnceCallback<void(bool created)>> callback;
return *callback;
}
// static
std::unique_ptr<content::NavigationThrottle>
AppInstallNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
std::unique_ptr<content::NavigationThrottle> throttle;
if (IsNavigationUserInitiated(handle)) {
throttle = std::make_unique<apps::AppInstallNavigationThrottle>(handle);
}
if (MaybeCreateCallbackForTesting()) {
std::move(MaybeCreateCallbackForTesting()).Run(static_cast<bool>(throttle));
}
return throttle;
}
AppInstallNavigationThrottle::QueryParams::QueryParams() = default;
AppInstallNavigationThrottle::QueryParams::QueryParams(
std::optional<std::string> serialized_package_id,
AppInstallSurface source)
: serialized_package_id(std::move(serialized_package_id)), source(source) {}
AppInstallNavigationThrottle::QueryParams::QueryParams(QueryParams&&) = default;
AppInstallNavigationThrottle::QueryParams::~QueryParams() = default;
bool AppInstallNavigationThrottle::QueryParams::operator==(
const QueryParams& other) const {
return serialized_package_id == other.serialized_package_id &&
source == other.source;
}
// static
AppInstallNavigationThrottle::QueryParams
AppInstallNavigationThrottle::ExtractQueryParams(std::string_view query) {
QueryParams result;
url::Component query_slice(0, query.length());
url::Component key_slice;
url::Component value_slice;
while (url::ExtractQueryKeyValue(query, &query_slice, &key_slice,
&value_slice)) {
std::string_view key = query.substr(key_slice.begin, key_slice.len);
auto decode_value = [&]() {
url::RawCanonOutputW<kMaxDecodeLength> decoded_value;
url::DecodeURLEscapeSequences(
query.substr(value_slice.begin, value_slice.len),
url::DecodeURLMode::kUTF8OrIsomorphic, &decoded_value);
// TODO(b/299825321): Make DecodeURLEscapeSequences() work with
// RawCanonOutput to avoid this redundant UTF8 -> UTF16 -> UTF8
// conversion.
return base::UTF16ToUTF8(decoded_value.view());
};
if (key == kAppInstallPackageIdParam) {
std::string serialized_package_id = decode_value();
if (!serialized_package_id.empty()) {
result.serialized_package_id = std::move(serialized_package_id);
}
} else if (key == kAppInstallSourceParam) {
result.source = SourceParamToAppInstallSurface(decode_value());
}
}
return result;
}
AppInstallNavigationThrottle::AppInstallNavigationThrottle(
content::NavigationHandle* navigation_handle)
: content::NavigationThrottle(navigation_handle) {}
AppInstallNavigationThrottle::~AppInstallNavigationThrottle() = default;
const char* AppInstallNavigationThrottle::GetNameForLogging() {
return "AppInstallNavigationThrottle";
}
ThrottleCheckResult AppInstallNavigationThrottle::WillStartRequest() {
return HandleRequest();
}
ThrottleCheckResult AppInstallNavigationThrottle::WillRedirectRequest() {
return HandleRequest();
}
ThrottleCheckResult AppInstallNavigationThrottle::HandleRequest() {
const GURL& url = navigation_handle()->GetURL();
if (!url.SchemeIs(chromeos::kAppInstallUriScheme) &&
!url.SchemeIs(chromeos::kLegacyAppInstallUriScheme)) {
return content::NavigationThrottle::PROCEED;
}
// We accept `cros-apps:install-app` or `cros-apps://install-app`, when parsed
// with an opaque path (no host, path starts with //) or not.
if (url.host() != kAppInstallHost && url.path_piece() != kAppInstallHost &&
url.path_piece() != kAppInstallPath) {
return content::NavigationThrottle::PROCEED;
}
QueryParams query_params = ExtractQueryParams(url.query_piece());
if (!query_params.serialized_package_id.has_value()) {
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
content::WebContents* web_contents = navigation_handle()->GetWebContents();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
auto* proxy = AppServiceProxyFactory::GetForProfile(profile);
std::optional<AppInstallService::WindowIdentifier> anchor_window =
GetAnchorWindow(web_contents, proxy);
proxy->AppInstallService().InstallAppWithFallback(
query_params.source,
std::move(query_params.serialized_package_id).value(), anchor_window,
base::DoNothing());
if (!web_contents->GetLastCommittedURL().is_valid()) {
web_contents->ClosePage();
}
return content::NavigationThrottle::CANCEL_AND_IGNORE;
}
} // namespace apps