blob: ebb9e6dd3eb92b1cecb4d49eb7c420aed8eda263 [file] [log] [blame]
// Copyright 2014 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/extensions/api/runtime/chrome_runtime_api_delegate.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/updater/extension_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "components/update_client/update_query_params.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/delayed_install_manager.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/browser/warning_service.h"
#include "extensions/browser/warning_set.h"
#include "extensions/common/api/runtime.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/manifest.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "net/base/backoff_entry.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#endif
#if BUILDFLAG(IS_WIN)
#include "base/win/windows_version.h"
#endif
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#else
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#endif
using extensions::Extension;
using extensions::ExtensionSystem;
using extensions::ExtensionUpdater;
using extensions::api::runtime::PlatformInfo;
namespace {
// If an extension reloads itself within this many milliseconds of reloading
// itself, the reload is considered suspiciously fast.
const int kFastReloadTime = 10000;
// Same as above, but we shorten the fast reload interval for unpacked
// extensions for ease of testing.
const int kUnpackedFastReloadTime = 1000;
// A holder class for the policy we use for exponential backoff of update check
// requests.
class BackoffPolicy {
public:
BackoffPolicy();
~BackoffPolicy();
// Returns the actual policy to use.
static const net::BackoffEntry::Policy* Get();
private:
net::BackoffEntry::Policy policy_;
};
// We use a LazyInstance since one of the the policy values references an
// extern symbol, which would cause a static initializer to be generated if we
// just declared the policy struct as a static variable.
base::LazyInstance<BackoffPolicy>::DestructorAtExit g_backoff_policy =
LAZY_INSTANCE_INITIALIZER;
BackoffPolicy::BackoffPolicy() {
policy_ = {
// num_errors_to_ignore
0,
// initial_delay_ms (note that we set 'always_use_initial_delay' to false
// below)
extensions::kDefaultUpdateFrequency.InMilliseconds(),
// multiply_factor
1,
// jitter_factor
0.1,
// maximum_backoff_ms (-1 means no maximum)
-1,
// entry_lifetime_ms (-1 means never discard)
-1,
// always_use_initial_delay
false,
};
}
BackoffPolicy::~BackoffPolicy() = default;
// static
const net::BackoffEntry::Policy* BackoffPolicy::Get() {
return &g_backoff_policy.Get().policy_;
}
const base::TickClock* g_test_clock = nullptr;
} // namespace
struct ChromeRuntimeAPIDelegate::UpdateCheckInfo {
std::unique_ptr<net::BackoffEntry> backoff =
std::make_unique<net::BackoffEntry>(BackoffPolicy::Get(), g_test_clock);
std::vector<UpdateCheckCallback> callbacks;
};
ChromeRuntimeAPIDelegate::ChromeRuntimeAPIDelegate(
content::BrowserContext* context)
: browser_context_(context), registered_for_updates_(false) {
extension_registry_observation_.Observe(
extensions::ExtensionRegistry::Get(browser_context_));
}
ChromeRuntimeAPIDelegate::~ChromeRuntimeAPIDelegate() = default;
// static
void ChromeRuntimeAPIDelegate::set_tick_clock_for_tests(
const base::TickClock* clock) {
g_test_clock = clock;
}
void ChromeRuntimeAPIDelegate::AddUpdateObserver(
extensions::UpdateObserver* observer) {
registered_for_updates_ = true;
ExtensionUpdater::Get(browser_context_)->AddObserver(observer);
}
void ChromeRuntimeAPIDelegate::RemoveUpdateObserver(
extensions::UpdateObserver* observer) {
if (registered_for_updates_) {
ExtensionUpdater::Get(browser_context_)->RemoveObserver(observer);
}
}
void ChromeRuntimeAPIDelegate::ReloadExtension(
const extensions::ExtensionId& extension_id) {
const Extension* extension =
extensions::ExtensionRegistry::Get(browser_context_)
->GetInstalledExtension(extension_id);
int fast_reload_time = kFastReloadTime;
int fast_reload_count = extensions::RuntimeAPI::kFastReloadCount;
// If an extension is unpacked, we allow for a faster reload interval
// and more fast reload attempts before terminating the extension.
// This is intended to facilitate extension testing for developers.
if (extensions::Manifest::IsUnpackedLocation(extension->location())) {
fast_reload_time = kUnpackedFastReloadTime;
fast_reload_count = extensions::RuntimeAPI::kUnpackedFastReloadCount;
}
std::pair<base::TimeTicks, int>& reload_info =
last_reload_time_[extension_id];
base::TimeTicks now =
g_test_clock ? g_test_clock->NowTicks() : base::TimeTicks::Now();
if (reload_info.first.is_null() ||
(now - reload_info.first).InMilliseconds() > fast_reload_time) {
reload_info.second = 0;
} else {
reload_info.second++;
}
if (!reload_info.first.is_null()) {
UMA_HISTOGRAM_LONG_TIMES("Extensions.RuntimeReloadTime",
now - reload_info.first);
}
UMA_HISTOGRAM_COUNTS_100("Extensions.RuntimeReloadFastCount",
reload_info.second);
reload_info.first = now;
extensions::ExtensionRegistrar* registrar =
extensions::ExtensionRegistrar::Get(browser_context_);
if (reload_info.second >= fast_reload_count) {
// Unloading an extension clears all warnings, so first terminate the
// extension, and then add the warning. Since this is called from an
// extension function unloading the extension has to be done
// asynchronously. Fortunately PostTask guarentees FIFO order so just
// post both tasks.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&extensions::ExtensionRegistrar::TerminateExtension,
registrar->GetWeakPtr(), extension_id));
extensions::WarningSet warnings;
warnings.insert(
extensions::Warning::CreateReloadTooFrequentWarning(extension_id));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&extensions::WarningService::NotifyWarningsOnUI,
// TODO(crbug.com/40061562): Remove
// `UnsafeDanglingUntriaged`
base::UnsafeDanglingUntriaged(browser_context_),
warnings));
} else {
// We can't call ReloadExtension directly, since when this method finishes
// it tries to decrease the reference count for the extension, which fails
// if the extension has already been reloaded; so instead we post a task.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&extensions::ExtensionRegistrar::ReloadExtension,
registrar->GetWeakPtr(), extension_id));
}
}
bool ChromeRuntimeAPIDelegate::CheckForUpdates(
const extensions::ExtensionId& extension_id,
UpdateCheckCallback callback) {
Profile* profile = Profile::FromBrowserContext(browser_context_);
ExtensionUpdater* updater = ExtensionUpdater::Get(profile);
if (!updater->enabled()) {
return false;
}
UpdateCheckInfo& info = update_check_info_[extension_id];
// If not enough time has elapsed, or we have 10 or more outstanding calls,
// return a status of throttled.
if (info.backoff->ShouldRejectRequest() || info.callbacks.size() >= 10) {
UpdateCheckResult result = UpdateCheckResult(
extensions::api::runtime::RequestUpdateCheckStatus::kThrottled, "");
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
} else {
info.callbacks.push_back(std::move(callback));
extensions::ExtensionUpdater::CheckParams params;
params.ids = {extension_id};
params.update_found_callback =
base::BindRepeating(&ChromeRuntimeAPIDelegate::OnExtensionUpdateFound,
base::Unretained(this));
params.callback =
base::BindOnce(&ChromeRuntimeAPIDelegate::UpdateCheckComplete,
base::Unretained(this), extension_id);
updater->CheckNow(std::move(params));
}
return true;
}
void ChromeRuntimeAPIDelegate::OpenURL(const GURL& uninstall_url) {
Profile* profile = Profile::FromBrowserContext(browser_context_);
#if BUILDFLAG(ENABLE_EXTENSIONS)
Browser* browser = chrome::FindLastActiveWithProfile(profile);
if (!browser) {
browser = Browser::Create(Browser::CreateParams(profile, false));
}
if (!browser) {
return;
}
NavigateParams params(browser, uninstall_url,
ui::PAGE_TRANSITION_CLIENT_REDIRECT);
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
params.user_gesture = false;
Navigate(&params);
#else
TabModel* tab_model = nullptr;
for (TabModel* model : TabModelList::models()) {
if (model->GetProfile() == profile) {
tab_model = model;
break;
}
}
if (!tab_model) {
return;
}
std::unique_ptr<content::WebContents> contents = content::WebContents::Create(
content::WebContents::CreateParams(browser_context_));
content::WebContents* new_web_contents = contents.release();
tab_model->CreateTab(nullptr, new_web_contents, /*select=*/true);
content::NavigationController::LoadURLParams load_params(uninstall_url);
load_params.transition_type = ui::PAGE_TRANSITION_FROM_API;
base::WeakPtr<content::NavigationHandle> navigation_handle =
new_web_contents->GetController().LoadURLWithParams(load_params);
// Navigation can fail for any number of reasons at the content layer.
// Unfortunately, we can't provide a detailed error message here, because
// there are too many possible triggers. At least add a log for diagnostics.
if (!navigation_handle) {
LOG(ERROR) << "navigation rejected for uninstall_url"
<< uninstall_url.spec();
}
#endif
}
// Helper function for GetPlatformInfo(). nacl_arch is deprecated, so
// please do not add any new values here.
extensions::api::runtime::PlatformNaclArch GetPlatformInfoNaClArch() {
#if defined(ARCH_CPU_X86_FAMILY)
#if defined(ARCH_CPU_X86_64)
return extensions::api::runtime::PlatformNaclArch::kX86_64;
#elif BUILDFLAG(IS_WIN)
return base::win::OSInfo::GetInstance()->IsWowX86OnAMD64()
? extensions::api::runtime::PlatformNaclArch::kX86_64
: extensions::api::runtime::PlatformNaclArch::kX86_32;
#else
return extensions::api::runtime::PlatformNaclArch::kX86_32;
#endif
#elif defined(ARCH_CPU_ARM_FAMILY)
return extensions::api::runtime::PlatformNaclArch::kArm;
#elif defined(ARCH_CPU_MIPSEL)
return extensions::api::runtime::PlatformNaclArch::kMips;
#elif defined(ARCH_CPU_MIPS64EL)
return extensions::api::runtime::PlatformNaclArch::kMips64;
#else
// NOTE: Other architectures did not support extensions at the time
// of NaCl removal.
return extensions::api::runtime::PlatformNaclArch::kNone;
#endif
}
bool ChromeRuntimeAPIDelegate::GetPlatformInfo(PlatformInfo* info) {
const char* os = update_client::UpdateQueryParams::GetOS();
if (UNSAFE_TODO(strcmp(os, "mac")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kMac;
} else if (UNSAFE_TODO(strcmp(os, "win")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kWin;
} else if (UNSAFE_TODO(strcmp(os, "cros")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kCros;
} else if (UNSAFE_TODO(strcmp(os, "linux")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kLinux;
} else if (UNSAFE_TODO(strcmp(os, "openbsd")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kOpenbsd;
} else if (UNSAFE_TODO(strcmp(os, "android")) == 0) {
info->os = extensions::api::runtime::PlatformOs::kAndroid;
} else {
NOTREACHED() << "Platform not supported: " << os;
}
const char* arch = update_client::UpdateQueryParams::GetArch();
if (UNSAFE_TODO(strcmp(arch, "arm")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kArm;
} else if (UNSAFE_TODO(strcmp(arch, "arm64")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kArm64;
} else if (UNSAFE_TODO(strcmp(arch, "x86")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kX86_32;
} else if (UNSAFE_TODO(strcmp(arch, "x64")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kX86_64;
} else if (UNSAFE_TODO(strcmp(arch, "mipsel")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kMips;
} else if (UNSAFE_TODO(strcmp(arch, "mips64el")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kMips64;
} else if (UNSAFE_TODO(strcmp(arch, "riscv64")) == 0) {
info->arch = extensions::api::runtime::PlatformArch::kRiscv64;
} else {
NOTREACHED();
}
info->nacl_arch = GetPlatformInfoNaClArch();
return true;
}
bool ChromeRuntimeAPIDelegate::RestartDevice(std::string* error_message) {
#if BUILDFLAG(IS_CHROMEOS)
if (chromeos::IsKioskSession()) {
chromeos::PowerManagerClient::Get()->RequestRestart(
power_manager::REQUEST_RESTART_API, "chrome.runtime API");
return true;
}
#endif
*error_message = "Function available only for ChromeOS kiosk mode.";
return false;
}
bool ChromeRuntimeAPIDelegate::OpenOptionsPage(
const Extension* extension,
content::BrowserContext* browser_context) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
return extensions::ExtensionTabUtil::OpenOptionsPageFromAPI(extension,
browser_context);
#else
// TODO(crbug.com/383366125): Implement this when options page for extensions
// becomes available for desktop android.
NOTIMPLEMENTED_LOG_ONCE();
return false;
#endif
}
int ChromeRuntimeAPIDelegate::GetDeveloperToolsWindowId(
content::WebContents* developer_tools_web_contents) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
// For developer tools contexts, first check the docked state. If the
// developer tools are docked, return the window ID of the inspected web
// contents. Otherwise, return the window ID of the developer tools window.
CHECK_EQ(extensions::GetViewType(developer_tools_web_contents),
extensions::mojom::ViewType::kDeveloperTools);
CHECK(DevToolsWindow::IsDevToolsWindow(developer_tools_web_contents));
DevToolsWindow* devtools_window =
DevToolsWindow::AsDevToolsWindow(developer_tools_web_contents);
content::WebContents* inspected_web_contents =
devtools_window->GetInspectedWebContents();
bool is_docked = inspected_web_contents->GetTopLevelNativeWindow() ==
developer_tools_web_contents->GetTopLevelNativeWindow();
content::WebContents* web_contents_to_use =
is_docked ? inspected_web_contents : developer_tools_web_contents;
return extensions::ExtensionTabUtil::GetWindowIdOfTab(web_contents_to_use);
#else
// TODO(crbug.com/383366125): Implement this function for desktop android.
NOTIMPLEMENTED();
return -1;
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
}
void ChromeRuntimeAPIDelegate::OnExtensionUpdateFound(
const extensions::ExtensionId& extension_id,
const base::Version& version) {
if (version.IsValid()) {
UpdateCheckResult result = UpdateCheckResult(
extensions::api::runtime::RequestUpdateCheckStatus::kUpdateAvailable,
version.GetString());
CallUpdateCallbacks(extension_id, std::move(result));
}
}
void ChromeRuntimeAPIDelegate::OnExtensionInstalled(
content::BrowserContext* browser_context,
const Extension* extension,
bool is_update) {
if (!is_update) {
return;
}
auto info = update_check_info_.find(extension->id());
if (info != update_check_info_.end()) {
info->second.backoff->Reset();
}
}
void ChromeRuntimeAPIDelegate::UpdateCheckComplete(
const extensions::ExtensionId& extension_id) {
const Extension* update =
extensions::DelayedInstallManager::Get(browser_context_)
->GetPendingExtensionUpdate(extension_id);
UpdateCheckInfo& info = update_check_info_[extension_id];
// We always inform the BackoffEntry of a "failure" here, because we only
// want to consider an update check request a success from a throttling
// standpoint once the extension goes on to actually update to a new
// version. See OnExtensionInstalled for where we reset the BackoffEntry.
info.backoff->InformOfRequest(false);
if (update) {
UpdateCheckResult result = UpdateCheckResult(
extensions::api::runtime::RequestUpdateCheckStatus::kUpdateAvailable,
update->VersionString());
CallUpdateCallbacks(extension_id, std::move(result));
} else {
UpdateCheckResult result = UpdateCheckResult(
extensions::api::runtime::RequestUpdateCheckStatus::kNoUpdate, "");
CallUpdateCallbacks(extension_id, std::move(result));
}
}
void ChromeRuntimeAPIDelegate::CallUpdateCallbacks(
const extensions::ExtensionId& extension_id,
const UpdateCheckResult& result) {
auto it = update_check_info_.find(extension_id);
if (it == update_check_info_.end()) {
return;
}
std::vector<UpdateCheckCallback> callbacks;
it->second.callbacks.swap(callbacks);
for (auto& callback : callbacks) {
std::move(callback).Run(result);
}
}