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