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/extension_prefs.h" 15 #include "chrome/browser/extensions/extension_service.h" 16 #include "chrome/browser/extensions/extension_system.h" 17 #include "chrome/browser/extensions/tab_helper.h" 18 #include "chrome/browser/profiles/profile.h" 19 #include "chrome/browser/search_engines/template_url.h" 20 #include "chrome/browser/search_engines/template_url_service.h" 21 #include "chrome/browser/search_engines/template_url_service_factory.h" 22 #include "chrome/common/extensions/api/omnibox.h" 23 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h" 24 #include "content/public/browser/notification_details.h" 25 #include "content/public/browser/notification_service.h" 26 #include "extensions/browser/event_router.h" 27 #include "extensions/common/extension.h" 28 #include "ui/gfx/image/image.h" 29 30 namespace extensions { 31 32 namespace omnibox = api::omnibox; 33 namespace SendSuggestions = omnibox::SendSuggestions; 34 namespace SetDefaultSuggestion = omnibox::SetDefaultSuggestion; 35 36 namespace { 37 38 const char kSuggestionContent[] = "content"; 39 const char kCurrentTabDisposition[] = "currentTab"; 40 const char kForegroundTabDisposition[] = "newForegroundTab"; 41 const char kBackgroundTabDisposition[] = "newBackgroundTab"; 42 43 // Pref key for omnibox.setDefaultSuggestion. 44 const char kOmniboxDefaultSuggestion[] = "omnibox_default_suggestion"; 45 46 #if defined(OS_LINUX) 47 static const int kOmniboxIconPaddingLeft = 2; 48 static const int kOmniboxIconPaddingRight = 2; 49 #elif defined(OS_MACOSX) 50 static const int kOmniboxIconPaddingLeft = 0; 51 static const int kOmniboxIconPaddingRight = 2; 52 #else 53 static const int kOmniboxIconPaddingLeft = 0; 54 static const int kOmniboxIconPaddingRight = 0; 55 #endif 56 57 scoped_ptr<omnibox::SuggestResult> GetOmniboxDefaultSuggestion( 58 Profile* profile, 59 const std::string& extension_id) { 60 ExtensionPrefs* prefs = 61 ExtensionSystem::Get(profile)->extension_service()->extension_prefs(); 62 63 scoped_ptr<omnibox::SuggestResult> suggestion; 64 const base::DictionaryValue* dict = NULL; 65 if (prefs && prefs->ReadPrefAsDictionary(extension_id, 66 kOmniboxDefaultSuggestion, 67 &dict)) { 68 suggestion.reset(new omnibox::SuggestResult); 69 omnibox::SuggestResult::Populate(*dict, suggestion.get()); 70 } 71 return suggestion.Pass(); 72 } 73 74 // Tries to set the omnibox default suggestion; returns true on success or 75 // false on failure. 76 bool SetOmniboxDefaultSuggestion( 77 Profile* profile, 78 const std::string& extension_id, 79 const omnibox::DefaultSuggestResult& suggestion) { 80 ExtensionPrefs* prefs = 81 ExtensionSystem::Get(profile)->extension_service()->extension_prefs(); 82 if (!prefs) 83 return false; 84 85 scoped_ptr<base::DictionaryValue> dict = suggestion.ToValue(); 86 // Add the content field so that the dictionary can be used to populate an 87 // omnibox::SuggestResult. 88 dict->SetWithoutPathExpansion(kSuggestionContent, new base::StringValue("")); 89 prefs->UpdateExtensionPref(extension_id, 90 kOmniboxDefaultSuggestion, 91 dict.release()); 92 93 return true; 94 } 95 96 } // namespace 97 98 // static 99 void ExtensionOmniboxEventRouter::OnInputStarted( 100 Profile* profile, const std::string& extension_id) { 101 scoped_ptr<Event> event(new Event( 102 omnibox::OnInputStarted::kEventName, 103 make_scoped_ptr(new base::ListValue()))); 104 event->restrict_to_browser_context = profile; 105 ExtensionSystem::Get(profile)->event_router()-> 106 DispatchEventToExtension(extension_id, event.Pass()); 107 } 108 109 // static 110 bool ExtensionOmniboxEventRouter::OnInputChanged( 111 Profile* profile, const std::string& extension_id, 112 const std::string& input, int suggest_id) { 113 if (!extensions::ExtensionSystem::Get(profile)->event_router()-> 114 ExtensionHasEventListener(extension_id, 115 omnibox::OnInputChanged::kEventName)) 116 return false; 117 118 scoped_ptr<base::ListValue> args(new base::ListValue()); 119 args->Set(0, new base::StringValue(input)); 120 args->Set(1, new base::FundamentalValue(suggest_id)); 121 122 scoped_ptr<Event> event(new Event(omnibox::OnInputChanged::kEventName, 123 args.Pass())); 124 event->restrict_to_browser_context = profile; 125 ExtensionSystem::Get(profile)->event_router()-> 126 DispatchEventToExtension(extension_id, event.Pass()); 127 return true; 128 } 129 130 // static 131 void ExtensionOmniboxEventRouter::OnInputEntered( 132 content::WebContents* web_contents, 133 const std::string& extension_id, 134 const std::string& input, 135 WindowOpenDisposition disposition) { 136 Profile* profile = 137 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 138 139 const Extension* extension = 140 ExtensionSystem::Get(profile)->extension_service()->extensions()-> 141 GetByID(extension_id); 142 CHECK(extension); 143 extensions::TabHelper::FromWebContents(web_contents)-> 144 active_tab_permission_granter()->GrantIfRequested(extension); 145 146 scoped_ptr<base::ListValue> args(new base::ListValue()); 147 args->Set(0, new base::StringValue(input)); 148 if (disposition == NEW_FOREGROUND_TAB) 149 args->Set(1, new base::StringValue(kForegroundTabDisposition)); 150 else if (disposition == NEW_BACKGROUND_TAB) 151 args->Set(1, new base::StringValue(kBackgroundTabDisposition)); 152 else 153 args->Set(1, new base::StringValue(kCurrentTabDisposition)); 154 155 scoped_ptr<Event> event(new Event(omnibox::OnInputEntered::kEventName, 156 args.Pass())); 157 event->restrict_to_browser_context = profile; 158 ExtensionSystem::Get(profile)->event_router()-> 159 DispatchEventToExtension(extension_id, event.Pass()); 160 161 content::NotificationService::current()->Notify( 162 chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED, 163 content::Source<Profile>(profile), 164 content::NotificationService::NoDetails()); 165 } 166 167 // static 168 void ExtensionOmniboxEventRouter::OnInputCancelled( 169 Profile* profile, const std::string& extension_id) { 170 scoped_ptr<Event> event(new Event( 171 omnibox::OnInputCancelled::kEventName, 172 make_scoped_ptr(new base::ListValue()))); 173 event->restrict_to_browser_context = profile; 174 ExtensionSystem::Get(profile)->event_router()-> 175 DispatchEventToExtension(extension_id, event.Pass()); 176 } 177 178 OmniboxAPI::OmniboxAPI(Profile* profile) 179 : profile_(profile), 180 url_service_(TemplateURLServiceFactory::GetForProfile(profile)) { 181 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, 182 content::Source<Profile>(profile)); 183 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED, 184 content::Source<Profile>(profile)); 185 if (url_service_) { 186 template_url_sub_ = url_service_->RegisterOnLoadedCallback( 187 base::Bind(&OmniboxAPI::OnTemplateURLsLoaded, 188 base::Unretained(this))); 189 } 190 191 // Use monochrome icons for Omnibox icons. 192 omnibox_popup_icon_manager_.set_monochrome(true); 193 omnibox_icon_manager_.set_monochrome(true); 194 omnibox_icon_manager_.set_padding(gfx::Insets(0, kOmniboxIconPaddingLeft, 195 0, kOmniboxIconPaddingRight)); 196 } 197 198 void OmniboxAPI::Shutdown() { 199 template_url_sub_.reset(); 200 } 201 202 OmniboxAPI::~OmniboxAPI() { 203 } 204 205 static base::LazyInstance<ProfileKeyedAPIFactory<OmniboxAPI> > 206 g_factory = LAZY_INSTANCE_INITIALIZER; 207 208 // static 209 ProfileKeyedAPIFactory<OmniboxAPI>* OmniboxAPI::GetFactoryInstance() { 210 return &g_factory.Get(); 211 } 212 213 // static 214 OmniboxAPI* OmniboxAPI::Get(Profile* profile) { 215 return ProfileKeyedAPIFactory<OmniboxAPI>::GetForProfile(profile); 216 } 217 218 void OmniboxAPI::Observe(int type, 219 const content::NotificationSource& source, 220 const content::NotificationDetails& details) { 221 if (type == chrome::NOTIFICATION_EXTENSION_LOADED) { 222 const Extension* extension = 223 content::Details<const Extension>(details).ptr(); 224 const std::string& keyword = OmniboxInfo::GetKeyword(extension); 225 if (!keyword.empty()) { 226 // Load the omnibox icon so it will be ready to display in the URL bar. 227 omnibox_popup_icon_manager_.LoadIcon(profile_, extension); 228 omnibox_icon_manager_.LoadIcon(profile_, extension); 229 230 if (url_service_) { 231 url_service_->Load(); 232 if (url_service_->loaded()) { 233 url_service_->RegisterOmniboxKeyword(extension->id(), 234 extension->name(), 235 keyword); 236 } else { 237 pending_extensions_.insert(extension); 238 } 239 } 240 } 241 } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) { 242 const Extension* extension = 243 content::Details<UnloadedExtensionInfo>(details)->extension; 244 if (!OmniboxInfo::GetKeyword(extension).empty()) { 245 if (url_service_) { 246 if (url_service_->loaded()) 247 url_service_->UnregisterOmniboxKeyword(extension->id()); 248 else 249 pending_extensions_.erase(extension); 250 } 251 } 252 } else { 253 NOTREACHED(); 254 } 255 } 256 257 gfx::Image OmniboxAPI::GetOmniboxIcon(const std::string& extension_id) { 258 return gfx::Image::CreateFrom1xBitmap( 259 omnibox_icon_manager_.GetIcon(extension_id)); 260 } 261 262 gfx::Image OmniboxAPI::GetOmniboxPopupIcon(const std::string& extension_id) { 263 return gfx::Image::CreateFrom1xBitmap( 264 omnibox_popup_icon_manager_.GetIcon(extension_id)); 265 } 266 267 void OmniboxAPI::OnTemplateURLsLoaded() { 268 // Register keywords for pending extensions. 269 template_url_sub_.reset(); 270 for (PendingExtensions::const_iterator i(pending_extensions_.begin()); 271 i != pending_extensions_.end(); ++i) { 272 url_service_->RegisterOmniboxKeyword((*i)->id(), 273 (*i)->name(), 274 OmniboxInfo::GetKeyword(*i)); 275 } 276 pending_extensions_.clear(); 277 } 278 279 template <> 280 void ProfileKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() { 281 DependsOn(ExtensionSystemFactory::GetInstance()); 282 DependsOn(TemplateURLServiceFactory::GetInstance()); 283 } 284 285 bool OmniboxSendSuggestionsFunction::RunImpl() { 286 scoped_ptr<SendSuggestions::Params> params( 287 SendSuggestions::Params::Create(*args_)); 288 EXTENSION_FUNCTION_VALIDATE(params); 289 290 content::NotificationService::current()->Notify( 291 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, 292 content::Source<Profile>(GetProfile()->GetOriginalProfile()), 293 content::Details<SendSuggestions::Params>(params.get())); 294 295 return true; 296 } 297 298 bool OmniboxSetDefaultSuggestionFunction::RunImpl() { 299 scoped_ptr<SetDefaultSuggestion::Params> params( 300 SetDefaultSuggestion::Params::Create(*args_)); 301 EXTENSION_FUNCTION_VALIDATE(params); 302 303 if (SetOmniboxDefaultSuggestion( 304 GetProfile(), extension_id(), params->suggestion)) { 305 content::NotificationService::current()->Notify( 306 chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED, 307 content::Source<Profile>(GetProfile()->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. 316 ACMatchClassifications StyleTypesToACMatchClassifications( 317 const omnibox::SuggestResult &suggestion) { 318 ACMatchClassifications match_classifications; 319 if (suggestion.description_styles) { 320 base::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 368 void ApplyDefaultSuggestionForExtensionKeyword( 369 Profile* profile, 370 const TemplateURL* keyword, 371 const base::string16& remaining_input, 372 AutocompleteMatch* match) { 373 DCHECK(keyword->GetType() == TemplateURL::OMNIBOX_API_EXTENSION); 374 375 scoped_ptr<omnibox::SuggestResult> suggestion( 376 GetOmniboxDefaultSuggestion(profile, keyword->GetExtensionId())); 377 if (!suggestion || suggestion->description.empty()) 378 return; // fall back to the universal default 379 380 const base::string16 kPlaceholderText(ASCIIToUTF16("%s")); 381 const base::string16 kReplacementText(ASCIIToUTF16("<input>")); 382 383 base::string16 description = UTF8ToUTF16(suggestion->description); 384 ACMatchClassifications& description_styles = match->contents_class; 385 description_styles = StyleTypesToACMatchClassifications(*suggestion); 386 387 // Replace "%s" with the user's input and adjust the style offsets to the 388 // new length of the description. 389 size_t placeholder(description.find(kPlaceholderText, 0)); 390 if (placeholder != base::string16::npos) { 391 base::string16 replacement = 392 remaining_input.empty() ? kReplacementText : remaining_input; 393 description.replace(placeholder, kPlaceholderText.length(), replacement); 394 395 for (size_t i = 0; i < description_styles.size(); ++i) { 396 if (description_styles[i].offset > placeholder) 397 description_styles[i].offset += replacement.length() - 2; 398 } 399 } 400 401 match->contents.assign(description); 402 } 403 404 } // namespace extensions 405