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/lazy_instance.h"
8 #include "base/strings/string16.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/extensions/tab_helper.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "chrome/browser/search_engines/template_url_service_factory.h"
13 #include "chrome/common/extensions/api/omnibox.h"
14 #include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
15 #include "components/search_engines/template_url.h"
16 #include "components/search_engines/template_url_service.h"
17 #include "content/public/browser/notification_details.h"
18 #include "content/public/browser/notification_service.h"
19 #include "extensions/browser/event_router.h"
20 #include "extensions/browser/extension_prefs.h"
21 #include "extensions/browser/extension_prefs_factory.h"
22 #include "extensions/browser/extension_registry.h"
23 #include "extensions/browser/notification_types.h"
24 #include "ui/gfx/image/image.h"
25
26 namespace extensions {
27
28 namespace omnibox = api::omnibox;
29 namespace SendSuggestions = omnibox::SendSuggestions;
30 namespace SetDefaultSuggestion = omnibox::SetDefaultSuggestion;
31
32 namespace {
33
34 const char kSuggestionContent[] = "content";
35 const char kCurrentTabDisposition[] = "currentTab";
36 const char kForegroundTabDisposition[] = "newForegroundTab";
37 const char kBackgroundTabDisposition[] = "newBackgroundTab";
38
39 // Pref key for omnibox.setDefaultSuggestion.
40 const char kOmniboxDefaultSuggestion[] = "omnibox_default_suggestion";
41
42 #if defined(OS_LINUX)
43 static const int kOmniboxIconPaddingLeft = 2;
44 static const int kOmniboxIconPaddingRight = 2;
45 #elif defined(OS_MACOSX)
46 static const int kOmniboxIconPaddingLeft = 0;
47 static const int kOmniboxIconPaddingRight = 2;
48 #else
49 static const int kOmniboxIconPaddingLeft = 0;
50 static const int kOmniboxIconPaddingRight = 0;
51 #endif
52
GetOmniboxDefaultSuggestion(Profile * profile,const std::string & extension_id)53 scoped_ptr<omnibox::SuggestResult> GetOmniboxDefaultSuggestion(
54 Profile* profile,
55 const std::string& extension_id) {
56 ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
57
58 scoped_ptr<omnibox::SuggestResult> suggestion;
59 const base::DictionaryValue* dict = NULL;
60 if (prefs && prefs->ReadPrefAsDictionary(extension_id,
61 kOmniboxDefaultSuggestion,
62 &dict)) {
63 suggestion.reset(new omnibox::SuggestResult);
64 omnibox::SuggestResult::Populate(*dict, suggestion.get());
65 }
66 return suggestion.Pass();
67 }
68
69 // Tries to set the omnibox default suggestion; returns true on success or
70 // false on failure.
SetOmniboxDefaultSuggestion(Profile * profile,const std::string & extension_id,const omnibox::DefaultSuggestResult & suggestion)71 bool SetOmniboxDefaultSuggestion(
72 Profile* profile,
73 const std::string& extension_id,
74 const omnibox::DefaultSuggestResult& suggestion) {
75 ExtensionPrefs* prefs = ExtensionPrefs::Get(profile);
76 if (!prefs)
77 return false;
78
79 scoped_ptr<base::DictionaryValue> dict = suggestion.ToValue();
80 // Add the content field so that the dictionary can be used to populate an
81 // omnibox::SuggestResult.
82 dict->SetWithoutPathExpansion(kSuggestionContent, new base::StringValue(""));
83 prefs->UpdateExtensionPref(extension_id,
84 kOmniboxDefaultSuggestion,
85 dict.release());
86
87 return true;
88 }
89
90 // Returns a string used as a template URL string of the extension.
GetTemplateURLStringForExtension(const std::string & extension_id)91 std::string GetTemplateURLStringForExtension(const std::string& extension_id) {
92 // This URL is not actually used for navigation. It holds the extension's ID.
93 return std::string(extensions::kExtensionScheme) + "://" +
94 extension_id + "/?q={searchTerms}";
95 }
96
97 } // namespace
98
99 // static
OnInputStarted(Profile * profile,const std::string & extension_id)100 void ExtensionOmniboxEventRouter::OnInputStarted(
101 Profile* profile, const std::string& extension_id) {
102 scoped_ptr<Event> event(new Event(
103 omnibox::OnInputStarted::kEventName,
104 make_scoped_ptr(new base::ListValue())));
105 event->restrict_to_browser_context = profile;
106 EventRouter::Get(profile)
107 ->DispatchEventToExtension(extension_id, event.Pass());
108 }
109
110 // static
OnInputChanged(Profile * profile,const std::string & extension_id,const std::string & input,int suggest_id)111 bool ExtensionOmniboxEventRouter::OnInputChanged(
112 Profile* profile, const std::string& extension_id,
113 const std::string& input, int suggest_id) {
114 EventRouter* event_router = EventRouter::Get(profile);
115 if (!event_router->ExtensionHasEventListener(
116 extension_id, omnibox::OnInputChanged::kEventName))
117 return false;
118
119 scoped_ptr<base::ListValue> args(new base::ListValue());
120 args->Set(0, new base::StringValue(input));
121 args->Set(1, new base::FundamentalValue(suggest_id));
122
123 scoped_ptr<Event> event(new Event(omnibox::OnInputChanged::kEventName,
124 args.Pass()));
125 event->restrict_to_browser_context = profile;
126 event_router->DispatchEventToExtension(extension_id, event.Pass());
127 return true;
128 }
129
130 // static
OnInputEntered(content::WebContents * web_contents,const std::string & extension_id,const std::string & input,WindowOpenDisposition disposition)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 ExtensionRegistry::Get(profile)->enabled_extensions().GetByID(
141 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 EventRouter::Get(profile)
159 ->DispatchEventToExtension(extension_id, event.Pass());
160
161 content::NotificationService::current()->Notify(
162 extensions::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
163 content::Source<Profile>(profile),
164 content::NotificationService::NoDetails());
165 }
166
167 // static
OnInputCancelled(Profile * profile,const std::string & extension_id)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 EventRouter::Get(profile)
175 ->DispatchEventToExtension(extension_id, event.Pass());
176 }
177
OmniboxAPI(content::BrowserContext * context)178 OmniboxAPI::OmniboxAPI(content::BrowserContext* context)
179 : profile_(Profile::FromBrowserContext(context)),
180 url_service_(TemplateURLServiceFactory::GetForProfile(profile_)),
181 extension_registry_observer_(this) {
182 extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
183 if (url_service_) {
184 template_url_sub_ = url_service_->RegisterOnLoadedCallback(
185 base::Bind(&OmniboxAPI::OnTemplateURLsLoaded,
186 base::Unretained(this)));
187 }
188
189 // Use monochrome icons for Omnibox icons.
190 omnibox_popup_icon_manager_.set_monochrome(true);
191 omnibox_icon_manager_.set_monochrome(true);
192 omnibox_icon_manager_.set_padding(gfx::Insets(0, kOmniboxIconPaddingLeft,
193 0, kOmniboxIconPaddingRight));
194 }
195
Shutdown()196 void OmniboxAPI::Shutdown() {
197 template_url_sub_.reset();
198 }
199
~OmniboxAPI()200 OmniboxAPI::~OmniboxAPI() {
201 }
202
203 static base::LazyInstance<BrowserContextKeyedAPIFactory<OmniboxAPI> >
204 g_factory = LAZY_INSTANCE_INITIALIZER;
205
206 // static
GetFactoryInstance()207 BrowserContextKeyedAPIFactory<OmniboxAPI>* OmniboxAPI::GetFactoryInstance() {
208 return g_factory.Pointer();
209 }
210
211 // static
Get(content::BrowserContext * context)212 OmniboxAPI* OmniboxAPI::Get(content::BrowserContext* context) {
213 return BrowserContextKeyedAPIFactory<OmniboxAPI>::Get(context);
214 }
215
OnExtensionLoaded(content::BrowserContext * browser_context,const Extension * extension)216 void OmniboxAPI::OnExtensionLoaded(content::BrowserContext* browser_context,
217 const Extension* extension) {
218 const std::string& keyword = OmniboxInfo::GetKeyword(extension);
219 if (!keyword.empty()) {
220 // Load the omnibox icon so it will be ready to display in the URL bar.
221 omnibox_popup_icon_manager_.LoadIcon(profile_, extension);
222 omnibox_icon_manager_.LoadIcon(profile_, extension);
223
224 if (url_service_) {
225 url_service_->Load();
226 if (url_service_->loaded()) {
227 url_service_->RegisterOmniboxKeyword(
228 extension->id(), extension->name(), keyword,
229 GetTemplateURLStringForExtension(extension->id()));
230 } else {
231 pending_extensions_.insert(extension);
232 }
233 }
234 }
235 }
236
OnExtensionUnloaded(content::BrowserContext * browser_context,const Extension * extension,UnloadedExtensionInfo::Reason reason)237 void OmniboxAPI::OnExtensionUnloaded(content::BrowserContext* browser_context,
238 const Extension* extension,
239 UnloadedExtensionInfo::Reason reason) {
240 if (!OmniboxInfo::GetKeyword(extension).empty() && url_service_) {
241 if (url_service_->loaded()) {
242 url_service_->RemoveExtensionControlledTURL(
243 extension->id(), TemplateURL::OMNIBOX_API_EXTENSION);
244 } else {
245 pending_extensions_.erase(extension);
246 }
247 }
248 }
249
GetOmniboxIcon(const std::string & extension_id)250 gfx::Image OmniboxAPI::GetOmniboxIcon(const std::string& extension_id) {
251 return gfx::Image::CreateFrom1xBitmap(
252 omnibox_icon_manager_.GetIcon(extension_id));
253 }
254
GetOmniboxPopupIcon(const std::string & extension_id)255 gfx::Image OmniboxAPI::GetOmniboxPopupIcon(const std::string& extension_id) {
256 return gfx::Image::CreateFrom1xBitmap(
257 omnibox_popup_icon_manager_.GetIcon(extension_id));
258 }
259
OnTemplateURLsLoaded()260 void OmniboxAPI::OnTemplateURLsLoaded() {
261 // Register keywords for pending extensions.
262 template_url_sub_.reset();
263 for (PendingExtensions::const_iterator i(pending_extensions_.begin());
264 i != pending_extensions_.end(); ++i) {
265 url_service_->RegisterOmniboxKeyword(
266 (*i)->id(), (*i)->name(), OmniboxInfo::GetKeyword(*i),
267 GetTemplateURLStringForExtension((*i)->id()));
268 }
269 pending_extensions_.clear();
270 }
271
272 template <>
DeclareFactoryDependencies()273 void BrowserContextKeyedAPIFactory<OmniboxAPI>::DeclareFactoryDependencies() {
274 DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
275 DependsOn(ExtensionPrefsFactory::GetInstance());
276 DependsOn(TemplateURLServiceFactory::GetInstance());
277 }
278
RunSync()279 bool OmniboxSendSuggestionsFunction::RunSync() {
280 scoped_ptr<SendSuggestions::Params> params(
281 SendSuggestions::Params::Create(*args_));
282 EXTENSION_FUNCTION_VALIDATE(params);
283
284 content::NotificationService::current()->Notify(
285 extensions::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
286 content::Source<Profile>(GetProfile()->GetOriginalProfile()),
287 content::Details<SendSuggestions::Params>(params.get()));
288
289 return true;
290 }
291
RunSync()292 bool OmniboxSetDefaultSuggestionFunction::RunSync() {
293 scoped_ptr<SetDefaultSuggestion::Params> params(
294 SetDefaultSuggestion::Params::Create(*args_));
295 EXTENSION_FUNCTION_VALIDATE(params);
296
297 if (SetOmniboxDefaultSuggestion(
298 GetProfile(), extension_id(), params->suggestion)) {
299 content::NotificationService::current()->Notify(
300 extensions::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
301 content::Source<Profile>(GetProfile()->GetOriginalProfile()),
302 content::NotificationService::NoDetails());
303 }
304
305 return true;
306 }
307
308 // This function converts style information populated by the JSON schema
309 // compiler into an ACMatchClassifications object.
StyleTypesToACMatchClassifications(const omnibox::SuggestResult & suggestion)310 ACMatchClassifications StyleTypesToACMatchClassifications(
311 const omnibox::SuggestResult &suggestion) {
312 ACMatchClassifications match_classifications;
313 if (suggestion.description_styles) {
314 base::string16 description = base::UTF8ToUTF16(suggestion.description);
315 std::vector<int> styles(description.length(), 0);
316
317 for (std::vector<linked_ptr<omnibox::SuggestResult::DescriptionStylesType> >
318 ::iterator i = suggestion.description_styles->begin();
319 i != suggestion.description_styles->end(); ++i) {
320 omnibox::SuggestResult::DescriptionStylesType* style = i->get();
321
322 int length = description.length();
323 if (style->length)
324 length = *style->length;
325
326 size_t offset = style->offset >= 0 ? style->offset :
327 std::max(0, static_cast<int>(description.length()) + style->offset);
328
329 int type_class;
330 switch (style->type) {
331 case omnibox::SuggestResult::DescriptionStylesType::TYPE_URL:
332 type_class = AutocompleteMatch::ACMatchClassification::URL;
333 break;
334 case omnibox::SuggestResult::DescriptionStylesType::TYPE_MATCH:
335 type_class = AutocompleteMatch::ACMatchClassification::MATCH;
336 break;
337 case omnibox::SuggestResult::DescriptionStylesType::TYPE_DIM:
338 type_class = AutocompleteMatch::ACMatchClassification::DIM;
339 break;
340 default:
341 type_class = AutocompleteMatch::ACMatchClassification::NONE;
342 return match_classifications;
343 }
344
345 for (size_t j = offset; j < offset + length && j < styles.size(); ++j)
346 styles[j] |= type_class;
347 }
348
349 for (size_t i = 0; i < styles.size(); ++i) {
350 if (i == 0 || styles[i] != styles[i-1])
351 match_classifications.push_back(
352 ACMatchClassification(i, styles[i]));
353 }
354 } else {
355 match_classifications.push_back(
356 ACMatchClassification(0, ACMatchClassification::NONE));
357 }
358
359 return match_classifications;
360 }
361
ApplyDefaultSuggestionForExtensionKeyword(Profile * profile,const TemplateURL * keyword,const base::string16 & remaining_input,AutocompleteMatch * match)362 void ApplyDefaultSuggestionForExtensionKeyword(
363 Profile* profile,
364 const TemplateURL* keyword,
365 const base::string16& remaining_input,
366 AutocompleteMatch* match) {
367 DCHECK(keyword->GetType() == TemplateURL::OMNIBOX_API_EXTENSION);
368
369 scoped_ptr<omnibox::SuggestResult> suggestion(
370 GetOmniboxDefaultSuggestion(profile, keyword->GetExtensionId()));
371 if (!suggestion || suggestion->description.empty())
372 return; // fall back to the universal default
373
374 const base::string16 kPlaceholderText(base::ASCIIToUTF16("%s"));
375 const base::string16 kReplacementText(base::ASCIIToUTF16("<input>"));
376
377 base::string16 description = base::UTF8ToUTF16(suggestion->description);
378 ACMatchClassifications& description_styles = match->contents_class;
379 description_styles = StyleTypesToACMatchClassifications(*suggestion);
380
381 // Replace "%s" with the user's input and adjust the style offsets to the
382 // new length of the description.
383 size_t placeholder(description.find(kPlaceholderText, 0));
384 if (placeholder != base::string16::npos) {
385 base::string16 replacement =
386 remaining_input.empty() ? kReplacementText : remaining_input;
387 description.replace(placeholder, kPlaceholderText.length(), replacement);
388
389 for (size_t i = 0; i < description_styles.size(); ++i) {
390 if (description_styles[i].offset > placeholder)
391 description_styles[i].offset += replacement.length() - 2;
392 }
393 }
394
395 match->contents.assign(description);
396 }
397
398 } // namespace extensions
399