blob: e18f953a3f5759c7c71c887145addf8256ba24a5 [file] [log] [blame]
// Copyright 2019 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/manifest_update_manager.h"
#include <map>
#include <memory>
#include <tuple>
#include <utility>
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/manifest_update_utils.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.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_constants.h"
#include "chrome/browser/web_applications/web_app_management_type.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/page_load_metrics/browser/metrics_web_contents_observer.h"
#include "components/webapps/browser/features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/web_applications/web_app_system_web_app_delegate_map_utils.h"
#endif
class Profile;
namespace web_app {
namespace {
// Returns a shared instance of UpdatePendingCallback.
ManifestUpdateManager::UpdatePendingCallback*
GetUpdatePendingCallbackMutableForTesting() {
static base::NoDestructor<ManifestUpdateManager::UpdatePendingCallback>
g_update_pending_callback;
return g_update_pending_callback.get();
}
ManifestUpdateManager::ResultCallback* GetResultCallbackMutableForTesting() {
static base::NoDestructor<ManifestUpdateManager::ResultCallback>
g_result_callback;
return g_result_callback.get();
}
} // namespace
ManifestUpdateManager::ScopedBypassWindowCloseWaitingForTesting::
ScopedBypassWindowCloseWaitingForTesting() {
BypassWindowCloseWaitingForTesting() = true; // IN-TEST
}
ManifestUpdateManager::ScopedBypassWindowCloseWaitingForTesting::
~ScopedBypassWindowCloseWaitingForTesting() {
BypassWindowCloseWaitingForTesting() = false; // IN-TEST
}
// TODO(crbug.com/40272003): Also handle DidFinishNavigation() and
// do not start the ManifestUpdateCheckCommand if different origin
// navigation happens.
class ManifestUpdateManager::PreUpdateWebContentsObserver
: public content::WebContentsObserver {
public:
PreUpdateWebContentsObserver(base::OnceClosure load_complete_callback,
content::WebContents* contents,
bool hang_task_callback_for_testing)
: content::WebContentsObserver(contents),
load_complete_callback_(std::move(load_complete_callback)),
hang_task_callback_for_testing_(hang_task_callback_for_testing) {}
private:
bool IsInvalidRenderFrameHost(content::RenderFrameHost* render_frame_host) {
return !render_frame_host || !render_frame_host->IsInPrimaryMainFrame();
}
// content::WebContentsObserver:
// TODO(crbug.com/40873503): Investigate what other functions can be observed
// so that for WebAppIntegrationTestDriver::CloseCustomToolbar(), the same
// observer can be used.
void DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) override {
if (IsInvalidRenderFrameHost(render_frame_host)) {
return;
}
page_load_complete_ = true;
MaybeRunLoadCompleteCallback();
}
// This is triggered when the manifest URL gets updated for a page,
// see WebContentsImpl::OnManifestUrlChanged() for more information.
void DidUpdateWebManifestURL(content::RenderFrameHost* target_frame,
const GURL& manifest_url) override {
if (IsInvalidRenderFrameHost(target_frame) || !manifest_url.is_valid()) {
return;
}
current_manifest_url_valid_ = true;
MaybeRunLoadCompleteCallback();
}
void WebContentsDestroyed() override {
Observe(nullptr);
if (load_complete_callback_) {
std::move(load_complete_callback_).Run();
}
}
// The final load complete callback is only run once the page has finished
// loading and the manifest url for the page is valid.
void MaybeRunLoadCompleteCallback() {
if (!page_load_complete_ || !current_manifest_url_valid_ ||
hang_task_callback_for_testing_) {
return;
}
Observe(nullptr);
if (load_complete_callback_) {
std::move(load_complete_callback_).Run();
}
}
base::OnceClosure load_complete_callback_;
bool hang_task_callback_for_testing_;
bool current_manifest_url_valid_ = false;
bool page_load_complete_ = false;
};
constexpr base::TimeDelta kDelayBetweenChecks = base::Days(1);
constexpr const char kDisableManifestUpdateThrottle[] =
"disable-manifest-update-throttle";
// static
void ManifestUpdateManager::SetUpdatePendingCallbackForTesting(
UpdatePendingCallback callback) {
*GetUpdatePendingCallbackMutableForTesting() = // IN-TEST
std::move(callback);
}
// static
void ManifestUpdateManager::SetResultCallbackForTesting(
ResultCallback callback) {
*GetResultCallbackMutableForTesting() = // IN-TEST
std::move(callback);
}
// static
bool& ManifestUpdateManager::BypassWindowCloseWaitingForTesting() {
static bool bypass_window_close_waiting_for_testing_ = false;
return bypass_window_close_waiting_for_testing_;
}
ManifestUpdateManager::ManifestUpdateManager() = default;
ManifestUpdateManager::~ManifestUpdateManager() = default;
#if BUILDFLAG(IS_CHROMEOS)
void ManifestUpdateManager::SetSystemWebAppDelegateMap(
const ash::SystemWebAppDelegateMap* system_web_apps_delegate_map) {
system_web_apps_delegate_map_ = system_web_apps_delegate_map;
}
#endif
void ManifestUpdateManager::SetProvider(base::PassKey<WebAppProvider>,
WebAppProvider& provider) {
provider_ = &provider;
}
void ManifestUpdateManager::Start() {
install_manager_observation_.Observe(&provider_->install_manager());
CHECK(!started_);
started_ = true;
}
void ManifestUpdateManager::Shutdown() {
install_manager_observation_.Reset();
update_stages_.clear();
started_ = false;
}
void ManifestUpdateManager::MaybeUpdate(
const GURL& url,
const std::optional<webapps::AppId>& app_id,
content::WebContents* web_contents) {
if (!started_) {
return;
}
if (!app_id.has_value() || !provider_->registrar_unsafe().AppMatches(
*app_id, WebAppFilter::InstalledInChrome())) {
NotifyResult(url, app_id, ManifestUpdateResult::kNoAppInScope);
return;
}
// Skip the cases when the app's scope and the site mismatch e.g. scope
// extensions.
if (provider_->registrar_unsafe().GetUrlInAppScopeScore(
url.spec(), app_id.value()) == 0) {
NotifyResult(url, app_id, ManifestUpdateResult::kNoAppInScope);
return;
}
#if BUILDFLAG(IS_CHROMEOS)
if (system_web_apps_delegate_map_ &&
IsSystemWebApp(provider_->registrar_unsafe(),
*system_web_apps_delegate_map_, *app_id)) {
NotifyResult(url, *app_id, ManifestUpdateResult::kAppIsSystemWebApp);
return;
}
#endif
if (provider_->registrar_unsafe().IsPlaceholderApp(
*app_id, WebAppManagement::kPolicy) ||
provider_->registrar_unsafe().IsPlaceholderApp(
*app_id, WebAppManagement::kKiosk)) {
NotifyResult(url, *app_id, ManifestUpdateResult::kAppIsPlaceholder);
return;
}
if (provider_->registrar_unsafe().IsIsolated(*app_id)) {
// Manifests of Isolated Web Apps are only updated when a new version of the
// app is installed.
NotifyResult(url, *app_id, ManifestUpdateResult::kAppIsIsolatedWebApp);
return;
}
if (base::Contains(update_stages_, *app_id)) {
return;
}
base::Time check_time =
time_override_for_testing_.value_or(base::Time::Now());
if (!MaybeConsumeUpdateCheck(url.DeprecatedGetOriginAsURL(), *app_id,
check_time)) {
NotifyResult(url, *app_id, ManifestUpdateResult::kThrottled);
return;
}
auto load_observer = std::make_unique<PreUpdateWebContentsObserver>(
base::BindOnce(
&ManifestUpdateManager::StartCheckAfterPageAndManifestUrlLoad,
weak_factory_.GetWeakPtr(), *app_id, check_time,
web_contents->GetWeakPtr()),
web_contents, hang_update_checks_for_testing_);
update_stages_.emplace(std::piecewise_construct,
std::forward_as_tuple(*app_id),
std::forward_as_tuple(url, std::move(load_observer)));
}
ManifestUpdateManager::UpdateStage::UpdateStage(
const GURL& url,
std::unique_ptr<PreUpdateWebContentsObserver> observer)
: url(url), observer(std::move(observer)) {}
ManifestUpdateManager::UpdateStage::~UpdateStage() = default;
void ManifestUpdateManager::StartCheckAfterPageAndManifestUrlLoad(
const webapps::AppId& app_id,
base::Time check_time,
base::WeakPtr<content::WebContents> web_contents) {
auto update_stage_it = update_stages_.find(app_id);
CHECK(update_stage_it != update_stages_.end());
UpdateStage& update_stage = update_stage_it->second;
GURL url(update_stage.url);
CHECK(update_stage.observer);
CHECK_EQ(update_stage.stage,
UpdateStage::Stage::kWaitingForPageLoadAndManifestUrl);
// If web_contents have been destroyed before page load,
// then no need of running the command.
if (!web_contents || web_contents->IsBeingDestroyed()) {
OnUpdateStopped(/*web_contents=*/nullptr, url, app_id,
ManifestUpdateResult::kWebContentsDestroyed);
return;
}
// The observer's task is done, the other stages is used to keep track of the
// 2 manifest update commands. See ManifestUpdateDataFetchCommand and
// ManifestUpdateFinalizeCommand for more details.
update_stage.observer.reset();
update_stage.stage = UpdateStage::Stage::kCheckingManifestDiff;
if (load_finished_callback_)
std::move(load_finished_callback_).Run();
auto app_window_close_await_callback =
base::BindOnce(&ManifestUpdateManager::OnManifestCheckAwaitAppWindowClose,
weak_factory_.GetWeakPtr(), web_contents, url, app_id);
if (base::FeatureList::IsEnabled(features::kWebAppEnableUpdateTokenParsing)) {
// TODO(crbug.com/414851433): Remove ScheduleManifestUpdateCheck() and
// rename ScheduleManifestUpdateCheckV2 to ScheduleManifestUpdateCheck()
// instead.
provider_->scheduler().ScheduleManifestUpdateCheckV2(
url, app_id, check_time, web_contents,
std::move(app_window_close_await_callback));
} else {
provider_->scheduler().ScheduleManifestUpdateCheck(
url, app_id, check_time, web_contents,
std::move(app_window_close_await_callback));
}
}
void ManifestUpdateManager::OnManifestCheckAwaitAppWindowClose(
base::WeakPtr<content::WebContents> contents,
const GURL& url,
const webapps::AppId& app_id,
ManifestUpdateCheckResult check_result,
std::unique_ptr<WebAppInstallInfo> install_info) {
auto update_stage_it = update_stages_.find(app_id);
if (update_stage_it == update_stages_.end()) {
// If the web_app already has already been uninstalled after the
// manifest update data fetch has happened, then we can early exit.
return;
}
if (check_result ==
ManifestUpdateCheckResult::kCancelledDueToMainFrameNavigation) {
update_stages_.erase(app_id);
NotifyResult(url, app_id,
ManifestUpdateResult::kCancelledDueToMainFrameNavigation);
return;
}
if (!contents || contents->IsBeingDestroyed() ||
!contents->GetBrowserContext()) {
update_stages_.erase(app_id);
NotifyResult(url, app_id, ManifestUpdateResult::kWebContentsDestroyed);
return;
}
UpdateStage& update_stage = update_stage_it->second;
CHECK_EQ(update_stage.stage, UpdateStage::Stage::kCheckingManifestDiff);
if (check_result != ManifestUpdateCheckResult::kAppUpdateNeeded) {
OnUpdateStopped(contents, url, app_id,
FinalResultFromManifestUpdateCheckResult(check_result));
return;
}
CHECK(install_info);
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
auto keep_alive = std::make_unique<ScopedKeepAlive>(
KeepAliveOrigin::APP_MANIFEST_UPDATE, KeepAliveRestartOption::DISABLED);
std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive;
if (!profile->IsOffTheRecord()) {
profile_keep_alive = std::make_unique<ScopedProfileKeepAlive>(
profile, ProfileKeepAliveOrigin::kWebAppUpdate);
}
provider_->scheduler().ScheduleManifestUpdateFinalize(
url, app_id, std::move(install_info), std::move(keep_alive),
std::move(profile_keep_alive),
base::BindOnce(&ManifestUpdateManager::OnUpdateStopped,
weak_factory_.GetWeakPtr(), contents));
}
bool ManifestUpdateManager::IsUpdateConsumed(const webapps::AppId& app_id,
base::Time check_time) {
std::optional<base::Time> last_check_time = GetLastUpdateCheckTime(app_id);
if (last_check_time.has_value() &&
check_time < *last_check_time + kDelayBetweenChecks &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
kDisableManifestUpdateThrottle)) {
return true;
}
return false;
}
bool ManifestUpdateManager::IsUpdateCommandPending(
const webapps::AppId& app_id) {
return base::Contains(update_stages_, app_id);
}
// WebAppInstallManager:
void ManifestUpdateManager::OnWebAppWillBeUninstalled(
const webapps::AppId& app_id) {
CHECK(started_);
auto it = update_stages_.find(app_id);
if (it != update_stages_.end()) {
NotifyResult(it->second.url, app_id,
ManifestUpdateResult::kAppUninstalling);
update_stages_.erase(it);
}
CHECK(!base::Contains(update_stages_, app_id));
last_update_check_.erase(app_id);
}
void ManifestUpdateManager::OnWebAppInstallManagerDestroyed() {
install_manager_observation_.Reset();
}
// Throttling updates to at most once per day is consistent with Android.
// See |UPDATE_INTERVAL| in WebappDataStorage.java.
bool ManifestUpdateManager::MaybeConsumeUpdateCheck(
const GURL& origin,
const webapps::AppId& app_id,
base::Time check_time) {
if (IsUpdateConsumed(app_id, check_time)) {
return false;
}
SetLastUpdateCheckTime(origin, app_id, check_time);
return true;
}
std::optional<base::Time> ManifestUpdateManager::GetLastUpdateCheckTime(
const webapps::AppId& app_id) const {
auto it = last_update_check_.find(app_id);
return it != last_update_check_.end() ? std::optional<base::Time>(it->second)
: std::nullopt;
}
void ManifestUpdateManager::SetLastUpdateCheckTime(const GURL& origin,
const webapps::AppId& app_id,
base::Time time) {
last_update_check_[app_id] = time;
}
void ManifestUpdateManager::OnUpdateStopped(
base::WeakPtr<content::WebContents> contents,
const GURL& url,
const webapps::AppId& app_id,
ManifestUpdateResult result) {
auto update_stage_it = update_stages_.find(app_id);
// If the app has been uninstalled in the middle of the manifest
// update, a kAppUninstalled has already been fired.
if (update_stage_it == update_stages_.end())
return;
update_stages_.erase(app_id);
// If a manifest update happened successfully, record feature usage of
// applying a manifest, and update the corresponding WebDXFeature counter for
// kManifest as well.
if (contents && result == ManifestUpdateResult::kAppUpdated) {
page_load_metrics::MetricsWebContentsObserver::RecordFeatureUsage(
contents->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kWebAppManifestUpdate);
}
NotifyResult(url, app_id, result);
}
void ManifestUpdateManager::NotifyResult(
const GURL& url,
const std::optional<webapps::AppId>& app_id,
ManifestUpdateResult result) {
// Don't log kNoAppInScope because it will be far too noisy (most page loads
// will hit it).
if (result != ManifestUpdateResult::kNoAppInScope) {
base::UmaHistogramEnumeration("Webapp.Update.ManifestUpdateResult", result);
}
if (*GetResultCallbackMutableForTesting()) {
std::move(*GetResultCallbackMutableForTesting()).Run(url, result);
}
}
void ManifestUpdateManager::ResetManifestThrottleForTesting(
const webapps::AppId& app_id) {
// Erase the throttle info from the map so that corresponding
// manifest writes can go through.
auto it = last_update_check_.find(app_id);
if (it != last_update_check_.end()) {
last_update_check_.erase(app_id);
}
}
bool ManifestUpdateManager::HasUpdatesPendingLoadFinishForTesting() {
for (const auto& update_data : update_stages_) {
if (update_data.second.stage ==
UpdateStage::Stage::kWaitingForPageLoadAndManifestUrl) {
return true;
}
}
return false;
}
void ManifestUpdateManager::SetLoadFinishedCallbackForTesting(
base::OnceClosure load_finished_callback) {
load_finished_callback_ = std::move(load_finished_callback);
}
bool ManifestUpdateManager::IsAppPendingPageAndManifestUrlLoadForTesting(
const webapps::AppId& app_id) {
CHECK_IS_TEST();
auto update_stage_it = update_stages_.find(app_id);
if (update_stage_it == update_stages_.end()) {
return false;
}
return (update_stage_it->second.stage ==
UpdateStage::Stage::kWaitingForPageLoadAndManifestUrl);
}
} // namespace web_app