blob: d4f962231da2e0b4e771cd282bc5874ef6cdcebb [file] [log] [blame]
// Copyright (c) 2012 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/ui/webui/extensions/extensions_ui.h"
#include <memory>
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/chrome_extension_browser_constants.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/dark_mode_handler.h"
#include "chrome/browser/ui/webui/metrics_handler.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/extensions_resources.h"
#include "chrome/grit/extensions_resources_map.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/google/core/common/google_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/content_features.h"
#include "extensions/common/extension_urls.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/ownership/owner_settings_service_chromeos_factory.h"
#include "chrome/browser/ui/webui/extensions/chromeos/kiosk_apps_handler.h"
#endif
namespace extensions {
namespace {
constexpr char kInDevModeKey[] = "inDevMode";
constexpr char kShowActivityLogKey[] = "showActivityLog";
constexpr char kLoadTimeClassesKey[] = "loadTimeClasses";
struct LocalizedString {
const char* name;
int id;
};
void AddLocalizedStringsBulk(content::WebUIDataSource* html_source,
const LocalizedString localized_strings[],
size_t num_strings) {
for (size_t i = 0; i < num_strings; i++) {
html_source->AddLocalizedString(localized_strings[i].name,
localized_strings[i].id);
}
}
class ExtensionWebUiTimer : public content::WebContentsObserver {
public:
explicit ExtensionWebUiTimer(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
~ExtensionWebUiTimer() override {}
void DidStartNavigation(
content::NavigationHandle* navigation_handle) override {
if (navigation_handle->IsInMainFrame() &&
!navigation_handle->IsSameDocument()) {
timer_.reset(new base::ElapsedTimer());
}
}
void DocumentLoadedInFrame(
content::RenderFrameHost* render_frame_host) override {
if (render_frame_host != web_contents()->GetMainFrame() ||
!timer_) { // See comment in DocumentOnLoadCompletedInMainFrame()
return;
}
UMA_HISTOGRAM_TIMES("Extensions.WebUi.DocumentLoadedInMainFrameTime.MD",
timer_->Elapsed());
}
void DocumentOnLoadCompletedInMainFrame() override {
// TODO(devlin): The usefulness of these metrics remains to be seen.
if (!timer_) {
// This object could have been created for a child RenderFrameHost so it
// would never get a DidStartNavigation with the main frame. However it
// will receive this current callback.
return;
}
UMA_HISTOGRAM_TIMES("Extensions.WebUi.LoadCompletedInMainFrame.MD",
timer_->Elapsed());
timer_.reset();
}
void WebContentsDestroyed() override { delete this; }
private:
std::unique_ptr<base::ElapsedTimer> timer_;
DISALLOW_COPY_AND_ASSIGN(ExtensionWebUiTimer);
};
std::string GetLoadTimeClasses(bool in_dev_mode) {
return in_dev_mode ? "in-dev-mode" : std::string();
}
content::WebUIDataSource* CreateMdExtensionsSource(bool in_dev_mode) {
content::WebUIDataSource* source =
content::WebUIDataSource::Create(chrome::kChromeUIExtensionsHost);
source->SetJsonPath("strings.js");
constexpr LocalizedString localized_strings[] = {
// Add common strings.
{"add", IDS_ADD},
{"back", IDS_ACCNAME_BACK},
{"cancel", IDS_CANCEL},
{"close", IDS_CLOSE},
{"confirm", IDS_CONFIRM},
{"controlledSettingPolicy", IDS_CONTROLLED_SETTING_POLICY},
{"done", IDS_DONE},
{"learnMore", IDS_LEARN_MORE},
{"noSearchResults", IDS_SEARCH_NO_RESULTS},
{"ok", IDS_OK},
{"save", IDS_SAVE},
{"searchResultsPlural", IDS_SEARCH_RESULTS_PLURAL},
{"searchResultsSingular", IDS_SEARCH_RESULTS_SINGULAR},
// Multi-use strings defined in md_extensions_strings.grdp.
{"remove", IDS_MD_EXTENSIONS_REMOVE},
// Add extension-specific strings.
{"title", IDS_MANAGE_EXTENSIONS_SETTING_WINDOWS_TITLE},
{"toolbarTitle", IDS_MD_EXTENSIONS_TOOLBAR_TITLE},
{"mainMenu", IDS_MD_EXTENSIONS_MENU_BUTTON_LABEL},
{"search", IDS_MD_EXTENSIONS_SEARCH},
// TODO(dpapad): Use a single merged string resource for "Clear search".
{"clearSearch", IDS_DOWNLOAD_CLEAR_SEARCH},
{"sidebarExtensions", IDS_MD_EXTENSIONS_SIDEBAR_EXTENSIONS},
{"appsTitle", IDS_MD_EXTENSIONS_APPS_TITLE},
{"noExtensionsOrApps", IDS_MD_EXTENSIONS_NO_INSTALLED_ITEMS},
{"noDescription", IDS_MD_EXTENSIONS_NO_DESCRIPTION},
{"viewInStore", IDS_MD_EXTENSIONS_ITEM_CHROME_WEB_STORE},
{"extensionWebsite", IDS_MD_EXTENSIONS_ITEM_EXTENSION_WEBSITE},
{"dropToInstall", IDS_EXTENSIONS_INSTALL_DROP_TARGET},
{"errorsPageHeading", IDS_MD_EXTENSIONS_ERROR_PAGE_HEADING},
{"clearAll", IDS_MD_EXTENSIONS_ERROR_CLEAR_ALL},
{"clearEntry", IDS_MD_EXTENSIONS_A11Y_CLEAR_ENTRY},
{"logLevel", IDS_EXTENSIONS_LOG_LEVEL_INFO},
{"warnLevel", IDS_EXTENSIONS_LOG_LEVEL_WARN},
{"errorLevel", IDS_EXTENSIONS_LOG_LEVEL_ERROR},
{"anonymousFunction", IDS_MD_EXTENSIONS_ERROR_ANONYMOUS_FUNCTION},
{"errorContext", IDS_MD_EXTENSIONS_ERROR_CONTEXT},
{"errorContextUnknown", IDS_MD_EXTENSIONS_ERROR_CONTEXT_UNKNOWN},
{"stackTrace", IDS_MD_EXTENSIONS_ERROR_STACK_TRACE},
// TODO(dpapad): Unify with Settings' IDS_SETTINGS_WEB_STORE.
{"openChromeWebStore", IDS_MD_EXTENSIONS_SIDEBAR_OPEN_CHROME_WEB_STORE},
{"keyboardShortcuts", IDS_MD_EXTENSIONS_SIDEBAR_KEYBOARD_SHORTCUTS},
{"incognitoInfoWarning", IDS_EXTENSIONS_INCOGNITO_WARNING},
{"hostPermissionsEdit", IDS_MD_EXTENSIONS_HOST_PERMISSIONS_EDIT},
{"hostPermissionsHeading", IDS_MD_EXTENSIONS_ITEM_HOST_PERMISSIONS_HEADING},
{"hostAccessOnClick", IDS_MD_EXTENSIONS_HOST_ACCESS_ON_CLICK},
{"hostAccessOnSpecificSites",
IDS_MD_EXTENSIONS_HOST_ACCESS_ON_SPECIFIC_SITES},
{"hostAccessOnAllSites", IDS_MD_EXTENSIONS_HOST_ACCESS_ON_ALL_SITES},
{"hostAllowedHosts", IDS_EXTENSIONS_ITEM_ALLOWED_HOSTS},
{"itemId", IDS_MD_EXTENSIONS_ITEM_ID},
{"itemInspectViews", IDS_MD_EXTENSIONS_ITEM_INSPECT_VIEWS},
// NOTE: This text reads "<n> more". It's possible that it should be using
// a plural string instead. Unfortunately, this is non-trivial since we
// don't expose that capability to JS yet. Since we don't know it's a
// problem, use a simple placeholder for now.
{"itemInspectViewsExtra", IDS_MD_EXTENSIONS_ITEM_INSPECT_VIEWS_EXTRA},
{"noActiveViews", IDS_MD_EXTENSIONS_ITEM_NO_ACTIVE_VIEWS},
{"itemAllowIncognito", IDS_MD_EXTENSIONS_ITEM_ALLOW_INCOGNITO},
{"itemDescriptionLabel", IDS_MD_EXTENSIONS_ITEM_DESCRIPTION},
{"itemDependencies", IDS_MD_EXTENSIONS_ITEM_DEPENDENCIES},
{"itemDependentEntry", IDS_MD_EXTENSIONS_DEPENDENT_ENTRY},
{"itemDetails", IDS_MD_EXTENSIONS_ITEM_DETAILS},
{"itemErrors", IDS_MD_EXTENSIONS_ITEM_ERRORS},
{"accessibilityErrorLine", IDS_MD_EXTENSIONS_ACCESSIBILITY_ERROR_LINE},
{"accessibilityErrorMultiLine",
IDS_MD_EXTENSIONS_ACCESSIBILITY_ERROR_MULTI_LINE},
{"activityLogPageHeading", IDS_MD_EXTENSIONS_ACTIVITY_LOG_PAGE_HEADING},
{"appIcon", IDS_MD_EXTENSIONS_APP_ICON},
{"extensionIcon", IDS_MD_EXTENSIONS_EXTENSION_ICON},
{"extensionA11yAssociation", IDS_MD_EXTENSIONS_EXTENSION_A11Y_ASSOCIATION},
{"itemIdHeading", IDS_MD_EXTENSIONS_ITEM_ID_HEADING},
{"extensionEnabled", IDS_MD_EXTENSIONS_EXTENSION_ENABLED},
{"appEnabled", IDS_MD_EXTENSIONS_APP_ENABLED},
{"installWarnings", IDS_EXTENSIONS_INSTALL_WARNINGS},
{"itemExtensionPath", IDS_EXTENSIONS_PATH},
{"itemOff", IDS_MD_EXTENSIONS_ITEM_OFF},
{"itemOn", IDS_MD_EXTENSIONS_ITEM_ON},
{"itemOptions", IDS_MD_EXTENSIONS_ITEM_OPTIONS},
{"itemPermissions", IDS_MD_EXTENSIONS_ITEM_PERMISSIONS},
{"itemPermissionsEmpty", IDS_MD_EXTENSIONS_ITEM_PERMISSIONS_EMPTY},
{"itemRemoveExtension", IDS_MD_EXTENSIONS_ITEM_REMOVE_EXTENSION},
{"itemSource", IDS_MD_EXTENSIONS_ITEM_SOURCE},
{"itemSourcePolicy", IDS_MD_EXTENSIONS_ITEM_SOURCE_POLICY},
{"itemSourceSideloaded", IDS_MD_EXTENSIONS_ITEM_SOURCE_SIDELOADED},
{"itemSourceUnpacked", IDS_MD_EXTENSIONS_ITEM_SOURCE_UNPACKED},
{"itemSourceWebstore", IDS_MD_EXTENSIONS_ITEM_SOURCE_WEBSTORE},
{"itemVersion", IDS_MD_EXTENSIONS_ITEM_VERSION},
// TODO(dpapad): Replace this with an Extensions specific string.
{"itemSize", IDS_DIRECTORY_LISTING_SIZE},
{"itemAllowOnFileUrls", IDS_EXTENSIONS_ALLOW_FILE_ACCESS},
{"itemAllowOnAllSites", IDS_EXTENSIONS_ALLOW_ON_ALL_URLS},
{"itemCollectErrors", IDS_EXTENSIONS_ENABLE_ERROR_COLLECTION},
{"itemCorruptInstall", IDS_EXTENSIONS_CORRUPTED_EXTENSION},
{"itemRepair", IDS_EXTENSIONS_REPAIR_CORRUPTED},
{"itemReload", IDS_EXTENSIONS_RELOAD_TERMINATED},
{"loadErrorCouldNotLoadManifest",
IDS_MD_EXTENSIONS_LOAD_ERROR_COULD_NOT_LOAD_MANIFEST},
{"loadErrorHeading", IDS_MD_EXTENSIONS_LOAD_ERROR_HEADING},
{"loadErrorFileLabel", IDS_MD_EXTENSIONS_LOAD_ERROR_FILE_LABEL},
{"loadErrorErrorLabel", IDS_MD_EXTENSIONS_LOAD_ERROR_ERROR_LABEL},
{"loadErrorRetry", IDS_MD_EXTENSIONS_LOAD_ERROR_RETRY},
{"loadingActivities", IDS_MD_EXTENSIONS_LOADING_ACTIVITIES},
{"noActivities", IDS_MD_EXTENSIONS_NO_ACTIVITIES},
{"noErrorsToShow", IDS_EXTENSIONS_ERROR_NO_ERRORS_CODE_MESSAGE},
{"runtimeHostsDialogInputError",
IDS_MD_EXTENSIONS_RUNTIME_HOSTS_DIALOG_INPUT_ERROR},
{"runtimeHostsDialogInputLabel",
IDS_MD_EXTENSIONS_RUNTIME_HOSTS_DIALOG_INPUT_LABEL},
{"runtimeHostsDialogTitle", IDS_MD_EXTENSIONS_RUNTIME_HOSTS_DIALOG_TITLE},
{"packDialogTitle", IDS_MD_EXTENSIONS_PACK_DIALOG_TITLE},
{"packDialogWarningTitle", IDS_MD_EXTENSIONS_PACK_DIALOG_WARNING_TITLE},
{"packDialogErrorTitle", IDS_MD_EXTENSIONS_PACK_DIALOG_ERROR_TITLE},
{"packDialogProceedAnyway", IDS_MD_EXTENSIONS_PACK_DIALOG_PROCEED_ANYWAY},
{"packDialogBrowse", IDS_MD_EXTENSIONS_PACK_DIALOG_BROWSE_BUTTON},
{"packDialogExtensionRoot",
IDS_MD_EXTENSIONS_PACK_DIALOG_EXTENSION_ROOT_LABEL},
{"packDialogKeyFile", IDS_MD_EXTENSIONS_PACK_DIALOG_KEY_FILE_LABEL},
{"packDialogContent", IDS_EXTENSION_PACK_DIALOG_HEADING},
{"packDialogConfirm", IDS_MD_EXTENSIONS_PACK_DIALOG_CONFIRM_BUTTON},
{"shortcutNotSet", IDS_MD_EXTENSIONS_SHORTCUT_NOT_SET},
{"shortcutScopeGlobal", IDS_MD_EXTENSIONS_SHORTCUT_SCOPE_GLOBAL},
{"shortcutScopeLabel", IDS_MD_EXTENSIONS_SHORTCUT_SCOPE_LABEL},
{"shortcutScopeInChrome", IDS_MD_EXTENSIONS_SHORTCUT_SCOPE_IN_CHROME},
{"shortcutTypeAShortcut", IDS_MD_EXTENSIONS_TYPE_A_SHORTCUT},
{"shortcutIncludeStartModifier", IDS_MD_EXTENSIONS_INCLUDE_START_MODIFIER},
{"shortcutTooManyModifiers", IDS_MD_EXTENSIONS_TOO_MANY_MODIFIERS},
{"shortcutNeedCharacter", IDS_MD_EXTENSIONS_NEED_CHARACTER},
{"toolbarDevMode", IDS_MD_EXTENSIONS_DEVELOPER_MODE},
{"toolbarLoadUnpacked", IDS_MD_EXTENSIONS_TOOLBAR_LOAD_UNPACKED},
{"toolbarPack", IDS_MD_EXTENSIONS_TOOLBAR_PACK},
{"toolbarUpdateNow", IDS_MD_EXTENSIONS_TOOLBAR_UPDATE_NOW},
{"toolbarUpdateNowTooltip", IDS_MD_EXTENSIONS_TOOLBAR_UPDATE_NOW_TOOLTIP},
{"toolbarUpdateDone", IDS_MD_EXTENSIONS_TOOLBAR_UPDATE_DONE},
{"toolbarUpdatingToast", IDS_MD_EXTENSIONS_TOOLBAR_UPDATING_TOAST},
{"updateRequiredByPolicy",
IDS_MD_EXTENSIONS_DISABLED_UPDATE_REQUIRED_BY_POLICY},
{"viewActivityLog", IDS_EXTENSIONS_VIEW_ACTIVITY_LOG},
{"viewBackgroundPage", IDS_EXTENSIONS_BACKGROUND_PAGE},
{"viewIncognito", IDS_EXTENSIONS_VIEW_INCOGNITO},
{"viewInactive", IDS_EXTENSIONS_VIEW_INACTIVE},
{"viewIframe", IDS_EXTENSIONS_VIEW_IFRAME},
#if defined(OS_CHROMEOS)
{"manageKioskApp", IDS_MD_EXTENSIONS_MANAGE_KIOSK_APP},
{"kioskAddApp", IDS_MD_EXTENSIONS_KIOSK_ADD_APP},
{"kioskAddAppHint", IDS_MD_EXTENSIONS_KIOSK_ADD_APP_HINT},
{"kioskEnableAutoLaunch", IDS_MD_EXTENSIONS_KIOSK_ENABLE_AUTO_LAUNCH},
{"kioskDisableAutoLaunch", IDS_MD_EXTENSIONS_KIOSK_DISABLE_AUTO_LAUNCH},
{"kioskAutoLaunch", IDS_MD_EXTENSIONS_KIOSK_AUTO_LAUNCH},
{"kioskInvalidApp", IDS_MD_EXTENSIONS_KIOSK_INVALID_APP},
{"kioskDisableBailout",
IDS_MD_EXTENSIONS_KIOSK_DISABLE_BAILOUT_SHORTCUT_LABEL},
{"kioskDisableBailoutWarningTitle",
IDS_MD_EXTENSIONS_KIOSK_DISABLE_BAILOUT_SHORTCUT_WARNING_TITLE},
#endif
};
AddLocalizedStringsBulk(source, localized_strings,
base::size(localized_strings));
source->AddString("errorLinesNotShownSingular",
l10n_util::GetPluralStringFUTF16(
IDS_MD_EXTENSIONS_ERROR_LINES_NOT_SHOWN, 1));
source->AddString("errorLinesNotShownPlural",
l10n_util::GetPluralStringFUTF16(
IDS_MD_EXTENSIONS_ERROR_LINES_NOT_SHOWN, 2));
source->AddString(
"itemSuspiciousInstall",
l10n_util::GetStringFUTF16(
IDS_EXTENSIONS_ADDED_WITHOUT_KNOWLEDGE,
l10n_util::GetStringUTF16(IDS_EXTENSION_WEB_STORE_TITLE)));
source->AddString(
"suspiciousInstallHelpUrl",
base::ASCIIToUTF16(google_util::AppendGoogleLocaleParam(
GURL(chrome::kRemoveNonCWSExtensionURL),
g_browser_process->GetApplicationLocale())
.spec()));
#if defined(OS_CHROMEOS)
source->AddString(
"kioskDisableBailoutWarningBody",
l10n_util::GetStringFUTF16(
IDS_MD_EXTENSIONS_KIOSK_DISABLE_BAILOUT_SHORTCUT_WARNING_BODY,
l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_OS_NAME)));
#endif
source->AddString(
"getMoreExtensionsUrl",
base::ASCIIToUTF16(
google_util::AppendGoogleLocaleParam(
GURL(extension_urls::GetWebstoreExtensionsCategoryURL()),
g_browser_process->GetApplicationLocale())
.spec()));
source->AddString(
"hostPermissionsLearnMoreLink",
l10n_util::GetStringFUTF16(
IDS_MD_EXTENSIONS_HOST_PERMISSIONS_LEARN_MORE,
base::ASCIIToUTF16(
chrome_extension_constants::kRuntimeHostPermissionsHelpURL)));
source->AddBoolean(kInDevModeKey, in_dev_mode);
source->AddBoolean(kShowActivityLogKey,
base::CommandLine::ForCurrentProcess()->HasSwitch(
::switches::kEnableExtensionActivityLogging));
source->AddString(kLoadTimeClassesKey, GetLoadTimeClasses(in_dev_mode));
#if BUILDFLAG(OPTIMIZE_WEBUI)
source->AddResourcePath("crisper.js", IDR_MD_EXTENSIONS_CRISPER_JS);
source->SetDefaultResource(
base::FeatureList::IsEnabled(features::kWebUIPolymer2) ?
IDR_MD_EXTENSIONS_VULCANIZED_P2_HTML :
IDR_MD_EXTENSIONS_VULCANIZED_HTML);
source->UseGzip();
#else
// Add all MD Extensions resources.
for (size_t i = 0; i < kExtensionsResourcesSize; ++i) {
source->AddResourcePath(kExtensionsResources[i].name,
kExtensionsResources[i].value);
}
source->SetDefaultResource(IDR_MD_EXTENSIONS_EXTENSIONS_HTML);
#endif
return source;
}
} // namespace
ExtensionsUI::ExtensionsUI(content::WebUI* web_ui) : WebUIController(web_ui) {
Profile* profile = Profile::FromWebUI(web_ui);
content::WebUIDataSource* source = nullptr;
in_dev_mode_.Init(
prefs::kExtensionsUIDeveloperMode, profile->GetPrefs(),
base::Bind(&ExtensionsUI::OnDevModeChanged, base::Unretained(this)));
source = CreateMdExtensionsSource(*in_dev_mode_);
DarkModeHandler::Initialize(web_ui, source);
#if defined(OS_CHROMEOS)
auto kiosk_app_handler = std::make_unique<chromeos::KioskAppsHandler>(
chromeos::OwnerSettingsServiceChromeOSFactory::GetForBrowserContext(
profile));
web_ui->AddMessageHandler(std::move(kiosk_app_handler));
#endif
// Need to allow <object> elements so that the <extensionoptions> browser
// plugin can be loaded within chrome://extensions.
source->OverrideContentSecurityPolicyObjectSrc("object-src 'self';");
content::WebUIDataSource::Add(profile, source);
// Handles its own lifetime.
new ExtensionWebUiTimer(web_ui->GetWebContents());
}
ExtensionsUI::~ExtensionsUI() {}
// static
base::RefCountedMemory* ExtensionsUI::GetFaviconResourceBytes(
ui::ScaleFactor scale_factor) {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb.LoadDataResourceBytesForScale(IDR_EXTENSIONS_FAVICON, scale_factor);
}
void ExtensionsUI::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(
prefs::kExtensionsUIDeveloperMode, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}
// Normally volatile data does not belong in loadTimeData, but in this case
// prevents flickering on a very prominent surface (top of the landing page).
void ExtensionsUI::OnDevModeChanged() {
auto update = std::make_unique<base::DictionaryValue>();
update->SetBoolean(kInDevModeKey, *in_dev_mode_);
update->SetString(kLoadTimeClassesKey, GetLoadTimeClasses(*in_dev_mode_));
content::WebUIDataSource::Update(Profile::FromWebUI(web_ui()),
chrome::kChromeUIExtensionsHost,
std::move(update));
}
} // namespace extensions