blob: 1d4131f34f34783b9650e9ee32c11249cc2ca113 [file] [log] [blame]
// Copyright 2017 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/service_worker_payment_app_finder.h"
#include <algorithm>
#include <vector>
#include "base/base64.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/stl_util.h"
#include "base/supports_user_data.h"
#include "base/task/single_thread_task_runner.h"
#include "components/payments/content/developer_console_logger.h"
#include "components/payments/content/installable_payment_app_crawler.h"
#include "components/payments/content/manifest_verifier.h"
#include "components/payments/content/payment_manifest_web_data_service.h"
#include "components/payments/content/utility/payment_manifest_parser.h"
#include "components/payments/core/features.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/payment_manifest_downloader.h"
#include "components/payments/core/url_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/payment_app_provider.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/stored_payment_app.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "ui/gfx/image/image.h"
#include "url/url_canon.h"
namespace payments {
namespace {
// Returns true if |app| supports at least one of the |requests|.
bool AppSupportsAtLeastOneRequestedMethodData(
const content::StoredPaymentApp& app,
const std::vector<mojom::PaymentMethodDataPtr>& requests) {
for (const auto& enabled_method : app.enabled_methods) {
for (const auto& request : requests) {
if (enabled_method == request->supported_method) {
return true;
}
}
}
return false;
}
void RemovePortNumbersFromScopesForTest(
content::InstalledPaymentAppsFinder::PaymentApps* apps) {
GURL::Replacements replacements;
replacements.ClearPort();
for (auto& app : *apps) {
app.second->scope = app.second->scope.ReplaceComponents(replacements);
}
}
class SelfDeletingServiceWorkerPaymentAppFinder
: public base::SupportsUserData::Data {
public:
static base::WeakPtr<SelfDeletingServiceWorkerPaymentAppFinder>
CreateAndSetOwnedBy(content::WebContents* owner) {
auto owned =
std::make_unique<SelfDeletingServiceWorkerPaymentAppFinder>(owner);
auto* pointer = owned.get();
owner->SetUserData(pointer, std::move(owned));
return pointer->weak_ptr_factory_.GetWeakPtr();
}
explicit SelfDeletingServiceWorkerPaymentAppFinder(
content::WebContents* owner)
: owner_(owner) {}
SelfDeletingServiceWorkerPaymentAppFinder(
const SelfDeletingServiceWorkerPaymentAppFinder& other) = delete;
SelfDeletingServiceWorkerPaymentAppFinder& operator=(
const SelfDeletingServiceWorkerPaymentAppFinder& other) = delete;
~SelfDeletingServiceWorkerPaymentAppFinder() override = default;
// After |callback| has fired, the factory refreshes its own cache in the
// background. Once the cache has been refreshed, the factory invokes the
// |finished_using_resources_callback|. At this point, it's safe to delete
// this factory. Don't destroy the factory and don't call this method again
// until |finished_using_resources_callback| has run.
void GetAllPaymentApps(
const url::Origin& merchant_origin,
content::RenderFrameHost* initiator_render_frame_host,
std::unique_ptr<PaymentManifestDownloader> downloader,
scoped_refptr<PaymentManifestWebDataService> cache,
const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
ServiceWorkerPaymentAppFinder::GetAllPaymentAppsCallback callback,
base::OnceClosure finished_using_resources_callback) {
DCHECK(!verifier_);
DCHECK(initiator_render_frame_host);
DCHECK(initiator_render_frame_host->IsActive());
downloader_ = std::move(downloader);
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(initiator_render_frame_host);
parser_ = std::make_unique<PaymentManifestParser>(
std::make_unique<DeveloperConsoleLogger>(web_contents));
cache_ = cache;
verifier_ = std::make_unique<ManifestVerifier>(
merchant_origin, web_contents, downloader_.get(), parser_.get(),
cache_.get());
if (base::FeatureList::IsEnabled(
features::kWebPaymentsJustInTimePaymentApp)) {
crawler_ = std::make_unique<InstallablePaymentAppCrawler>(
merchant_origin, initiator_render_frame_host, downloader_.get(),
parser_.get(), cache_.get());
if (ignore_port_in_origin_comparison_for_testing_)
crawler_->IgnorePortInOriginComparisonForTesting();
}
// Method data cannot be copied and is passed in as a const-ref, which
// cannot be moved, so make a manual copy for using below.
for (const auto& method_data : requested_method_data) {
requested_method_data_.emplace_back(method_data.Clone());
}
callback_ = std::move(callback);
finished_using_resources_callback_ =
std::move(finished_using_resources_callback);
content::InstalledPaymentAppsFinder::GetInstance(
initiator_render_frame_host->GetBrowserContext())
->GetAllPaymentApps(base::BindOnce(
&SelfDeletingServiceWorkerPaymentAppFinder::OnGotAllPaymentApps,
weak_ptr_factory_.GetWeakPtr()));
}
void IgnorePortInOriginComparisonForTesting() {
ignore_port_in_origin_comparison_for_testing_ = true;
}
private:
// base::SupportsUserData::Data implementation.
std::unique_ptr<Data> Clone() override {
return nullptr; // Cloning is not supported.
}
static void RemoveUnrequestedMethods(
const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
content::InstalledPaymentAppsFinder::PaymentApps* apps) {
std::set<std::string> requested_methods;
for (const auto& requested_method_datum : requested_method_data) {
requested_methods.insert(requested_method_datum->supported_method);
}
for (auto& app : *apps) {
std::sort(app.second->enabled_methods.begin(),
app.second->enabled_methods.end());
app.second->enabled_methods =
base::STLSetIntersection<std::vector<std::string>>(
app.second->enabled_methods, requested_methods);
}
}
void OnGotAllPaymentApps(
content::InstalledPaymentAppsFinder::PaymentApps apps) {
if (ignore_port_in_origin_comparison_for_testing_)
RemovePortNumbersFromScopesForTest(&apps);
RemoveUnrequestedMethods(requested_method_data_, &apps);
ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData(
requested_method_data_, &apps);
if (apps.empty()) {
OnPaymentAppsVerified(std::move(apps), first_error_message_);
OnPaymentAppsVerifierFinishedUsingResources();
return;
}
// The |verifier_| will invoke |OnPaymentAppsVerified| with the list of all
// valid payment apps. This list may be empty, if none of the apps were
// found to be valid.
is_payment_verifier_finished_using_resources_ = false;
verifier_->Verify(
std::move(apps),
base::BindOnce(
&SelfDeletingServiceWorkerPaymentAppFinder::OnPaymentAppsVerified,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
OnPaymentAppsVerifierFinishedUsingResources,
weak_ptr_factory_.GetWeakPtr()));
}
void OnPaymentAppsVerified(
content::InstalledPaymentAppsFinder::PaymentApps apps,
const std::string& error_message) {
if (first_error_message_.empty())
first_error_message_ = error_message;
installed_apps_ = std::move(apps);
// TODO(crbug.com/1421326): Once kPaymentHandlerAlwaysRefreshIcon is rolled
// out fully, remove the 'missing icons' path and rely on the refresh path
// to handle any payment app that is missing an icon. This will cause fixing
// missing icons to be async rather than synchronous, but by now this is a
// very rare case anyway.
std::set<GURL> method_manifest_urls_for_missing_icons;
std::set<GURL> method_manifest_urls_for_icon_refresh;
for (auto& app : installed_apps_) {
for (const auto& method : app.second->enabled_methods) {
// Only payment methods with manifests are eligible for refetching the
// icon of their installed payment apps.
GURL method_manifest_url = GURL(method);
if (!UrlUtil::IsValidUrlBasedPaymentMethodIdentifier(
method_manifest_url)) {
continue;
}
method_manifest_urls_for_icon_refresh.insert(method_manifest_url);
if (!app.second->icon.get() || app.second->icon.get()->drawsNothing()) {
method_manifest_urls_for_missing_icons.insert(method_manifest_url);
}
}
}
// Crawl installable web payment apps if no web payment apps have been
// installed or when an installed app is missing an icon.
//
// If `crawler_` is null, that indicates that JIT install has been disabled
// entirely, and so we should be doing no crawling.
if ((installed_apps_.empty() ||
!method_manifest_urls_for_missing_icons.empty()) &&
crawler_ != nullptr) {
is_payment_app_crawler_finished_using_resources_ = false;
crawler_->Start(
requested_method_data_,
std::move(method_manifest_urls_for_missing_icons),
base::BindOnce(
&SelfDeletingServiceWorkerPaymentAppFinder::OnPaymentAppsCrawled,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
OnPaymentAppsCrawlerFinishedUsingResources,
weak_ptr_factory_.GetWeakPtr()));
return;
}
// To ensure that payment apps are able to respond to web-app manifest
// changes (such as their icon changing), we refetch the web-app manifest
// for already-installed apps. This process can be slow, so we don't block
// the creation of payment apps on it - the app will be updated in the
// background and changes will take effect on any subsequent Payment Request
// launch.
if (base::FeatureList::IsEnabled(
features::kPaymentHandlerAlwaysRefreshIcon) &&
!method_manifest_urls_for_icon_refresh.empty() && crawler_ != nullptr) {
DCHECK(!installed_apps_.empty());
is_payment_app_crawler_finished_using_resources_ = false;
crawler_->Start(
requested_method_data_,
std::move(method_manifest_urls_for_icon_refresh),
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
OnPaymentAppsCrawledForUpdatedInfo,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
OnPaymentAppsCrawlerFinishedUsingResources,
weak_ptr_factory_.GetWeakPtr()));
// Deliberately copy installed_apps_, as it is still needed in
// |OnPaymentAppsCrawledForUpdatedInfo|.
content::InstalledPaymentAppsFinder::PaymentApps installed_apps_copy;
for (const auto& app : installed_apps_) {
installed_apps_copy[app.first] =
std::make_unique<content::StoredPaymentApp>(*app.second);
}
std::move(callback_).Run(
std::move(installed_apps_copy),
ServiceWorkerPaymentAppFinder::InstallablePaymentApps(),
first_error_message_);
} else {
// Release crawler_ since it will not be used from now on.
crawler_.reset();
std::move(callback_).Run(
std::move(installed_apps_),
ServiceWorkerPaymentAppFinder::InstallablePaymentApps(),
first_error_message_);
}
}
void OnPaymentAppsCrawled(
std::map<GURL, std::unique_ptr<WebAppInstallationInfo>> apps_info,
std::map<GURL, std::unique_ptr<RefetchedIcon>> refetched_icons,
const std::string& error_message) {
if (first_error_message_.empty())
first_error_message_ = error_message;
UpdatePaymentAppIcons(refetched_icons);
std::move(callback_).Run(std::move(installed_apps_), std::move(apps_info),
first_error_message_);
}
void OnPaymentAppsCrawledForUpdatedInfo(
std::map<GURL, std::unique_ptr<WebAppInstallationInfo>> apps_info,
std::map<GURL, std::unique_ptr<RefetchedIcon>> refetched_icons,
// We deliberately ignore the error message, as this method is an optional
// asynchronous update - if it failed, it is ok to fail silently.
const std::string& ignored_error_message) {
// This crawl should only have been triggered for refetched icons, and in
// that mode the crawler should not suggest installable apps to us.
DCHECK(apps_info.empty());
// TODO(crbug.com/1421326): Consider optimizing either this database write
// or the entire re-crawling process to avoid fetching/saving icons when
// nothing has changed in the manifest.
UpdatePaymentAppIcons(refetched_icons);
}
void UpdatePaymentAppIcons(
const std::map<GURL, std::unique_ptr<RefetchedIcon>>& refetched_icons) {
for (auto& refetched_icon : refetched_icons) {
GURL web_app_manifest_url = refetched_icon.first;
RefetchedIcon* data = refetched_icon.second.get();
for (auto& app : installed_apps_) {
// It is possible (unlikely) to have multiple apps with same origins.
// The proper validation is to store web_app_manifest_url in
// StoredPaymentApp and confirm that it is the same as the
// web_app_manifest_url from which icon is fetched.
if (crawler_->IsSameOriginWith(GURL(app.second->scope),
web_app_manifest_url)) {
UpdatePaymentAppIcon(app.second, data->icon, data->method_name);
app.second->icon = std::move(data->icon);
break;
}
}
}
}
void UpdatePaymentAppIcon(
const std::unique_ptr<content::StoredPaymentApp>& app,
const std::unique_ptr<SkBitmap>& icon,
const std::string& method_name) {
number_of_app_icons_to_update_++;
DCHECK(!icon->empty());
gfx::Image decoded_image = gfx::Image::CreateFrom1xBitmap(*(icon));
scoped_refptr<base::RefCountedMemory> raw_data =
decoded_image.As1xPNGBytes();
std::string string_encoded_icon = base::Base64Encode(*raw_data);
content::PaymentAppProvider::GetOrCreateForWebContents(owner_)
->UpdatePaymentAppIcon(
app->registration_id, app->scope.spec(), app->name,
string_encoded_icon, method_name, app->supported_delegations,
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
OnUpdatePaymentAppIcon,
weak_ptr_factory_.GetWeakPtr()));
}
void OnUpdatePaymentAppIcon(payments::mojom::PaymentHandlerStatus status) {
DCHECK(number_of_app_icons_to_update_ > 0);
number_of_app_icons_to_update_--;
if (number_of_app_icons_to_update_ == 0)
FinishUsingResourcesIfReady();
}
void OnPaymentAppsCrawlerFinishedUsingResources() {
crawler_.reset();
is_payment_app_crawler_finished_using_resources_ = true;
FinishUsingResourcesIfReady();
}
void OnPaymentAppsVerifierFinishedUsingResources() {
verifier_.reset();
is_payment_verifier_finished_using_resources_ = true;
FinishUsingResourcesIfReady();
}
void FinishUsingResourcesIfReady() {
if (is_payment_verifier_finished_using_resources_ &&
is_payment_app_crawler_finished_using_resources_ &&
!finished_using_resources_callback_.is_null() &&
number_of_app_icons_to_update_ == 0) {
downloader_.reset();
parser_.reset();
std::move(finished_using_resources_callback_).Run();
base::SingleThreadTaskRunner::GetCurrentDefault()->PostNonNestableTask(
FROM_HERE,
base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::DeleteSelf,
weak_ptr_factory_.GetWeakPtr()));
}
}
void DeleteSelf() { owner_->RemoveUserData(this); }
// |owner_| owns this SelfDeletingServiceWorkerPaymentAppFinder, so it is
// always valid.
raw_ptr<content::WebContents> owner_;
std::unique_ptr<PaymentManifestDownloader> downloader_;
std::unique_ptr<PaymentManifestParser> parser_;
scoped_refptr<PaymentManifestWebDataService> cache_;
std::vector<mojom::PaymentMethodDataPtr> requested_method_data_;
ServiceWorkerPaymentAppFinder::GetAllPaymentAppsCallback callback_;
base::OnceClosure finished_using_resources_callback_;
std::string first_error_message_;
std::unique_ptr<ManifestVerifier> verifier_;
bool is_payment_verifier_finished_using_resources_ = true;
std::unique_ptr<InstallablePaymentAppCrawler> crawler_;
bool is_payment_app_crawler_finished_using_resources_ = true;
bool ignore_port_in_origin_comparison_for_testing_ = false;
content::InstalledPaymentAppsFinder::PaymentApps installed_apps_;
size_t number_of_app_icons_to_update_ = 0;
base::WeakPtrFactory<SelfDeletingServiceWorkerPaymentAppFinder>
weak_ptr_factory_{this};
};
} // namespace
ServiceWorkerPaymentAppFinder::~ServiceWorkerPaymentAppFinder() = default;
void ServiceWorkerPaymentAppFinder::GetAllPaymentApps(
const url::Origin& merchant_origin,
scoped_refptr<PaymentManifestWebDataService> cache,
std::vector<mojom::PaymentMethodDataPtr> requested_method_data,
base::WeakPtr<CSPChecker> csp_checker,
GetAllPaymentAppsCallback callback,
base::OnceClosure finished_writing_cache_callback_for_testing) {
DCHECK(!requested_method_data.empty());
if (!render_frame_host().IsActive())
return;
// Do not look up payment handlers for ignored payment methods.
std::erase_if(requested_method_data,
[&](const mojom::PaymentMethodDataPtr& method_data) {
return base::Contains(ignored_methods_,
method_data->supported_method);
});
if (requested_method_data.empty()) {
std::move(callback).Run(
content::InstalledPaymentAppsFinder::PaymentApps(),
std::map<GURL, std::unique_ptr<WebAppInstallationInfo>>(),
/*error_message=*/"");
return;
}
auto* web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host());
auto self_delete_factory =
SelfDeletingServiceWorkerPaymentAppFinder::CreateAndSetOwnedBy(
web_contents);
std::unique_ptr<PaymentManifestDownloader> downloader;
if (test_downloader_ != nullptr) {
test_downloader_->SetCSPCheckerForTesting(csp_checker); // IN-TEST
downloader = std::move(test_downloader_);
self_delete_factory->IgnorePortInOriginComparisonForTesting();
} else {
downloader = std::make_unique<payments::PaymentManifestDownloader>(
std::make_unique<DeveloperConsoleLogger>(web_contents), csp_checker,
render_frame_host()
.GetBrowserContext()
->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess());
}
self_delete_factory->GetAllPaymentApps(
merchant_origin, &render_frame_host(), std::move(downloader), cache,
requested_method_data, std::move(callback),
std::move(finished_writing_cache_callback_for_testing));
}
// static
void ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData(
const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
content::InstalledPaymentAppsFinder::PaymentApps* apps) {
for (auto it = apps->begin(); it != apps->end();) {
if (AppSupportsAtLeastOneRequestedMethodData(*it->second,
requested_method_data)) {
++it;
} else {
it = apps->erase(it);
}
}
}
void ServiceWorkerPaymentAppFinder::IgnorePaymentMethodForTest(
const std::string& method) {
ignored_methods_.insert(method);
}
ServiceWorkerPaymentAppFinder::ServiceWorkerPaymentAppFinder(
content::RenderFrameHost* rfh)
: content::DocumentUserData<ServiceWorkerPaymentAppFinder>(rfh),
ignored_methods_({methods::kGooglePlayBilling}),
test_downloader_(nullptr) {}
void ServiceWorkerPaymentAppFinder::
SetDownloaderAndIgnorePortInOriginComparisonForTesting(
std::unique_ptr<PaymentManifestDownloader> downloader) {
test_downloader_ = std::move(downloader);
}
DOCUMENT_USER_DATA_KEY_IMPL(ServiceWorkerPaymentAppFinder);
} // namespace payments