| // 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 |