blob: f50f6a7eaf21b3c53d193f3f0c4b0c45be7538bd [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/installable_payment_app_crawler.h"
#include <limits>
#include <utility>
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/payments/content/icon/icon_size.h"
#include "components/payments/core/features.h"
#include "components/payments/core/native_error_strings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/payment_app_provider_util.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
namespace payments {
RefetchedMetadata::RefetchedMetadata() = default;
RefetchedMetadata::~RefetchedMetadata() = default;
// TODO(crbug.com/40548519): Use cache to accelerate crawling procedure.
InstallablePaymentAppCrawler::InstallablePaymentAppCrawler(
const url::Origin& merchant_origin,
content::RenderFrameHost* initiator_render_frame_host,
PaymentManifestDownloader* downloader,
PaymentManifestParser* parser,
WebPaymentsWebDataService* cache)
: log_(content::WebContents::FromRenderFrameHost(
initiator_render_frame_host)),
merchant_origin_(merchant_origin),
initiator_frame_routing_id_(initiator_render_frame_host->GetGlobalId()),
downloader_(downloader),
parser_(parser),
number_of_payment_method_manifest_to_download_(0),
number_of_payment_method_manifest_to_parse_(0),
number_of_web_app_manifest_to_download_(0),
number_of_web_app_manifest_to_parse_(0),
number_of_web_app_icons_to_download_and_decode_(0) {}
InstallablePaymentAppCrawler::~InstallablePaymentAppCrawler() = default;
void InstallablePaymentAppCrawler::Start(
const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
std::set<GURL> method_manifest_urls_for_metadata_refresh,
FinishedCrawlingCallback callback,
base::OnceClosure finished_using_resources) {
callback_ = std::move(callback);
finished_using_resources_ = std::move(finished_using_resources);
std::set<GURL> manifests_to_download;
if (method_manifest_urls_for_metadata_refresh.empty()) {
// Crawl for JIT installable web apps.
crawling_mode_ = CrawlingMode::kJustInTimeInstallation;
for (const auto& method_data : requested_method_data) {
if (!base::IsStringUTF8(method_data->supported_method))
continue;
GURL url = GURL(method_data->supported_method);
if (url.is_valid()) {
manifests_to_download.insert(url);
}
}
} else {
// Crawl to refresh metadata for already installed apps.
crawling_mode_ = CrawlingMode::kInstalledAppMetadataRefresh;
method_manifest_urls_for_metadata_refresh_ =
std::move(method_manifest_urls_for_metadata_refresh);
for (const auto& method : method_manifest_urls_for_metadata_refresh_) {
DCHECK(method.is_valid());
manifests_to_download.insert(method);
}
}
if (manifests_to_download.empty()) {
// Post the result back asynchronously.
PostTaskToFinishCrawlingPaymentAppsIfReady();
return;
}
// May cause this InstallablePaymentAppCrawler object to be synchronously
// deleted in the last iteration, so no code should come after the loop.
number_of_payment_method_manifest_to_download_ = manifests_to_download.size();
for (const auto& url : manifests_to_download) {
downloader_->DownloadPaymentMethodManifest(
merchant_origin_, url,
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentMethodManifestDownloaded,
weak_ptr_factory_.GetWeakPtr(), url));
}
}
void InstallablePaymentAppCrawler::IgnorePortInOriginComparisonForTesting() {
ignore_port_in_origin_comparison_for_testing_ = true;
}
bool InstallablePaymentAppCrawler::IsSameOriginWith(const GURL& a,
const GURL& b) {
if (ignore_port_in_origin_comparison_for_testing_) {
GURL::Replacements replacements;
replacements.ClearPort();
return url::IsSameOriginWith(a.ReplaceComponents(replacements),
b.ReplaceComponents(replacements));
}
return url::IsSameOriginWith(a, b);
}
void InstallablePaymentAppCrawler::OnPaymentMethodManifestDownloaded(
const GURL& method_manifest_url,
const GURL& method_manifest_url_after_redirects,
const std::string& content,
const std::string& error_message) {
// Enforced in PaymentManifestDownloader.
DCHECK(net::registry_controlled_domains::SameDomainOrHost(
method_manifest_url, method_manifest_url_after_redirects,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));
number_of_payment_method_manifest_to_download_--;
if (content.empty()) {
SetFirstError(error_message);
FinishCrawlingPaymentAppsIfReady();
return;
}
number_of_payment_method_manifest_to_parse_++;
parser_->ParsePaymentMethodManifest(
method_manifest_url, content,
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentMethodManifestParsed,
weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
method_manifest_url_after_redirects, content));
}
void InstallablePaymentAppCrawler::OnPaymentMethodManifestParsed(
const GURL& method_manifest_url,
const GURL& method_manifest_url_after_redirects,
const std::string& content,
const std::vector<GURL>& default_applications,
const std::vector<url::Origin>& supported_origins) {
number_of_payment_method_manifest_to_parse_--;
auto* rfh = content::RenderFrameHost::FromID(initiator_frame_routing_id_);
if (!rfh)
return;
content::PermissionController* permission_controller =
rfh->GetBrowserContext()->GetPermissionController();
DCHECK(permission_controller);
// If there are no valid entries in default_applications, this task will
// finish the crawling.
PostTaskToFinishCrawlingPaymentAppsIfReady();
// The `DownloadWebAppManifest()` method may synchronously call
// `OnPaymentWebAppManifestDownloaded()`, e.g., if the owning page has gone
// away already. This may result in this InstallablePaymentAppCrawler object
// to be deleted, so no code should be run after this loop.
//
// Note that only the last iteration of the loop can result in a deletion, as
// `number_of_web_app_manifest_to_download_` must be zero for this to happen.
number_of_web_app_manifest_to_download_ += default_applications.size();
for (const auto& web_app_manifest_url : default_applications) {
if (downloaded_web_app_manifests_.find(web_app_manifest_url) !=
downloaded_web_app_manifests_.end()) {
// Do not download the same web app manifest again since a web app could
// be the default application of multiple payment methods.
number_of_web_app_manifest_to_download_--;
continue;
}
if (!IsSameOriginWith(method_manifest_url_after_redirects,
web_app_manifest_url)) {
number_of_web_app_manifest_to_download_--;
std::string error_message = base::ReplaceStringPlaceholders(
errors::kCrossOriginWebAppManifestNotAllowed,
{web_app_manifest_url.spec(),
method_manifest_url_after_redirects.spec()},
nullptr);
SetFirstError(error_message);
continue;
}
if (permission_controller
->GetPermissionResultForOriginWithoutContext(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::PAYMENT_HANDLER),
url::Origin::Create(web_app_manifest_url))
.status != blink::mojom::PermissionStatus::GRANTED) {
// Do not download the web app manifest if it is blocked.
number_of_web_app_manifest_to_download_--;
continue;
}
downloaded_web_app_manifests_.insert(web_app_manifest_url);
if (method_manifest_url_after_redirects == web_app_manifest_url) {
// For this to happen, the payment manifest must have been valid which
// means its content should have been non-empty. If somehow we get here
// but content is empty, it would cause a synchronous deletion of 'this',
// so guard against that.
CHECK(!content.empty());
OnPaymentWebAppManifestDownloaded(
method_manifest_url, web_app_manifest_url, web_app_manifest_url,
content, /*error_message=*/"");
continue;
}
// May cause this InstallablePaymentAppCrawler object to be synchronously
// deleted in the last iteration of the loop.
downloader_->DownloadWebAppManifest(
url::Origin::Create(method_manifest_url_after_redirects),
web_app_manifest_url,
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentWebAppManifestDownloaded,
weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
web_app_manifest_url));
}
}
void InstallablePaymentAppCrawler::OnPaymentWebAppManifestDownloaded(
const GURL& method_manifest_url,
const GURL& web_app_manifest_url,
const GURL& web_app_manifest_url_after_redirects,
const std::string& content,
const std::string& error_message) {
#if DCHECK_IS_ON()
GURL::Replacements replacements;
if (ignore_port_in_origin_comparison_for_testing_)
replacements.ClearPort();
// Enforced in PaymentManifestDownloader.
DCHECK_EQ(
web_app_manifest_url.ReplaceComponents(replacements),
web_app_manifest_url_after_redirects.ReplaceComponents(replacements));
#endif // DCHECK_IS_ON()
number_of_web_app_manifest_to_download_--;
if (content.empty()) {
SetFirstError(error_message);
FinishCrawlingPaymentAppsIfReady();
return;
}
number_of_web_app_manifest_to_parse_++;
parser_->ParseWebAppInstallationInfo(
content,
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentWebAppInstallationInfo,
weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
web_app_manifest_url));
}
void InstallablePaymentAppCrawler::OnPaymentWebAppInstallationInfo(
const GURL& method_manifest_url,
const GURL& web_app_manifest_url,
std::unique_ptr<WebAppInstallationInfo> app_info,
std::unique_ptr<std::vector<PaymentManifestParser::WebAppIcon>> icons) {
number_of_web_app_manifest_to_parse_--;
// Only download and decode payment app's icon if it is valid and stored.
if (CompleteAndStorePaymentWebAppInfoIfValid(
method_manifest_url, web_app_manifest_url, std::move(app_info))) {
if (!DownloadAndDecodeWebAppIcon(method_manifest_url, web_app_manifest_url,
std::move(icons)) &&
crawling_mode_ == CrawlingMode::kJustInTimeInstallation &&
!base::FeatureList::IsEnabled(
features::kAllowJITInstallationWhenAppIconIsMissing)) {
std::string error_message = base::ReplaceStringPlaceholders(
errors::kInvalidWebAppIcon, {web_app_manifest_url.spec()}, nullptr);
SetFirstError(error_message);
// App without a valid icon is not JIT installable.
installable_apps_.erase(method_manifest_url);
}
}
FinishCrawlingPaymentAppsIfReady();
}
bool InstallablePaymentAppCrawler::CompleteAndStorePaymentWebAppInfoIfValid(
const GURL& method_manifest_url,
const GURL& web_app_manifest_url,
std::unique_ptr<WebAppInstallationInfo> app_info) {
if (app_info == nullptr)
return false;
if (app_info->sw_js_url.empty() || !base::IsStringUTF8(app_info->sw_js_url)) {
SetFirstError(errors::kInvalidServiceWorkerUrl);
return false;
}
// Check and complete relative url.
if (!GURL(app_info->sw_js_url).is_valid()) {
GURL absolute_url = web_app_manifest_url.Resolve(app_info->sw_js_url);
if (!absolute_url.is_valid()) {
SetFirstError(base::ReplaceStringPlaceholders(
errors::kCannotResolveServiceWorkerUrl,
{app_info->sw_js_url, web_app_manifest_url.spec()}, nullptr));
return false;
}
app_info->sw_js_url = absolute_url.spec();
}
if (!IsSameOriginWith(web_app_manifest_url, GURL(app_info->sw_js_url))) {
SetFirstError(base::ReplaceStringPlaceholders(
errors::kCrossOriginServiceWorkerUrlNotAllowed,
{app_info->sw_js_url, web_app_manifest_url.spec()}, nullptr));
return false;
}
if (!app_info->sw_scope.empty() && !base::IsStringUTF8(app_info->sw_scope)) {
SetFirstError(errors::kInvalidServiceWorkerScope);
return false;
}
if (!GURL(app_info->sw_scope).is_valid()) {
GURL absolute_scope =
web_app_manifest_url.GetWithoutFilename().Resolve(app_info->sw_scope);
if (!absolute_scope.is_valid()) {
SetFirstError(base::ReplaceStringPlaceholders(
errors::kCannotResolveServiceWorkerScope,
{app_info->sw_scope, web_app_manifest_url.spec()}, nullptr));
return false;
}
app_info->sw_scope = absolute_scope.spec();
}
if (!IsSameOriginWith(web_app_manifest_url, GURL(app_info->sw_scope))) {
SetFirstError(base::ReplaceStringPlaceholders(
errors::kCrossOriginServiceWorkerScopeNotAllowed,
{app_info->sw_scope, web_app_manifest_url.spec()}, nullptr));
return false;
}
std::string error_message;
if (!content::PaymentAppProviderUtil::IsValidInstallablePaymentApp(
web_app_manifest_url, GURL(app_info->sw_js_url),
GURL(app_info->sw_scope), &error_message)) {
SetFirstError(error_message);
return false;
}
// TODO(crbug.com/40548519): Support multiple installable payment apps for a
// payment method.
if (installable_apps_.find(method_manifest_url) != installable_apps_.end()) {
SetFirstError(errors::kInstallingMultipleDefaultAppsNotSupported);
return false;
}
// If this is called by tests, convert service worker URLs back to test server
// URLs so the service worker code can be fetched and installed. This is done
// last so same-origin checks are performed on the "logical" URLs instead of
// real test URLs with ports.
if (ignore_port_in_origin_comparison_for_testing_) {
app_info->sw_js_url =
downloader_->FindTestServerURL(GURL(app_info->sw_js_url)).spec();
app_info->sw_scope =
downloader_->FindTestServerURL(GURL(app_info->sw_scope)).spec();
}
switch (crawling_mode_) {
case CrawlingMode::kJustInTimeInstallation:
installable_apps_[method_manifest_url] = std::move(app_info);
break;
case CrawlingMode::kInstalledAppMetadataRefresh: {
auto refetched_metadata = std::make_unique<RefetchedMetadata>();
refetched_metadata->method_name = method_manifest_url.spec();
refetched_metadata->supported_delegations =
app_info->supported_delegations;
refetched_app_metadata_.insert(
std::make_pair(web_app_manifest_url, std::move(refetched_metadata)));
break;
}
}
return true;
}
bool InstallablePaymentAppCrawler::DownloadAndDecodeWebAppIcon(
const GURL& method_manifest_url,
const GURL& web_app_manifest_url,
std::unique_ptr<std::vector<PaymentManifestParser::WebAppIcon>> icons) {
if (icons == nullptr || icons->empty()) {
log_.Warn(
"No valid icon information for installable payment handler found in "
"web app manifest \"" +
web_app_manifest_url.spec() + "\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
return false;
}
std::vector<blink::Manifest::ImageResource> manifest_icons;
for (const auto& icon : *icons) {
if (icon.src.empty() || !base::IsStringUTF8(icon.src)) {
log_.Warn(
"The installable payment handler's icon src URL is not a non-empty "
"UTF8 string in web app manifest \"" +
web_app_manifest_url.spec() + "\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
continue;
}
GURL icon_src = GURL(icon.src);
if (!icon_src.is_valid()) {
icon_src = web_app_manifest_url.Resolve(icon.src);
if (!icon_src.is_valid()) {
log_.Warn(
"Failed to resolve the installable payment handler's icon src url "
"\"" +
icon.src + "\" in web app manifest \"" +
web_app_manifest_url.spec() + "\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
continue;
}
}
blink::Manifest::ImageResource manifest_icon;
manifest_icon.src = icon_src;
manifest_icon.type = base::UTF8ToUTF16(icon.type);
manifest_icon.purpose.emplace_back(
blink::mojom::ManifestImageResource_Purpose::ANY);
// TODO(crbug.com/40548519): Parse icon sizes.
manifest_icon.sizes.emplace_back(gfx::Size());
manifest_icons.emplace_back(manifest_icon);
}
if (manifest_icons.empty()) {
log_.Warn("No valid icons found in web app manifest \"" +
web_app_manifest_url.spec() +
"\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
return false;
}
// If the initiator frame doesn't exists any more, e.g. the frame has
// navigated away, don't download the icon.
// TODO(crbug.com/40121328): Move this sanity check to ManifestIconDownloader
// after DownloadImage refactor is done.
auto* rfh = content::RenderFrameHost::FromID(initiator_frame_routing_id_);
auto* web_contents = rfh && rfh->IsActive()
? content::WebContents::FromRenderFrameHost(rfh)
: nullptr;
if (!web_contents) {
log_.Warn(
"Cannot download icons after the webpage has been closed (web app "
"manifest \"" +
web_app_manifest_url.spec() + "\" for payment handler manifest \"" +
method_manifest_url.spec() + "\").");
// Post the result back asynchronously.
PostTaskToFinishCrawlingPaymentAppsIfReady();
return false;
}
gfx::NativeView native_view = web_contents->GetNativeView();
GURL best_icon_url = blink::ManifestIconSelector::FindBestMatchingIcon(
manifest_icons, IconSizeCalculator::IdealIconHeight(native_view),
IconSizeCalculator::MinimumIconHeight(),
content::ManifestIconDownloader::kMaxWidthToHeightRatio,
blink::mojom::ManifestImageResource_Purpose::ANY);
if (!best_icon_url.is_valid()) {
log_.Warn("No suitable icon found in web app manifest \"" +
web_app_manifest_url.spec() +
"\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
return false;
}
number_of_web_app_icons_to_download_and_decode_++;
bool can_download_icon = content::ManifestIconDownloader::Download(
web_contents, downloader_->FindTestServerURL(best_icon_url),
IconSizeCalculator::IdealIconHeight(native_view),
IconSizeCalculator::MinimumIconHeight(),
/* maximum_icon_size_in_px= */ std::numeric_limits<int>::max(),
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentWebAppIconDownloadAndDecoded,
weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
web_app_manifest_url),
false, /* square_only */
initiator_frame_routing_id_);
DCHECK(can_download_icon);
return can_download_icon;
}
void InstallablePaymentAppCrawler::OnPaymentWebAppIconDownloadAndDecoded(
const GURL& method_manifest_url,
const GURL& web_app_manifest_url,
const SkBitmap& icon) {
number_of_web_app_icons_to_download_and_decode_--;
switch (crawling_mode_) {
case CrawlingMode::kJustInTimeInstallation: {
auto it = installable_apps_.find(method_manifest_url);
CHECK(it != installable_apps_.end());
DCHECK(
IsSameOriginWith(GURL(it->second->sw_scope), web_app_manifest_url));
if (icon.drawsNothing() &&
!base::FeatureList::IsEnabled(
features::kAllowJITInstallationWhenAppIconIsMissing)) {
log_.Error(
"Failed to download or decode the icon from web app manifest \"" +
web_app_manifest_url.spec() + "\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
std::string error_message = base::ReplaceStringPlaceholders(
errors::kInvalidWebAppIcon, {web_app_manifest_url.spec()}, nullptr);
SetFirstError(error_message);
installable_apps_.erase(it);
} else {
it->second->icon = std::make_unique<SkBitmap>(icon);
}
break;
}
case CrawlingMode::kInstalledAppMetadataRefresh: {
auto it =
method_manifest_urls_for_metadata_refresh_.find(method_manifest_url);
CHECK(it != method_manifest_urls_for_metadata_refresh_.end());
if (icon.drawsNothing()) {
log_.Warn("Failed to refetch a valid icon from web app manifest \"" +
web_app_manifest_url.spec() +
"\" for payment handler manifest \"" +
method_manifest_url.spec() + "\".");
} else {
CHECK(refetched_app_metadata_.contains(web_app_manifest_url));
refetched_app_metadata_[web_app_manifest_url]->icon =
std::make_unique<SkBitmap>(icon);
}
break;
}
}
FinishCrawlingPaymentAppsIfReady();
}
void InstallablePaymentAppCrawler::
PostTaskToFinishCrawlingPaymentAppsIfReady() {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&InstallablePaymentAppCrawler::FinishCrawlingPaymentAppsIfReady,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallablePaymentAppCrawler::FinishCrawlingPaymentAppsIfReady() {
if (number_of_payment_method_manifest_to_download_ != 0 ||
number_of_payment_method_manifest_to_parse_ != 0 ||
number_of_web_app_manifest_to_download_ != 0 ||
number_of_web_app_manifest_to_parse_ != 0 ||
number_of_web_app_icons_to_download_and_decode_ != 0) {
return;
}
std::move(callback_).Run(std::move(installable_apps_),
std::move(refetched_app_metadata_),
first_error_message_);
std::move(finished_using_resources_).Run();
}
void InstallablePaymentAppCrawler::SetFirstError(
const std::string& error_message) {
log_.Error(error_message);
if (first_error_message_.empty())
first_error_message_ = error_message;
}
} // namespace payments.