blob: 3ccf5ea1204774e377557628b7d348f41fea9722 [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/extensions/api/omnibox/omnibox_api.h"
#include <stddef.h>
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/tab_helper.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/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_prefs_factory.h"
#include "extensions/browser/notification_types.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::unique_ptr<omnibox::SuggestResult> GetOmniboxDefaultSuggestion(
Profile* profile,
const std::string& extension_id) {
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
std::unique_ptr<omnibox::SuggestResult> suggestion;
const base::DictionaryValue* dict = NULL;
if (prefs && prefs->ReadPrefAsDictionary(extension_id,
kOmniboxDefaultSuggestion,
&dict)) {
suggestion = std::make_unique<omnibox::SuggestResult>();
omnibox::SuggestResult::Populate(*dict, suggestion.get());
}
return suggestion;
}
// Tries to set the omnibox default suggestion; returns true on success or
// false on failure.
bool SetOmniboxDefaultSuggestion(
Profile* profile,
const std::string& extension_id,
const omnibox::DefaultSuggestResult& suggestion) {
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
if (!prefs)
return false;
std::unique_ptr<base::DictionaryValue> dict = suggestion.ToValue();
// Add the content field so that the dictionary can be used to populate an
// omnibox::SuggestResult.
dict->SetWithoutPathExpansion(
kSuggestionContent,
std::make_unique<base::Value>(base::Value::Type::STRING));
prefs->UpdateExtensionPref(extension_id, kOmniboxDefaultSuggestion,
std::move(dict));
return true;
}
// Returns a string used as a template URL string of the extension.
std::string GetTemplateURLStringForExtension(const std::string& 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}";
}
} // namespace
// static
void ExtensionOmniboxEventRouter::OnInputStarted(
Profile* profile, const std::string& extension_id) {
auto event = std::make_unique<Event>(
events::OMNIBOX_ON_INPUT_STARTED, omnibox::OnInputStarted::kEventName,
std::make_unique<base::ListValue>(), profile);
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
}
// static
bool ExtensionOmniboxEventRouter::OnInputChanged(
Profile* profile, const std::string& 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;
auto args(std::make_unique<base::ListValue>());
args->Set(0, std::make_unique<base::Value>(input));
args->Set(1, std::make_unique<base::Value>(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 std::string& 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::TabHelper::FromWebContents(web_contents)->
active_tab_permission_granter()->GrantIfRequested(extension);
auto args(std::make_unique<base::ListValue>());
args->Set(0, std::make_unique<base::Value>(input));
if (disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB)
args->Set(1, std::make_unique<base::Value>(kForegroundTabDisposition));
else if (disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB)
args->Set(1, std::make_unique<base::Value>(kBackgroundTabDisposition));
else
args->Set(1, std::make_unique<base::Value>(kCurrentTabDisposition));
auto event = std::make_unique<Event>(events::OMNIBOX_ON_INPUT_ENTERED,
omnibox::OnInputEntered::kEventName,
std::move(args), profile);
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
content::NotificationService::current()->Notify(
extensions::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
content::Source<Profile>(profile),
content::NotificationService::NoDetails());
}
// static
void ExtensionOmniboxEventRouter::OnInputCancelled(
Profile* profile, const std::string& extension_id) {
auto event = std::make_unique<Event>(
events::OMNIBOX_ON_INPUT_CANCELLED, omnibox::OnInputCancelled::kEventName,
std::make_unique<base::ListValue>(), profile);
EventRouter::Get(profile)
->DispatchEventToExtension(extension_id, std::move(event));
}
void ExtensionOmniboxEventRouter::OnDeleteSuggestion(
Profile* profile,
const std::string& extension_id,
const std::string& suggestion_text) {
auto args(std::make_unique<base::ListValue>());
args->Set(0, std::make_unique<base::Value>(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));
}
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);
}
void OmniboxAPI::Shutdown() {
template_url_subscription_ = {};
}
OmniboxAPI::~OmniboxAPI() {
}
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_->RegisterOmniboxKeyword(
extension->id(), extension->short_name(), keyword,
GetTemplateURLStringForExtension(extension->id()),
ExtensionPrefs::Get(profile_)->GetInstallTime(extension->id()));
} 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);
}
}
}
gfx::Image OmniboxAPI::GetOmniboxIcon(const std::string& extension_id) {
return omnibox_icon_manager_.GetIcon(extension_id);
}
void OmniboxAPI::OnTemplateURLsLoaded() {
// Register keywords for pending extensions.
template_url_subscription_ = {};
for (const auto* i : pending_extensions_) {
url_service_->RegisterOmniboxKeyword(
i->id(), i->short_name(), OmniboxInfo::GetKeyword(i),
GetTemplateURLStringForExtension(i->id()),
ExtensionPrefs::Get(profile_)->GetInstallTime(i->id()));
}
pending_extensions_.clear();
}
template <>
void BrowserContextKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() {
DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
DependsOn(ExtensionPrefsFactory::GetInstance());
DependsOn(TemplateURLServiceFactory::GetInstance());
}
ExtensionFunction::ResponseAction OmniboxSendSuggestionsFunction::Run() {
std::unique_ptr<SendSuggestions::Params> params(
SendSuggestions::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
content::NotificationService::current()->Notify(
extensions::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
content::Source<Profile>(
Profile::FromBrowserContext(browser_context())->GetOriginalProfile()),
content::Details<SendSuggestions::Params>(params.get()));
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction OmniboxSetDefaultSuggestionFunction::Run() {
std::unique_ptr<SetDefaultSuggestion::Params> params(
SetDefaultSuggestion::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
Profile* profile = Profile::FromBrowserContext(browser_context());
if (SetOmniboxDefaultSuggestion(profile, extension_id(),
params->suggestion)) {
content::NotificationService::current()->Notify(
extensions::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
content::Source<Profile>(profile->GetOriginalProfile()),
content::NotificationService::NoDetails());
}
return RespondNow(NoArguments());
}
// This function converts style information populated by the JSON schema
// compiler into an ACMatchClassifications object.
ACMatchClassifications StyleTypesToACMatchClassifications(
const omnibox::SuggestResult &suggestion) {
ACMatchClassifications match_classifications;
if (suggestion.description_styles) {
std::u16string description = base::UTF8ToUTF16(suggestion.description);
std::vector<int> styles(description.length(), 0);
for (const omnibox::MatchClassification& style :
*suggestion.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::DESCRIPTION_STYLE_TYPE_URL:
type_class = AutocompleteMatch::ACMatchClassification::URL;
break;
case omnibox::DESCRIPTION_STYLE_TYPE_MATCH:
type_class = AutocompleteMatch::ACMatchClassification::MATCH;
break;
case omnibox::DESCRIPTION_STYLE_TYPE_DIM:
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::unique_ptr<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;
description_styles = StyleTypesToACMatchClassifications(*suggestion);
// 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 (size_t i = 0; i < description_styles.size(); ++i) {
if (description_styles[i].offset > placeholder)
description_styles[i].offset += replacement.length() - 2;
}
}
match->contents.assign(description);
}
} // namespace extensions