blob: 8950f833746f03b026bf80911b18a14f286ee9d0 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/components/file_handler_manager.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/web_app_file_handler_registration.h"
#include "chrome/browser/web_applications/components/web_app_prefs_utils.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/web_launch/file_handling_expiry.mojom.h"
namespace web_app {
namespace {
// Use a large double that can be safely saved in prefs, and be safely
// represented in JS timestamp (milliseconds from epoch). base::Time::Max() does
// not work here, it returns +Infinity (which is invalid and can not be
// represented in JSON).
//
// This value is `floor((2^53 - 1) / 1000)` because base::Time::FromDoubleT()
// accepts time offset in seconds. In reality, it means 287396-10-12 08:58:59
// UTC, which is a long distant future (long after File Handling goes out of
// origin trial or be deprecated).
//
// Do not change this value, because it is persisted to disk.
const double kMaxOriginTrialExpiryTime = 9007199254740;
} // namespace
bool FileHandlerManager::disable_automatic_file_handler_cleanup_for_testing_ =
false;
FileHandlerManager::FileHandlerManager(Profile* profile) : profile_(profile) {}
FileHandlerManager::~FileHandlerManager() = default;
void FileHandlerManager::SetSubsystems(WebAppRegistrar* registrar) {
registrar_ = registrar;
}
void FileHandlerManager::Start() {
DCHECK(registrar_);
if (!FileHandlerManager::
disable_automatic_file_handler_cleanup_for_testing_) {
content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
->PostTask(
FROM_HERE,
base::BindOnce(base::IgnoreResult(
&FileHandlerManager::CleanupAfterOriginTrials),
weak_ptr_factory_.GetWeakPtr()));
}
}
void FileHandlerManager::DisableOsIntegrationForTesting() {
disable_os_integration_for_testing_ = true;
}
int FileHandlerManager::TriggerFileHandlerCleanupForTesting() {
return CleanupAfterOriginTrials();
}
void FileHandlerManager::SetOnFileHandlingExpiryUpdatedForTesting(
base::RepeatingCallback<void()> on_file_handling_expiry_updated) {
on_file_handling_expiry_updated_for_testing_ =
on_file_handling_expiry_updated;
}
void FileHandlerManager::EnableAndRegisterOsFileHandlers(const AppId& app_id) {
if (!IsFileHandlingAPIAvailable(app_id))
return;
UpdateBoolWebAppPref(profile()->GetPrefs(), app_id, kFileHandlersEnabled,
/*value=*/true);
if (!ShouldRegisterFileHandlersWithOs() ||
disable_os_integration_for_testing_) {
return;
}
// File handler registration is done via shortcuts creation on MacOS,
// WebAppShortcutManager::BuildShortcutInfoForWebApp collects file handler
// information to shortcut_info->file_handler_extensions, then used by MacOS
// implementation of |internals::CreatePlatformShortcuts|. So we avoid creating
// shortcuts twice here.
#if !defined(OS_MAC)
std::string app_name = registrar_->GetAppShortName(app_id);
const apps::FileHandlers* file_handlers = GetAllFileHandlers(app_id);
if (file_handlers)
RegisterFileHandlersWithOs(app_id, app_name, profile(), *file_handlers);
#endif
}
void FileHandlerManager::DisableAndUnregisterOsFileHandlers(
const AppId& app_id,
std::unique_ptr<ShortcutInfo> info,
base::OnceCallback<void(bool)> callback) {
// Updating prefs must be done on the UI Thread.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
UpdateBoolWebAppPref(profile()->GetPrefs(), app_id, kFileHandlersEnabled,
/*value=*/false);
// Temporarily allow file handlers unregistration only if an app has them.
// TODO(crbug.com/1088434, crbug.com/1076688): Do not start async
// CreateShortcuts process in OnWebAppWillBeUninstalled / Unregistration.
const apps::FileHandlers* file_handlers = GetAllFileHandlers(app_id);
if (!ShouldRegisterFileHandlersWithOs() || !file_handlers ||
file_handlers->empty() || disable_os_integration_for_testing_) {
// This bool signals if there was not an error. Exiting early here is WAI,
// so this is a success.
std::move(callback).Run(true);
return;
}
// File handler information is embedded in the shortcut, when
// |DeleteSharedAppShims| is called in
// |OsIntegrationManager::UninstallOsHooks|, file handlers are also
// unregistered.
#if defined(OS_MAC)
// When updating file handlers, |callback| here triggers the registering of
// the new file handlers. It is therefore important that |callback| not be
// dropped on the floor.
// https://crbug.com/1201993
std::move(callback).Run(true);
#else
UnregisterFileHandlersWithOs(app_id, profile(), std::move(info),
std::move(callback));
#endif
}
void FileHandlerManager::MaybeUpdateFileHandlingOriginTrialExpiry(
content::WebContents* web_contents,
const AppId& app_id) {
// If an App has force enabled file handling, there is no need to check its
// WebContents.
if (IsFileHandlingForceEnabled(app_id)) {
if (on_file_handling_expiry_updated_for_testing_)
on_file_handling_expiry_updated_for_testing_.Run();
return;
}
mojo::AssociatedRemote<blink::mojom::FileHandlingExpiry> expiry_service;
web_contents->GetMainFrame()->GetRemoteAssociatedInterfaces()->GetInterface(
&expiry_service);
DCHECK(expiry_service);
auto* raw = expiry_service.get();
// Here we need to pass the |expiry_service| Mojom remote interface, so it is
// not destroyed before we get a reply.
raw->RequestOriginTrialExpiryTime(base::BindOnce(
&FileHandlerManager::OnOriginTrialExpiryTimeReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(expiry_service), app_id));
}
void FileHandlerManager::ForceEnableFileHandlingOriginTrial(
const AppId& app_id) {
UpdateFileHandlersForOriginTrialExpiryTime(
app_id, base::Time::FromDoubleT(kMaxOriginTrialExpiryTime));
}
void FileHandlerManager::DisableForceEnabledFileHandlingOriginTrial(
const AppId& app_id) {
double pref_expiry_time =
GetDoubleWebAppPref(profile()->GetPrefs(), app_id,
kFileHandlingOriginTrialExpiryTime)
.value_or(0);
if (pref_expiry_time == kMaxOriginTrialExpiryTime) {
UpdateFileHandlersForOriginTrialExpiryTime(app_id, base::Time());
}
}
const apps::FileHandlers* FileHandlerManager::GetEnabledFileHandlers(
const AppId& app_id) {
if (AreFileHandlersEnabled(app_id) && IsFileHandlingAPIAvailable(app_id) &&
!registrar_->IsAppFileHandlerPermissionBlocked(app_id))
return GetAllFileHandlers(app_id);
return nullptr;
}
bool FileHandlerManager::IsFileHandlingAPIAvailable(const AppId& app_id) {
double pref_expiry_time =
GetDoubleWebAppPref(profile()->GetPrefs(), app_id,
kFileHandlingOriginTrialExpiryTime)
.value_or(0);
return base::FeatureList::IsEnabled(blink::features::kFileHandlingAPI) ||
base::Time::FromDoubleT(pref_expiry_time) >= base::Time::Now();
}
bool FileHandlerManager::AreFileHandlersEnabled(const AppId& app_id) const {
return GetBoolWebAppPref(profile()->GetPrefs(), app_id, kFileHandlersEnabled);
}
void FileHandlerManager::OnOriginTrialExpiryTimeReceived(
mojo::AssociatedRemote<blink::mojom::FileHandlingExpiry> /*interface*/,
const AppId& app_id,
base::Time expiry_time) {
// Updates the expiry time, if file handling is enabled by origin trial
// tokens. If an App has force enabled file handling, it might not have expiry
// time associated with it.
if (!IsFileHandlingForceEnabled(app_id)) {
UpdateFileHandlersForOriginTrialExpiryTime(app_id, expiry_time);
}
if (on_file_handling_expiry_updated_for_testing_)
on_file_handling_expiry_updated_for_testing_.Run();
}
void FileHandlerManager::UpdateFileHandlersForOriginTrialExpiryTime(
const AppId& app_id,
const base::Time& expiry_time) {
web_app::UpdateDoubleWebAppPref(profile_->GetPrefs(), app_id,
kFileHandlingOriginTrialExpiryTime,
expiry_time.ToDoubleT());
// Only enable/disable file handlers if the state is changing, as
// enabling/disabling is a potentially expensive operation (it may involve
// creating an app shim, and will almost certainly involve IO).
const bool file_handlers_enabled = AreFileHandlersEnabled(app_id);
// If the trial is valid, ensure the file handlers are enabled.
// Otherwise disable them.
if (IsFileHandlingAPIAvailable(app_id)) {
if (!file_handlers_enabled)
EnableAndRegisterOsFileHandlers(app_id);
} else if (file_handlers_enabled) {
// TODO(crbug.com/1076688): Retrieve shortcuts before they're unregistered.
DisableAndUnregisterOsFileHandlers(app_id, nullptr, base::DoNothing());
}
}
void FileHandlerManager::DisableAutomaticFileHandlerCleanupForTesting() {
disable_automatic_file_handler_cleanup_for_testing_ = true;
}
int FileHandlerManager::CleanupAfterOriginTrials() {
int cleaned_up_count = 0;
for (const AppId& app_id : registrar_->GetAppIds()) {
if (!AreFileHandlersEnabled(app_id))
continue;
if (IsFileHandlingAPIAvailable(app_id))
continue;
// If the trial has expired, unregister handlers.
// TODO(crbug.com/1076688): Retrieve shortcuts before they're unregistered.
DisableAndUnregisterOsFileHandlers(app_id, nullptr, base::DoNothing());
cleaned_up_count++;
}
return cleaned_up_count;
}
const absl::optional<GURL> FileHandlerManager::GetMatchingFileHandlerURL(
const AppId& app_id,
const std::vector<base::FilePath>& launch_files) {
if (!IsFileHandlingAPIAvailable(app_id) || launch_files.empty())
return absl::nullopt;
const apps::FileHandlers* file_handlers = GetAllFileHandlers(app_id);
if (!file_handlers)
return absl::nullopt;
std::set<std::string> launch_file_extensions;
for (const auto& file_path : launch_files) {
std::string file_extension =
base::FilePath(file_path.Extension()).AsUTF8Unsafe();
if (file_extension.length() <= 1)
return absl::nullopt;
launch_file_extensions.insert(file_extension);
}
for (const auto& file_handler : *file_handlers) {
bool all_launch_file_extensions_supported = true;
std::set<std::string> supported_file_extensions =
apps::GetFileExtensionsFromFileHandlers({file_handler});
for (const auto& file_extension : launch_file_extensions) {
if (!base::Contains(supported_file_extensions, file_extension)) {
all_launch_file_extensions_supported = false;
break;
}
}
if (all_launch_file_extensions_supported)
return file_handler.action;
}
return absl::nullopt;
}
bool FileHandlerManager::IsFileHandlingForceEnabled(const AppId& app_id) {
double pref_expiry_time =
GetDoubleWebAppPref(profile()->GetPrefs(), app_id,
kFileHandlingOriginTrialExpiryTime)
.value_or(0);
return pref_expiry_time == kMaxOriginTrialExpiryTime;
}
} // namespace web_app