blob: 8beb6bec029b590f8294288bce36a45df42c8df9 [file] [log] [blame]
// Copyright 2012 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/omnibox/omnibox_api.h"
#include <stddef.h>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/permissions/active_tab_permission_granter.h"
#include "chrome/browser/omnibox/omnibox_input_watcher_factory.h"
#include "chrome/browser/omnibox/omnibox_suggestions_watcher_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/common/extensions/api/omnibox.h"
#include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
#include "components/omnibox/browser/omnibox_input_watcher.h"
#include "components/omnibox/browser/omnibox_suggestions_watcher.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_prefs_factory.h"
#include "extensions/browser/icon_util.h"
#include "extensions/browser/install_prefs_helper.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/mojom/api_permission_id.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
#include "ui/gfx/image/image.h"
namespace extensions {
namespace omnibox = api::omnibox;
namespace SendSuggestions = omnibox::SendSuggestions;
namespace SetDefaultSuggestion = omnibox::SetDefaultSuggestion;
namespace {
const char kSuggestionContent[] = "content";
const char kCurrentTabDisposition[] = "currentTab";
const char kForegroundTabDisposition[] = "newForegroundTab";
const char kBackgroundTabDisposition[] = "newBackgroundTab";
// Pref key for omnibox.setDefaultSuggestion.
const char kOmniboxDefaultSuggestion[] = "omnibox_default_suggestion";
std::optional<omnibox::SuggestResult> GetOmniboxDefaultSuggestion(
Profile* profile,
const ExtensionId& extension_id) {
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
if (!prefs) {
return std::nullopt;
}
const base::Value::Dict* dict =
prefs->ReadPrefAsDict(extension_id, kOmniboxDefaultSuggestion);
if (!dict) {
return std::nullopt;
}
return omnibox::SuggestResult::FromValue(*dict);
}
// Tries to set the omnibox default suggestion; returns true on success or
// false on failure.
bool SetOmniboxDefaultSuggestion(
Profile* profile,
const ExtensionId& extension_id,
const omnibox::DefaultSuggestResult& suggestion) {
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
if (!prefs)
return false;
base::Value::Dict dict = suggestion.ToValue();
// Add the content field so that the dictionary can be used to populate an
// omnibox::SuggestResult.
dict.Set(kSuggestionContent, base::Value(base::Value::Type::STRING));
prefs->UpdateExtensionPref(extension_id, kOmniboxDefaultSuggestion,
base::Value(std::move(dict)));
return true;
}
// Returns a string used as a template URL string of the extension.
std::string GetTemplateURLStringForExtension(const ExtensionId& extension_id) {
// This URL is not actually used for navigation. It holds the extension's ID.
return std::string(extensions::kExtensionScheme) + "://" +
extension_id + "/?q={searchTerms}";
}
bool IsUnscopedModeAllowed(const Extension* extension) {
// The extension can use unscoepd mode if the feature is enabled and the
// permission has been granted.
return base::FeatureList::IsEnabled(
extensions_features::kExperimentalOmniboxLabs) &&
extension->permissions_data()->HasAPIPermission(
mojom::APIPermissionID::kOmniboxDirectInput);
}
} // namespace
// static
void ExtensionOmniboxEventRouter::OnInputStarted(
Profile* profile,
const ExtensionId& extension_id) {
auto event = std::make_unique<Event>(events::OMNIBOX_ON_INPUT_STARTED,
omnibox::OnInputStarted::kEventName,
base::Value::List(), profile);
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
}
// static
bool ExtensionOmniboxEventRouter::OnInputChanged(
Profile* profile,
const ExtensionId& extension_id,
const std::string& input,
int suggest_id) {
EventRouter* event_router = EventRouter::Get(profile);
if (!event_router->ExtensionHasEventListener(
extension_id, omnibox::OnInputChanged::kEventName))
return false;
base::Value::List args;
args.Append(input);
args.Append(suggest_id);
auto event = std::make_unique<Event>(events::OMNIBOX_ON_INPUT_CHANGED,
omnibox::OnInputChanged::kEventName,
std::move(args), profile);
event_router->DispatchEventToExtension(extension_id, std::move(event));
return true;
}
// static
void ExtensionOmniboxEventRouter::OnInputEntered(
content::WebContents* web_contents,
const ExtensionId& extension_id,
const std::string& input,
WindowOpenDisposition disposition) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
const Extension* extension =
ExtensionRegistry::Get(profile)->enabled_extensions().GetByID(
extension_id);
CHECK(extension);
extensions::ActiveTabPermissionGranter::FromWebContents(web_contents)
->GrantIfRequested(extension);
base::Value::List args;
args.Append(input);
if (disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB)
args.Append(kForegroundTabDisposition);
else if (disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB)
args.Append(kBackgroundTabDisposition);
else
args.Append(kCurrentTabDisposition);
auto event = std::make_unique<Event>(events::OMNIBOX_ON_INPUT_ENTERED,
omnibox::OnInputEntered::kEventName,
std::move(args), profile);
event->user_gesture = EventRouter::UserGestureState::kEnabled;
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
OmniboxInputWatcherFactory::GetForBrowserContext(profile)
->NotifyInputEntered();
}
// static
void ExtensionOmniboxEventRouter::OnInputCancelled(
Profile* profile,
const ExtensionId& extension_id) {
auto event = std::make_unique<Event>(events::OMNIBOX_ON_INPUT_CANCELLED,
omnibox::OnInputCancelled::kEventName,
base::Value::List(), profile);
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
}
void ExtensionOmniboxEventRouter::OnDeleteSuggestion(
Profile* profile,
const ExtensionId& extension_id,
const std::string& suggestion_text) {
base::Value::List args;
args.Append(suggestion_text);
auto event = std::make_unique<Event>(events::OMNIBOX_ON_DELETE_SUGGESTION,
omnibox::OnDeleteSuggestion::kEventName,
std::move(args), profile);
EventRouter::Get(profile)->DispatchEventToExtension(extension_id,
std::move(event));
}
// static
void ExtensionOmniboxEventRouter::OnActionExecuted(
Profile* profile,
const ExtensionId& extension_id,
const std::string& action_name,
const std::string& content) {
EventRouter* event_router = EventRouter::Get(profile);
if (!event_router->ExtensionHasEventListener(
extension_id, omnibox::OnActionExecuted::kEventName)) {
return;
}
omnibox::ActionExecution action_execution;
action_execution.action_name = action_name;
action_execution.content = content;
auto event = std::make_unique<Event>(
events::OMNIBOX_ON_ACTION_EXECUTED, omnibox::OnActionExecuted::kEventName,
omnibox::OnActionExecuted::Create(std::move(action_execution)), profile);
event->user_gesture = EventRouter::UserGestureState::kEnabled;
event_router->DispatchEventToExtension(extension_id, std::move(event));
}
OmniboxAPI::OmniboxAPI(content::BrowserContext* context)
: profile_(Profile::FromBrowserContext(context)),
url_service_(TemplateURLServiceFactory::GetForProfile(profile_)) {
extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_));
if (url_service_) {
template_url_subscription_ =
url_service_->RegisterOnLoadedCallback(base::BindOnce(
&OmniboxAPI::OnTemplateURLsLoaded, base::Unretained(this)));
}
// Use monochrome icons for Omnibox icons.
omnibox_icon_manager_.set_monochrome(true);
permissions_manager_observation_.Observe(PermissionsManager::Get(profile_));
}
void OmniboxAPI::Shutdown() {
template_url_subscription_ = {};
permissions_manager_observation_.Reset();
}
OmniboxAPI::~OmniboxAPI() = default;
static base::LazyInstance<BrowserContextKeyedAPIFactory<OmniboxAPI>>::
DestructorAtExit g_omnibox_api_factory = LAZY_INSTANCE_INITIALIZER;
// static
BrowserContextKeyedAPIFactory<OmniboxAPI>* OmniboxAPI::GetFactoryInstance() {
return g_omnibox_api_factory.Pointer();
}
// static
OmniboxAPI* OmniboxAPI::Get(content::BrowserContext* context) {
return BrowserContextKeyedAPIFactory<OmniboxAPI>::Get(context);
}
void OmniboxAPI::OnExtensionLoaded(content::BrowserContext* browser_context,
const Extension* extension) {
const std::string& keyword = OmniboxInfo::GetKeyword(extension);
if (!keyword.empty()) {
// Load the omnibox icon so it will be ready to display in the URL bar.
omnibox_icon_manager_.LoadIcon(profile_, extension);
if (url_service_) {
url_service_->Load();
if (url_service_->loaded()) {
url_service_->RegisterExtensionControlledTURL(
extension->id(), extension->short_name(), keyword,
GetTemplateURLStringForExtension(extension->id()),
GetLastUpdateTime(ExtensionPrefs::Get(profile_), extension->id()),
IsUnscopedModeAllowed(extension));
} else {
pending_extensions_.insert(extension);
}
}
}
}
void OmniboxAPI::OnExtensionUnloaded(content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) {
if (!OmniboxInfo::GetKeyword(extension).empty() && url_service_) {
if (url_service_->loaded()) {
url_service_->RemoveExtensionControlledTURL(
extension->id(), TemplateURL::OMNIBOX_API_EXTENSION);
} else {
pending_extensions_.erase(extension);
}
}
}
void OmniboxAPI::OnExtensionPermissionsUpdated(
const Extension& extension,
const PermissionSet& permissions,
PermissionsManager::UpdateReason reason) {
if (!permissions.HasAPIPermission(
mojom::APIPermissionID::kOmniboxDirectInput)) {
return;
}
if (reason == PermissionsManager::UpdateReason::kAdded &&
base::FeatureList::IsEnabled(
extensions_features::kExperimentalOmniboxLabs)) {
url_service_->AddToUnscopedModeExtensionIds(extension.id());
} else if (reason == PermissionsManager::UpdateReason::kRemoved) {
url_service_->RemoveFromUnscopedModeExtensionIdsIfPresent(extension.id());
}
}
gfx::Image OmniboxAPI::GetOmniboxIcon(const ExtensionId& extension_id) {
return omnibox_icon_manager_.GetIcon(extension_id);
}
void OmniboxAPI::OnTemplateURLsLoaded() {
// Register keywords for pending extensions.
template_url_subscription_ = {};
for (const Extension* i : pending_extensions_) {
url_service_->RegisterExtensionControlledTURL(
i->id(), i->short_name(), OmniboxInfo::GetKeyword(i),
GetTemplateURLStringForExtension(i->id()),
GetLastUpdateTime(ExtensionPrefs::Get(profile_), i->id()),
IsUnscopedModeAllowed(i));
}
pending_extensions_.clear();
}
template <>
void BrowserContextKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() {
DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
DependsOn(ExtensionPrefsFactory::GetInstance());
DependsOn(TemplateURLServiceFactory::GetInstance());
DependsOn(PermissionsManager::GetFactory());
}
OmniboxSendSuggestionsFunction::OmniboxSendSuggestionsFunction() = default;
OmniboxSendSuggestionsFunction::~OmniboxSendSuggestionsFunction() = default;
ExtensionFunction::ResponseAction OmniboxSendSuggestionsFunction::Run() {
std::optional<api::omnibox::SendSuggestions::Params> params =
SendSuggestions::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
request_id_ = params->request_id;
if (!params->suggest_results.empty()) {
std::vector<std::string_view> inputs;
inputs.reserve(params->suggest_results.size());
for (const auto& suggestion : params->suggest_results) {
std::vector<ExtensionSuggestion::Action> actions;
inputs.push_back(suggestion.description);
if (suggestion.actions) {
if (!IsUnscopedModeAllowed(extension())) {
return RespondNow(
Error(ExtensionOmniboxEventRouter::
kActionsRequireDirectInputPermissionError));
}
if (suggestion.actions->size() >
ExtensionOmniboxEventRouter::kMaxSuggestionActions) {
return RespondNow(Error(base::StringPrintf(
ExtensionOmniboxEventRouter::kMaxSuggestionActionsExceededError,
suggestion.actions->size(),
ExtensionOmniboxEventRouter::kMaxSuggestionActions)));
}
actions.reserve(suggestion.actions->size());
for (const auto& action : *suggestion.actions) {
base::Value::Dict canvas_set =
action.icon ? action.icon->ToValue() : base::Value::Dict();
gfx::ImageSkia image_skia;
if (!canvas_set.empty()) {
base::Value::Dict& image_data = *canvas_set.FindDict("imageData");
// The image data should have been verified by the pre-validation
// param update.
CHECK(!image_data.empty());
// TODO(crbug.com/408069174): Move ParseIconFromCanvasDictionary
// outside `ExtensionAction` into a common file.
if (extensions::ParseIconFromCanvasDictionary(image_data,
&image_skia) !=
extensions::IconParseResult::kSuccess) {
return RespondNow(Error(base::StringPrintf(
ExtensionOmniboxEventRouter::kActionIconError,
suggestion.description, action.name)));
}
}
actions.emplace_back(action.name, action.label, action.tooltip_text,
gfx::Image(image_skia));
}
}
const std::vector<api::omnibox::MatchClassification> empty_styles;
const std::vector<api::omnibox::MatchClassification>* styles_ptr =
suggestion.description_styles ? &suggestion.description_styles.value()
: &empty_styles;
extension_suggestions_.emplace_back(
suggestion.content, suggestion.description,
suggestion.deletable.value_or(false),
StyleTypesToACMatchClassifications(styles_ptr,
suggestion.description),
std::move(actions), suggestion.icon_url);
}
if (is_from_service_worker()) {
ParseDescriptionsAndStyles(
inputs,
base::BindOnce(
&OmniboxSendSuggestionsFunction::OnParsedDescriptionsAndStyles,
this));
return RespondLater();
}
}
NotifySuggestionsReady();
return RespondNow(NoArguments());
}
void OmniboxSendSuggestionsFunction::OnParsedDescriptionsAndStyles(
DescriptionAndStylesResult result) {
DCHECK_NE(0u, extension_suggestions_.size());
// Since the XML parsing happens asynchronously, the browser context can be
// torn down in the interim. If this happens, early-out.
if (!browser_context()) {
return;
}
if (!result.error.empty()) {
Respond(Error(std::move(result.error)));
return;
}
if (result.descriptions_and_styles.size() != extension_suggestions_.size()) {
// This can technically happen if the extension provided input that mucked
// with our XML parsing (see suggestion_parser_unittest.cc). This isn't a
// security concern, but would mean that our mapping to record the other
// fields in the suggestion are mismatched. Abort. Since there's no
// legitimate case for this happening, just emit a generic error message.
Respond(Error("Invalid input."));
return;
}
for (size_t i = 0; i < extension_suggestions_.size(); ++i) {
extension_suggestions_[i].description =
base::UTF16ToUTF8(result.descriptions_and_styles[i].description);
extension_suggestions_[i].match_classifications =
StyleTypesToACMatchClassifications(
&result.descriptions_and_styles[i].styles,
extension_suggestions_[i].description);
}
NotifySuggestionsReady();
Respond(NoArguments());
}
void OmniboxSendSuggestionsFunction::NotifySuggestionsReady() {
Profile* profile =
Profile::FromBrowserContext(browser_context())->GetOriginalProfile();
OmniboxSuggestionsWatcherFactory::GetForBrowserContext(profile)
->NotifySuggestionsReady(extension_suggestions_, request_id_,
extension_id());
}
ExtensionFunction::ResponseAction OmniboxSetDefaultSuggestionFunction::Run() {
std::optional<SetDefaultSuggestion::Params> params =
SetDefaultSuggestion::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
if (!params->suggestion.description_styles) {
ParseDescriptionAndStyles(
params->suggestion.description,
base::BindOnce(
&OmniboxSetDefaultSuggestionFunction::OnParsedDescriptionAndStyles,
this));
return RespondLater();
}
SetDefaultSuggestion(params->suggestion);
return RespondNow(NoArguments());
}
void OmniboxSetDefaultSuggestionFunction::OnParsedDescriptionAndStyles(
DescriptionAndStylesResult result) {
if (!result.error.empty()) {
Respond(Error(std::move(result.error)));
return;
}
DCHECK_EQ(1u, result.descriptions_and_styles.size());
DescriptionAndStyles& single_result = result.descriptions_and_styles[0];
omnibox::DefaultSuggestResult default_suggestion;
default_suggestion.description = base::UTF16ToUTF8(single_result.description);
default_suggestion.description_styles.emplace();
default_suggestion.description_styles->swap(single_result.styles);
SetDefaultSuggestion(default_suggestion);
Respond(NoArguments());
}
void OmniboxSetDefaultSuggestionFunction::SetDefaultSuggestion(
const omnibox::DefaultSuggestResult& suggestion) {
Profile* profile = Profile::FromBrowserContext(browser_context());
if (SetOmniboxDefaultSuggestion(profile, extension_id(), suggestion)) {
OmniboxSuggestionsWatcherFactory::GetForBrowserContext(
profile->GetOriginalProfile())
->NotifyDefaultSuggestionChanged();
}
}
// This function converts style information populated by the JSON schema
// compiler into an ACMatchClassifications object.
ACMatchClassifications StyleTypesToACMatchClassifications(
const std::vector<omnibox::MatchClassification>* description_styles,
const std::string& suggestion_description) {
ACMatchClassifications match_classifications;
if (!description_styles->empty()) {
std::u16string description = base::UTF8ToUTF16(suggestion_description);
std::vector<int> styles(description.length(), 0);
for (const omnibox::MatchClassification& style : *description_styles) {
int length = style.length ? *style.length : description.length();
size_t offset = style.offset >= 0
? style.offset
: std::max(0, static_cast<int>(description.length()) +
style.offset);
int type_class;
switch (style.type) {
case omnibox::DescriptionStyleType::kUrl:
type_class = AutocompleteMatch::ACMatchClassification::URL;
break;
case omnibox::DescriptionStyleType::kMatch:
type_class = AutocompleteMatch::ACMatchClassification::MATCH;
break;
case omnibox::DescriptionStyleType::kDim:
type_class = AutocompleteMatch::ACMatchClassification::DIM;
break;
default:
type_class = AutocompleteMatch::ACMatchClassification::NONE;
return match_classifications;
}
for (size_t j = offset; j < offset + length && j < styles.size(); ++j)
styles[j] |= type_class;
}
for (size_t i = 0; i < styles.size(); ++i) {
if (i == 0 || styles[i] != styles[i-1])
match_classifications.push_back(
ACMatchClassification(i, styles[i]));
}
} else {
match_classifications.push_back(
ACMatchClassification(0, ACMatchClassification::NONE));
}
return match_classifications;
}
void ApplyDefaultSuggestionForExtensionKeyword(
Profile* profile,
const TemplateURL* keyword,
const std::u16string& remaining_input,
AutocompleteMatch* match) {
DCHECK(keyword->type() == TemplateURL::OMNIBOX_API_EXTENSION);
std::optional<omnibox::SuggestResult> suggestion(
GetOmniboxDefaultSuggestion(profile, keyword->GetExtensionId()));
if (!suggestion || suggestion->description.empty())
return; // fall back to the universal default
const std::u16string kPlaceholderText(u"%s");
const std::u16string kReplacementText(u"<input>");
std::u16string description = base::UTF8ToUTF16(suggestion->description);
ACMatchClassifications& description_styles = match->contents_class;
const std::vector<api::omnibox::MatchClassification> empty_styles;
const std::vector<api::omnibox::MatchClassification>* styles_list =
suggestion->description_styles ? &suggestion->description_styles.value()
: &empty_styles;
description_styles =
StyleTypesToACMatchClassifications(styles_list, suggestion->description);
// Replace "%s" with the user's input and adjust the style offsets to the
// new length of the description.
size_t placeholder(description.find(kPlaceholderText, 0));
if (placeholder != std::u16string::npos) {
std::u16string replacement =
remaining_input.empty() ? kReplacementText : remaining_input;
description.replace(placeholder, kPlaceholderText.length(), replacement);
for (auto& description_style : description_styles) {
if (description_style.offset > placeholder)
description_style.offset += replacement.length() - 2;
}
}
match->contents.assign(description);
}
} // namespace extensions