Home | History | Annotate | Download | only in autocomplete
      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/autocomplete/keyword_provider.h"
      6 
      7 #include <algorithm>
      8 #include <vector>
      9 
     10 #include "base/strings/string16.h"
     11 #include "base/strings/string_util.h"
     12 #include "base/strings/utf_string_conversions.h"
     13 #include "chrome/browser/autocomplete/autocomplete_match.h"
     14 #include "chrome/browser/autocomplete/autocomplete_provider_listener.h"
     15 #include "chrome/browser/autocomplete/keyword_extensions_delegate.h"
     16 #include "chrome/browser/chrome_notification_types.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/browser/search_engines/template_url.h"
     19 #include "chrome/browser/search_engines/template_url_service.h"
     20 #include "chrome/browser/search_engines/template_url_service_factory.h"
     21 #include "components/metrics/proto/omnibox_input_type.pb.h"
     22 #include "content/public/browser/notification_details.h"
     23 #include "content/public/browser/notification_source.h"
     24 #include "extensions/browser/extension_system.h"
     25 #include "grit/generated_resources.h"
     26 #include "net/base/escape.h"
     27 #include "net/base/net_util.h"
     28 #include "ui/base/l10n/l10n_util.h"
     29 
     30 #if defined(ENABLE_EXTENSIONS)
     31 #include "chrome/browser/autocomplete/keyword_extensions_delegate_impl.h"
     32 #endif
     33 
     34 namespace {
     35 
     36 // Helper functor for Start(), for sorting keyword matches by quality.
     37 class CompareQuality {
     38  public:
     39   // A keyword is of higher quality when a greater fraction of it has been
     40   // typed, that is, when it is shorter.
     41   //
     42   // TODO(pkasting): Most recent and most frequent keywords are probably
     43   // better rankings than the fraction of the keyword typed.  We should
     44   // always put any exact matches first no matter what, since the code in
     45   // Start() assumes this (and it makes sense).
     46   bool operator()(const TemplateURL* t_url1, const TemplateURL* t_url2) const {
     47     return t_url1->keyword().length() < t_url2->keyword().length();
     48   }
     49 };
     50 
     51 // Helper for KeywordProvider::Start(), for ending keyword mode unless
     52 // explicitly told otherwise.
     53 class ScopedEndExtensionKeywordMode {
     54  public:
     55   explicit ScopedEndExtensionKeywordMode(KeywordExtensionsDelegate* delegate);
     56   ~ScopedEndExtensionKeywordMode();
     57 
     58   void StayInKeywordMode();
     59 
     60  private:
     61   KeywordExtensionsDelegate* delegate_;
     62 
     63   DISALLOW_COPY_AND_ASSIGN(ScopedEndExtensionKeywordMode);
     64 };
     65 
     66 ScopedEndExtensionKeywordMode::ScopedEndExtensionKeywordMode(
     67     KeywordExtensionsDelegate* delegate)
     68     : delegate_(delegate) {
     69 }
     70 
     71 ScopedEndExtensionKeywordMode::~ScopedEndExtensionKeywordMode() {
     72   if (delegate_)
     73     delegate_->MaybeEndExtensionKeywordMode();
     74 }
     75 
     76 void ScopedEndExtensionKeywordMode::StayInKeywordMode() {
     77   delegate_ = NULL;
     78 }
     79 
     80 }  // namespace
     81 
     82 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener,
     83                                  Profile* profile)
     84     : AutocompleteProvider(listener, profile,
     85                            AutocompleteProvider::TYPE_KEYWORD),
     86       model_(NULL) {
     87 #if defined(ENABLE_EXTENSIONS)
     88   extensions_delegate_.reset(new KeywordExtensionsDelegateImpl(this));
     89 #endif
     90 }
     91 
     92 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener,
     93                                  TemplateURLService* model)
     94     : AutocompleteProvider(listener, NULL, AutocompleteProvider::TYPE_KEYWORD),
     95       model_(model) {
     96 }
     97 
     98 // static
     99 base::string16 KeywordProvider::SplitKeywordFromInput(
    100     const base::string16& input,
    101     bool trim_leading_whitespace,
    102     base::string16* remaining_input) {
    103   // Find end of first token.  The AutocompleteController has trimmed leading
    104   // whitespace, so we need not skip over that.
    105   const size_t first_white(input.find_first_of(base::kWhitespaceUTF16));
    106   DCHECK_NE(0U, first_white);
    107   if (first_white == base::string16::npos)
    108     return input;  // Only one token provided.
    109 
    110   // Set |remaining_input| to everything after the first token.
    111   DCHECK(remaining_input != NULL);
    112   const size_t remaining_start = trim_leading_whitespace ?
    113       input.find_first_not_of(base::kWhitespaceUTF16, first_white) :
    114       first_white + 1;
    115 
    116   if (remaining_start < input.length())
    117     remaining_input->assign(input.begin() + remaining_start, input.end());
    118 
    119   // Return first token as keyword.
    120   return input.substr(0, first_white);
    121 }
    122 
    123 // static
    124 base::string16 KeywordProvider::SplitReplacementStringFromInput(
    125     const base::string16& input,
    126     bool trim_leading_whitespace) {
    127   // The input may contain leading whitespace, strip it.
    128   base::string16 trimmed_input;
    129   base::TrimWhitespace(input, base::TRIM_LEADING, &trimmed_input);
    130 
    131   // And extract the replacement string.
    132   base::string16 remaining_input;
    133   SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
    134       &remaining_input);
    135   return remaining_input;
    136 }
    137 
    138 // static
    139 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput(
    140     TemplateURLService* model,
    141     AutocompleteInput* input) {
    142   if (!input->allow_exact_keyword_match())
    143     return NULL;
    144 
    145   base::string16 keyword, remaining_input;
    146   if (!ExtractKeywordFromInput(*input, &keyword, &remaining_input))
    147     return NULL;
    148 
    149   DCHECK(model);
    150   const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
    151   if (template_url &&
    152       template_url->SupportsReplacement(model->search_terms_data())) {
    153     // Adjust cursor position iff it was set before, otherwise leave it as is.
    154     size_t cursor_position = base::string16::npos;
    155     // The adjustment assumes that the keyword was stripped from the beginning
    156     // of the original input.
    157     if (input->cursor_position() != base::string16::npos &&
    158         !remaining_input.empty() &&
    159         EndsWith(input->text(), remaining_input, true)) {
    160       int offset = input->text().length() - input->cursor_position();
    161       // The cursor should never be past the last character or before the
    162       // first character.
    163       DCHECK_GE(offset, 0);
    164       DCHECK_LE(offset, static_cast<int>(input->text().length()));
    165       if (offset <= 0) {
    166         // Normalize the cursor to be exactly after the last character.
    167         cursor_position = remaining_input.length();
    168       } else {
    169         // If somehow the cursor was before the remaining text, set it to 0,
    170         // otherwise adjust it relative to the remaining text.
    171         cursor_position = offset > static_cast<int>(remaining_input.length()) ?
    172             0u : remaining_input.length() - offset;
    173       }
    174     }
    175     input->UpdateText(remaining_input, cursor_position, input->parts());
    176     return template_url;
    177   }
    178 
    179   return NULL;
    180 }
    181 
    182 base::string16 KeywordProvider::GetKeywordForText(
    183     const base::string16& text) const {
    184   const base::string16 keyword(TemplateURLService::CleanUserInputKeyword(text));
    185 
    186   if (keyword.empty())
    187     return keyword;
    188 
    189   TemplateURLService* url_service = GetTemplateURLService();
    190   if (!url_service)
    191     return base::string16();
    192 
    193   // Don't provide a keyword if it doesn't support replacement.
    194   const TemplateURL* const template_url =
    195       url_service->GetTemplateURLForKeyword(keyword);
    196   if (!template_url ||
    197       !template_url->SupportsReplacement(url_service->search_terms_data()))
    198     return base::string16();
    199 
    200   // Don't provide a keyword for inactive/disabled extension keywords.
    201   if ((template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION) &&
    202       extensions_delegate_ &&
    203       !extensions_delegate_->IsEnabledExtension(
    204           profile_, template_url->GetExtensionId()))
    205     return base::string16();
    206 
    207   return keyword;
    208 }
    209 
    210 AutocompleteMatch KeywordProvider::CreateVerbatimMatch(
    211     const base::string16& text,
    212     const base::string16& keyword,
    213     const AutocompleteInput& input) {
    214   // A verbatim match is allowed to be the default match.
    215   return CreateAutocompleteMatch(
    216       GetTemplateURLService()->GetTemplateURLForKeyword(keyword), input,
    217       keyword.length(), SplitReplacementStringFromInput(text, true), true, 0);
    218 }
    219 
    220 void KeywordProvider::Start(const AutocompleteInput& input,
    221                             bool minimal_changes) {
    222   // This object ensures we end keyword mode if we exit the function without
    223   // toggling keyword mode to on.
    224   ScopedEndExtensionKeywordMode keyword_mode_toggle(extensions_delegate_.get());
    225 
    226   matches_.clear();
    227 
    228   if (!minimal_changes) {
    229     done_ = true;
    230 
    231     // Input has changed. Increment the input ID so that we can discard any
    232     // stale extension suggestions that may be incoming.
    233     if (extensions_delegate_)
    234       extensions_delegate_->IncrementInputId();
    235   }
    236 
    237   // Split user input into a keyword and some query input.
    238   //
    239   // We want to suggest keywords even when users have started typing URLs, on
    240   // the assumption that they might not realize they no longer need to go to a
    241   // site to be able to search it.  So we call CleanUserInputKeyword() to strip
    242   // any initial scheme and/or "www.".  NOTE: Any heuristics or UI used to
    243   // automatically/manually create keywords will need to be in sync with
    244   // whatever we do here!
    245   //
    246   // TODO(pkasting): http://crbug/347744 If someday we remember usage frequency
    247   // for keywords, we might suggest keywords that haven't even been partially
    248   // typed, if the user uses them enough and isn't obviously typing something
    249   // else.  In this case we'd consider all input here to be query input.
    250   base::string16 keyword, remaining_input;
    251   if (!ExtractKeywordFromInput(input, &keyword, &remaining_input))
    252     return;
    253 
    254   // Get the best matches for this keyword.
    255   //
    256   // NOTE: We could cache the previous keywords and reuse them here in the
    257   // |minimal_changes| case, but since we'd still have to recalculate their
    258   // relevances and we can just recreate the results synchronously anyway, we
    259   // don't bother.
    260   TemplateURLService::TemplateURLVector matches;
    261   GetTemplateURLService()->FindMatchingKeywords(
    262       keyword, !remaining_input.empty(), &matches);
    263 
    264   for (TemplateURLService::TemplateURLVector::iterator i(matches.begin());
    265        i != matches.end(); ) {
    266     const TemplateURL* template_url = *i;
    267 
    268     // Prune any extension keywords that are disallowed in incognito mode (if
    269     // we're incognito), or disabled.
    270     if (profile_ &&
    271         (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION) &&
    272         extensions_delegate_ &&
    273         !extensions_delegate_->IsEnabledExtension(
    274             profile_, template_url->GetExtensionId())) {
    275       i = matches.erase(i);
    276       continue;
    277     }
    278 
    279     // Prune any substituting keywords if there is no substitution.
    280     if (template_url->SupportsReplacement(
    281             GetTemplateURLService()->search_terms_data()) &&
    282         remaining_input.empty() &&
    283         !input.allow_exact_keyword_match()) {
    284       i = matches.erase(i);
    285       continue;
    286     }
    287 
    288     ++i;
    289   }
    290   if (matches.empty())
    291     return;
    292   std::sort(matches.begin(), matches.end(), CompareQuality());
    293 
    294   // Limit to one exact or three inexact matches, and mark them up for display
    295   // in the autocomplete popup.
    296   // Any exact match is going to be the highest quality match, and thus at the
    297   // front of our vector.
    298   if (matches.front()->keyword() == keyword) {
    299     const TemplateURL* template_url = matches.front();
    300     const bool is_extension_keyword =
    301         template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION;
    302 
    303     // Only create an exact match if |remaining_input| is empty or if
    304     // this is an extension keyword.  If |remaining_input| is a
    305     // non-empty non-extension keyword (i.e., a regular keyword that
    306     // supports replacement and that has extra text following it),
    307     // then SearchProvider creates the exact (a.k.a. verbatim) match.
    308     if (!remaining_input.empty() && !is_extension_keyword)
    309       return;
    310 
    311     // TODO(pkasting): We should probably check that if the user explicitly
    312     // typed a scheme, that scheme matches the one in |template_url|.
    313 
    314     // When creating an exact match (either for the keyword itself, no
    315     // remaining query or an extension keyword, possibly with remaining
    316     // input), allow the match to be the default match.
    317     matches_.push_back(CreateAutocompleteMatch(
    318         template_url, input, keyword.length(), remaining_input, true, -1));
    319 
    320     if (profile_ && is_extension_keyword && extensions_delegate_) {
    321       if (extensions_delegate_->Start(input, minimal_changes, template_url,
    322                                       remaining_input))
    323         keyword_mode_toggle.StayInKeywordMode();
    324     }
    325   } else {
    326     if (matches.size() > kMaxMatches)
    327       matches.erase(matches.begin() + kMaxMatches, matches.end());
    328     for (TemplateURLService::TemplateURLVector::const_iterator i(
    329          matches.begin()); i != matches.end(); ++i) {
    330       matches_.push_back(CreateAutocompleteMatch(
    331           *i, input, keyword.length(), remaining_input, false, -1));
    332     }
    333   }
    334 }
    335 
    336 void KeywordProvider::Stop(bool clear_cached_results) {
    337   done_ = true;
    338   if (extensions_delegate_)
    339     extensions_delegate_->MaybeEndExtensionKeywordMode();
    340 }
    341 
    342 KeywordProvider::~KeywordProvider() {}
    343 
    344 // static
    345 bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput& input,
    346                                               base::string16* keyword,
    347                                               base::string16* remaining_input) {
    348   if ((input.type() == metrics::OmniboxInputType::INVALID) ||
    349       (input.type() == metrics::OmniboxInputType::FORCED_QUERY))
    350     return false;
    351 
    352   *keyword = TemplateURLService::CleanUserInputKeyword(
    353       SplitKeywordFromInput(input.text(), true, remaining_input));
    354   return !keyword->empty();
    355 }
    356 
    357 // static
    358 int KeywordProvider::CalculateRelevance(metrics::OmniboxInputType::Type type,
    359                                         bool complete,
    360                                         bool supports_replacement,
    361                                         bool prefer_keyword,
    362                                         bool allow_exact_keyword_match) {
    363   // This function is responsible for scoring suggestions of keywords
    364   // themselves and the suggestion of the verbatim query on an
    365   // extension keyword.  SearchProvider::CalculateRelevanceForKeywordVerbatim()
    366   // scores verbatim query suggestions for non-extension keywords.
    367   // These two functions are currently in sync, but there's no reason
    368   // we couldn't decide in the future to score verbatim matches
    369   // differently for extension and non-extension keywords.  If you
    370   // make such a change, however, you should update this comment to
    371   // describe it, so it's clear why the functions diverge.
    372   if (!complete)
    373     return (type == metrics::OmniboxInputType::URL) ? 700 : 450;
    374   if (!supports_replacement || (allow_exact_keyword_match && prefer_keyword))
    375     return 1500;
    376   return (allow_exact_keyword_match &&
    377           (type == metrics::OmniboxInputType::QUERY)) ?
    378       1450 : 1100;
    379 }
    380 
    381 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch(
    382     const TemplateURL* template_url,
    383     const AutocompleteInput& input,
    384     size_t prefix_length,
    385     const base::string16& remaining_input,
    386     bool allowed_to_be_default_match,
    387     int relevance) {
    388   DCHECK(template_url);
    389   const bool supports_replacement =
    390       template_url->url_ref().SupportsReplacement(
    391           GetTemplateURLService()->search_terms_data());
    392 
    393   // Create an edit entry of "[keyword] [remaining input]".  This is helpful
    394   // even when [remaining input] is empty, as the user can select the popup
    395   // choice and immediately begin typing in query input.
    396   const base::string16& keyword = template_url->keyword();
    397   const bool keyword_complete = (prefix_length == keyword.length());
    398   if (relevance < 0) {
    399     relevance =
    400         CalculateRelevance(input.type(), keyword_complete,
    401                            // When the user wants keyword matches to take
    402                            // preference, score them highly regardless of
    403                            // whether the input provides query text.
    404                            supports_replacement, input.prefer_keyword(),
    405                            input.allow_exact_keyword_match());
    406   }
    407   AutocompleteMatch match(this, relevance, false,
    408       supports_replacement ? AutocompleteMatchType::SEARCH_OTHER_ENGINE :
    409                              AutocompleteMatchType::HISTORY_KEYWORD);
    410   match.allowed_to_be_default_match = allowed_to_be_default_match;
    411   match.fill_into_edit = keyword;
    412   if (!remaining_input.empty() || supports_replacement)
    413     match.fill_into_edit.push_back(L' ');
    414   match.fill_into_edit.append(remaining_input);
    415   // If we wanted to set |result.inline_autocompletion| correctly, we'd need
    416   // CleanUserInputKeyword() to return the amount of adjustment it's made to
    417   // the user's input.  Because right now inexact keyword matches can't score
    418   // more highly than a "what you typed" match from one of the other providers,
    419   // we just don't bother to do this, and leave inline autocompletion off.
    420 
    421   // Create destination URL and popup entry content by substituting user input
    422   // into keyword templates.
    423   FillInURLAndContents(remaining_input, template_url, &match);
    424 
    425   match.keyword = keyword;
    426   match.transition = content::PAGE_TRANSITION_KEYWORD;
    427 
    428   return match;
    429 }
    430 
    431 void KeywordProvider::FillInURLAndContents(
    432     const base::string16& remaining_input,
    433     const TemplateURL* element,
    434     AutocompleteMatch* match) const {
    435   DCHECK(!element->short_name().empty());
    436   const TemplateURLRef& element_ref = element->url_ref();
    437   DCHECK(element_ref.IsValid(GetTemplateURLService()->search_terms_data()));
    438   int message_id = (element->GetType() == TemplateURL::OMNIBOX_API_EXTENSION) ?
    439       IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH;
    440   if (remaining_input.empty()) {
    441     // Allow extension keyword providers to accept empty string input. This is
    442     // useful to allow extensions to do something in the case where no input is
    443     // entered.
    444     if (element_ref.SupportsReplacement(
    445             GetTemplateURLService()->search_terms_data()) &&
    446         (element->GetType() != TemplateURL::OMNIBOX_API_EXTENSION)) {
    447       // No query input; return a generic, no-destination placeholder.
    448       match->contents.assign(
    449           l10n_util::GetStringFUTF16(message_id,
    450               element->AdjustedShortNameForLocaleDirection(),
    451               l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE)));
    452       match->contents_class.push_back(
    453           ACMatchClassification(0, ACMatchClassification::DIM));
    454     } else {
    455       // Keyword that has no replacement text (aka a shorthand for a URL).
    456       match->destination_url = GURL(element->url());
    457       match->contents.assign(element->short_name());
    458       AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(),
    459           match->contents.length(), ACMatchClassification::NONE,
    460           &match->contents_class);
    461     }
    462   } else {
    463     // Create destination URL by escaping user input and substituting into
    464     // keyword template URL.  The escaping here handles whitespace in user
    465     // input, but we rely on later canonicalization functions to do more
    466     // fixup to make the URL valid if necessary.
    467     DCHECK(element_ref.SupportsReplacement(
    468         GetTemplateURLService()->search_terms_data()));
    469     TemplateURLRef::SearchTermsArgs search_terms_args(remaining_input);
    470     search_terms_args.append_extra_query_params =
    471         element == GetTemplateURLService()->GetDefaultSearchProvider();
    472     match->destination_url = GURL(element_ref.ReplaceSearchTerms(
    473         search_terms_args, GetTemplateURLService()->search_terms_data()));
    474     std::vector<size_t> content_param_offsets;
    475     match->contents.assign(l10n_util::GetStringFUTF16(message_id,
    476                                                       element->short_name(),
    477                                                       remaining_input,
    478                                                       &content_param_offsets));
    479     DCHECK_EQ(2U, content_param_offsets.size());
    480     AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
    481         remaining_input.length(), match->contents.length(),
    482         ACMatchClassification::NONE, &match->contents_class);
    483   }
    484 }
    485 
    486 TemplateURLService* KeywordProvider::GetTemplateURLService() const {
    487   TemplateURLService* service = profile_ ?
    488       TemplateURLServiceFactory::GetForProfile(profile_) : model_;
    489   // Make sure the model is loaded. This is cheap and quickly bails out if
    490   // the model is already loaded.
    491   DCHECK(service);
    492   service->Load();
    493   return service;
    494 }
    495