1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/extensions/api/omnibox/omnibox_api.h" 6 7#include "base/json/json_writer.h" 8#include "base/lazy_instance.h" 9#include "base/metrics/histogram.h" 10#include "base/strings/string16.h" 11#include "base/strings/utf_string_conversions.h" 12#include "base/values.h" 13#include "chrome/browser/chrome_notification_types.h" 14#include "chrome/browser/extensions/event_router.h" 15#include "chrome/browser/extensions/extension_prefs.h" 16#include "chrome/browser/extensions/extension_service.h" 17#include "chrome/browser/extensions/extension_system.h" 18#include "chrome/browser/extensions/tab_helper.h" 19#include "chrome/browser/profiles/profile.h" 20#include "chrome/browser/search_engines/template_url.h" 21#include "chrome/browser/search_engines/template_url_service.h" 22#include "chrome/browser/search_engines/template_url_service_factory.h" 23#include "chrome/common/extensions/api/omnibox.h" 24#include "chrome/common/extensions/api/omnibox/omnibox_handler.h" 25#include "chrome/common/extensions/extension.h" 26#include "content/public/browser/notification_details.h" 27#include "content/public/browser/notification_service.h" 28#include "ui/gfx/image/image.h" 29 30namespace events { 31const char kOnInputStarted[] = "omnibox.onInputStarted"; 32const char kOnInputChanged[] = "omnibox.onInputChanged"; 33const char kOnInputEntered[] = "omnibox.onInputEntered"; 34const char kOnInputCancelled[] = "omnibox.onInputCancelled"; 35} // namespace events 36 37namespace extensions { 38 39namespace omnibox = api::omnibox; 40namespace SendSuggestions = omnibox::SendSuggestions; 41namespace SetDefaultSuggestion = omnibox::SetDefaultSuggestion; 42 43namespace { 44 45const char kSuggestionContent[] = "content"; 46const char kSuggestionDescription[] = "description"; 47const char kSuggestionDescriptionStyles[] = "descriptionStyles"; 48const char kSuggestionDescriptionStylesRaw[] = "descriptionStylesRaw"; 49const char kDescriptionStylesType[] = "type"; 50const char kDescriptionStylesOffset[] = "offset"; 51const char kDescriptionStylesLength[] = "length"; 52const char kCurrentTabDisposition[] = "currentTab"; 53const char kForegroundTabDisposition[] = "newForegroundTab"; 54const char kBackgroundTabDisposition[] = "newBackgroundTab"; 55 56// Pref key for omnibox.setDefaultSuggestion. 57const char kOmniboxDefaultSuggestion[] = "omnibox_default_suggestion"; 58 59#if defined(OS_LINUX) 60static const int kOmniboxIconPaddingLeft = 2; 61static const int kOmniboxIconPaddingRight = 2; 62#elif defined(OS_MACOSX) 63static const int kOmniboxIconPaddingLeft = 0; 64static const int kOmniboxIconPaddingRight = 2; 65#else 66static const int kOmniboxIconPaddingLeft = 0; 67static const int kOmniboxIconPaddingRight = 0; 68#endif 69 70scoped_ptr<omnibox::SuggestResult> GetOmniboxDefaultSuggestion( 71 Profile* profile, 72 const std::string& extension_id) { 73 ExtensionPrefs* prefs = 74 ExtensionSystem::Get(profile)->extension_service()->extension_prefs(); 75 76 scoped_ptr<omnibox::SuggestResult> suggestion; 77 const base::DictionaryValue* dict = NULL; 78 if (prefs && prefs->ReadPrefAsDictionary(extension_id, 79 kOmniboxDefaultSuggestion, 80 &dict)) { 81 suggestion.reset(new omnibox::SuggestResult); 82 omnibox::SuggestResult::Populate(*dict, suggestion.get()); 83 } 84 return suggestion.Pass(); 85} 86 87// Tries to set the omnibox default suggestion; returns true on success or 88// false on failure. 89bool SetOmniboxDefaultSuggestion( 90 Profile* profile, 91 const std::string& extension_id, 92 const omnibox::DefaultSuggestResult& suggestion) { 93 ExtensionPrefs* prefs = 94 ExtensionSystem::Get(profile)->extension_service()->extension_prefs(); 95 if (!prefs) 96 return false; 97 98 scoped_ptr<base::DictionaryValue> dict = suggestion.ToValue(); 99 // Add the content field so that the dictionary can be used to populate an 100 // omnibox::SuggestResult. 101 dict->SetWithoutPathExpansion(kSuggestionContent, new base::StringValue("")); 102 prefs->UpdateExtensionPref(extension_id, 103 kOmniboxDefaultSuggestion, 104 dict.release()); 105 106 return true; 107} 108 109} // namespace 110 111// static 112void ExtensionOmniboxEventRouter::OnInputStarted( 113 Profile* profile, const std::string& extension_id) { 114 scoped_ptr<Event> event(new Event( 115 events::kOnInputStarted, make_scoped_ptr(new base::ListValue()))); 116 event->restrict_to_profile = profile; 117 ExtensionSystem::Get(profile)->event_router()-> 118 DispatchEventToExtension(extension_id, event.Pass()); 119} 120 121// static 122bool ExtensionOmniboxEventRouter::OnInputChanged( 123 Profile* profile, const std::string& extension_id, 124 const std::string& input, int suggest_id) { 125 if (!extensions::ExtensionSystem::Get(profile)->event_router()-> 126 ExtensionHasEventListener(extension_id, events::kOnInputChanged)) 127 return false; 128 129 scoped_ptr<base::ListValue> args(new base::ListValue()); 130 args->Set(0, Value::CreateStringValue(input)); 131 args->Set(1, Value::CreateIntegerValue(suggest_id)); 132 133 scoped_ptr<Event> event(new Event(events::kOnInputChanged, args.Pass())); 134 event->restrict_to_profile = profile; 135 ExtensionSystem::Get(profile)->event_router()-> 136 DispatchEventToExtension(extension_id, event.Pass()); 137 return true; 138} 139 140// static 141void ExtensionOmniboxEventRouter::OnInputEntered( 142 content::WebContents* web_contents, 143 const std::string& extension_id, 144 const std::string& input, 145 WindowOpenDisposition disposition) { 146 Profile* profile = 147 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 148 149 const Extension* extension = 150 ExtensionSystem::Get(profile)->extension_service()->extensions()-> 151 GetByID(extension_id); 152 CHECK(extension); 153 extensions::TabHelper::FromWebContents(web_contents)-> 154 active_tab_permission_granter()->GrantIfRequested(extension); 155 156 scoped_ptr<base::ListValue> args(new base::ListValue()); 157 args->Set(0, Value::CreateStringValue(input)); 158 if (disposition == NEW_FOREGROUND_TAB) 159 args->Set(1, Value::CreateStringValue(kForegroundTabDisposition)); 160 else if (disposition == NEW_BACKGROUND_TAB) 161 args->Set(1, Value::CreateStringValue(kBackgroundTabDisposition)); 162 else 163 args->Set(1, Value::CreateStringValue(kCurrentTabDisposition)); 164 165 scoped_ptr<Event> event(new Event(events::kOnInputEntered, args.Pass())); 166 event->restrict_to_profile = profile; 167 ExtensionSystem::Get(profile)->event_router()-> 168 DispatchEventToExtension(extension_id, event.Pass()); 169 170 content::NotificationService::current()->Notify( 171 chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED, 172 content::Source<Profile>(profile), 173 content::NotificationService::NoDetails()); 174} 175 176// static 177void ExtensionOmniboxEventRouter::OnInputCancelled( 178 Profile* profile, const std::string& extension_id) { 179 scoped_ptr<Event> event(new Event( 180 events::kOnInputCancelled, make_scoped_ptr(new base::ListValue()))); 181 event->restrict_to_profile = profile; 182 ExtensionSystem::Get(profile)->event_router()-> 183 DispatchEventToExtension(extension_id, event.Pass()); 184} 185 186OmniboxAPI::OmniboxAPI(Profile* profile) 187 : profile_(profile), 188 url_service_(TemplateURLServiceFactory::GetForProfile(profile)) { 189 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, 190 content::Source<Profile>(profile)); 191 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, 192 content::Source<Profile>(profile)); 193 if (url_service_) { 194 registrar_.Add(this, chrome::NOTIFICATION_TEMPLATE_URL_SERVICE_LOADED, 195 content::Source<TemplateURLService>(url_service_)); 196 } 197 198 // Use monochrome icons for Omnibox icons. 199 omnibox_popup_icon_manager_.set_monochrome(true); 200 omnibox_icon_manager_.set_monochrome(true); 201 omnibox_icon_manager_.set_padding(gfx::Insets(0, kOmniboxIconPaddingLeft, 202 0, kOmniboxIconPaddingRight)); 203} 204 205OmniboxAPI::~OmniboxAPI() { 206} 207 208static base::LazyInstance<ProfileKeyedAPIFactory<OmniboxAPI> > 209 g_factory = LAZY_INSTANCE_INITIALIZER; 210 211// static 212ProfileKeyedAPIFactory<OmniboxAPI>* OmniboxAPI::GetFactoryInstance() { 213 return &g_factory.Get(); 214} 215 216// static 217OmniboxAPI* OmniboxAPI::Get(Profile* profile) { 218 return ProfileKeyedAPIFactory<OmniboxAPI>::GetForProfile(profile); 219} 220 221void OmniboxAPI::Observe(int type, 222 const content::NotificationSource& source, 223 const content::NotificationDetails& details) { 224 if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { 225 const Extension* extension = 226 content::Details<const Extension>(details).ptr(); 227 const std::string& keyword = OmniboxInfo::GetKeyword(extension); 228 if (!keyword.empty()) { 229 // Load the omnibox icon so it will be ready to display in the URL bar. 230 omnibox_popup_icon_manager_.LoadIcon(profile_, extension); 231 omnibox_icon_manager_.LoadIcon(profile_, extension); 232 233 if (url_service_) { 234 url_service_->Load(); 235 if (url_service_->loaded()) { 236 url_service_->RegisterExtensionKeyword(extension->id(), 237 extension->name(), 238 keyword); 239 } else { 240 pending_extensions_.insert(extension); 241 } 242 } 243 } 244 } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { 245 const Extension* extension = 246 content::Details<UnloadedExtensionInfo>(details)->extension; 247 if (!OmniboxInfo::GetKeyword(extension).empty()) { 248 if (url_service_) { 249 if (url_service_->loaded()) 250 url_service_->UnregisterExtensionKeyword(extension->id()); 251 else 252 pending_extensions_.erase(extension); 253 } 254 } 255 } else { 256 DCHECK(type == chrome::NOTIFICATION_TEMPLATE_URL_SERVICE_LOADED); 257 // Load pending extensions. 258 for (PendingExtensions::const_iterator i(pending_extensions_.begin()); 259 i != pending_extensions_.end(); ++i) { 260 url_service_->RegisterExtensionKeyword((*i)->id(), 261 (*i)->name(), 262 OmniboxInfo::GetKeyword(*i)); 263 } 264 pending_extensions_.clear(); 265 } 266} 267 268gfx::Image OmniboxAPI::GetOmniboxIcon(const std::string& extension_id) { 269 return gfx::Image::CreateFrom1xBitmap( 270 omnibox_icon_manager_.GetIcon(extension_id)); 271} 272 273gfx::Image OmniboxAPI::GetOmniboxPopupIcon(const std::string& extension_id) { 274 return gfx::Image::CreateFrom1xBitmap( 275 omnibox_popup_icon_manager_.GetIcon(extension_id)); 276} 277 278template <> 279void ProfileKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() { 280 DependsOn(ExtensionSystemFactory::GetInstance()); 281 DependsOn(TemplateURLServiceFactory::GetInstance()); 282} 283 284bool OmniboxSendSuggestionsFunction::RunImpl() { 285 scoped_ptr<SendSuggestions::Params> params( 286 SendSuggestions::Params::Create(*args_)); 287 EXTENSION_FUNCTION_VALIDATE(params); 288 289 content::NotificationService::current()->Notify( 290 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, 291 content::Source<Profile>(profile_->GetOriginalProfile()), 292 content::Details<SendSuggestions::Params>(params.get())); 293 294 return true; 295} 296 297bool OmniboxSetDefaultSuggestionFunction::RunImpl() { 298 scoped_ptr<SetDefaultSuggestion::Params> params( 299 SetDefaultSuggestion::Params::Create(*args_)); 300 EXTENSION_FUNCTION_VALIDATE(params); 301 302 if (SetOmniboxDefaultSuggestion(profile(), 303 extension_id(), 304 params->suggestion)) { 305 content::NotificationService::current()->Notify( 306 chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED, 307 content::Source<Profile>(profile_->GetOriginalProfile()), 308 content::NotificationService::NoDetails()); 309 } 310 311 return true; 312} 313 314// This function converts style information populated by the JSON schema 315// compiler into an ACMatchClassifications object. 316ACMatchClassifications StyleTypesToACMatchClassifications( 317 const omnibox::SuggestResult &suggestion) { 318 ACMatchClassifications match_classifications; 319 if (suggestion.description_styles) { 320 string16 description = UTF8ToUTF16(suggestion.description); 321 std::vector<int> styles(description.length(), 0); 322 323 for (std::vector<linked_ptr<omnibox::SuggestResult::DescriptionStylesType> > 324 ::iterator i = suggestion.description_styles->begin(); 325 i != suggestion.description_styles->end(); ++i) { 326 omnibox::SuggestResult::DescriptionStylesType* style = i->get(); 327 328 int length = description.length(); 329 if (style->length) 330 length = *style->length; 331 332 size_t offset = style->offset >= 0 ? style->offset : 333 std::max(0, static_cast<int>(description.length()) + style->offset); 334 335 int type_class; 336 switch (style->type) { 337 case omnibox::SuggestResult::DescriptionStylesType::TYPE_URL: 338 type_class = AutocompleteMatch::ACMatchClassification::URL; 339 break; 340 case omnibox::SuggestResult::DescriptionStylesType::TYPE_MATCH: 341 type_class = AutocompleteMatch::ACMatchClassification::MATCH; 342 break; 343 case omnibox::SuggestResult::DescriptionStylesType::TYPE_DIM: 344 type_class = AutocompleteMatch::ACMatchClassification::DIM; 345 break; 346 default: 347 type_class = AutocompleteMatch::ACMatchClassification::NONE; 348 return match_classifications; 349 } 350 351 for (size_t j = offset; j < offset + length && j < styles.size(); ++j) 352 styles[j] |= type_class; 353 } 354 355 for (size_t i = 0; i < styles.size(); ++i) { 356 if (i == 0 || styles[i] != styles[i-1]) 357 match_classifications.push_back( 358 ACMatchClassification(i, styles[i])); 359 } 360 } else { 361 match_classifications.push_back( 362 ACMatchClassification(0, ACMatchClassification::NONE)); 363 } 364 365 return match_classifications; 366} 367 368void ApplyDefaultSuggestionForExtensionKeyword( 369 Profile* profile, 370 const TemplateURL* keyword, 371 const string16& remaining_input, 372 AutocompleteMatch* match) { 373 DCHECK(keyword->IsExtensionKeyword()); 374 375 376 scoped_ptr<omnibox::SuggestResult> suggestion( 377 GetOmniboxDefaultSuggestion(profile, keyword->GetExtensionId())); 378 if (!suggestion || suggestion->description.empty()) 379 return; // fall back to the universal default 380 381 const string16 kPlaceholderText(ASCIIToUTF16("%s")); 382 const string16 kReplacementText(ASCIIToUTF16("<input>")); 383 384 string16 description = UTF8ToUTF16(suggestion->description); 385 ACMatchClassifications& description_styles = match->contents_class; 386 description_styles = StyleTypesToACMatchClassifications(*suggestion); 387 388 // Replace "%s" with the user's input and adjust the style offsets to the 389 // new length of the description. 390 size_t placeholder(description.find(kPlaceholderText, 0)); 391 if (placeholder != string16::npos) { 392 string16 replacement = 393 remaining_input.empty() ? kReplacementText : remaining_input; 394 description.replace(placeholder, kPlaceholderText.length(), replacement); 395 396 for (size_t i = 0; i < description_styles.size(); ++i) { 397 if (description_styles[i].offset > placeholder) 398 description_styles[i].offset += replacement.length() - 2; 399 } 400 } 401 402 match->contents.assign(description); 403} 404 405} // namespace extensions 406