1 // Copyright 2014 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 "components/omnibox/browser/keyword_provider.h"
6 
7 #include <algorithm>
8 #include <vector>
9 
10 #include "base/i18n/case_conversion.h"
11 #include "base/strings/string16.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/trace_event/trace_event.h"
15 #include "components/omnibox/browser/autocomplete_match.h"
16 #include "components/omnibox/browser/autocomplete_provider_client.h"
17 #include "components/omnibox/browser/autocomplete_provider_listener.h"
18 #include "components/omnibox/browser/keyword_extensions_delegate.h"
19 #include "components/omnibox/browser/omnibox_field_trial.h"
20 #include "components/omnibox/browser/search_provider.h"
21 #include "components/search_engines/omnibox_focus_type.h"
22 #include "components/search_engines/template_url.h"
23 #include "components/search_engines/template_url_service.h"
24 #include "components/strings/grit/components_strings.h"
25 #include "components/url_formatter/url_formatter.h"
26 #include "net/base/escape.h"
27 #include "third_party/metrics_proto/omnibox_input_type.pb.h"
28 #include "ui/base/l10n/l10n_util.h"
29 
30 namespace {
31 
32 // Helper functor for Start(), for sorting keyword matches by quality.
33 class CompareQuality {
34  public:
35   // A keyword is of higher quality when a greater fraction of the important
36   // part of it has been typed, that is, when the meaningful keyword length is
37   // shorter.
38   //
39   // TODO(pkasting): Most recent and most frequent keywords are probably
40   // better rankings than the fraction of the keyword typed.  We should
41   // always put any exact matches first no matter what, since the code in
42   // Start() assumes this (and it makes sense).
operator ()(const TemplateURLService::TURLAndMeaningfulLength t_url_match1,const TemplateURLService::TURLAndMeaningfulLength t_url_match2) const43   bool operator()(
44       const TemplateURLService::TURLAndMeaningfulLength t_url_match1,
45       const TemplateURLService::TURLAndMeaningfulLength t_url_match2) const {
46     return t_url_match1.second < t_url_match2.second;
47   }
48 };
49 
50 // Helper for KeywordProvider::Start(), for ending keyword mode unless
51 // explicitly told otherwise.
52 class ScopedEndExtensionKeywordMode {
53  public:
54   explicit ScopedEndExtensionKeywordMode(KeywordExtensionsDelegate* delegate);
55   ~ScopedEndExtensionKeywordMode();
56   ScopedEndExtensionKeywordMode(const ScopedEndExtensionKeywordMode&) = delete;
57   ScopedEndExtensionKeywordMode& operator=(
58       const ScopedEndExtensionKeywordMode&) = delete;
59 
60   void StayInKeywordMode();
61 
62  private:
63   KeywordExtensionsDelegate* delegate_;
64 };
65 
ScopedEndExtensionKeywordMode(KeywordExtensionsDelegate * delegate)66 ScopedEndExtensionKeywordMode::ScopedEndExtensionKeywordMode(
67     KeywordExtensionsDelegate* delegate)
68     : delegate_(delegate) {
69 }
70 
~ScopedEndExtensionKeywordMode()71 ScopedEndExtensionKeywordMode::~ScopedEndExtensionKeywordMode() {
72   if (delegate_)
73     delegate_->MaybeEndExtensionKeywordMode();
74 }
75 
StayInKeywordMode()76 void ScopedEndExtensionKeywordMode::StayInKeywordMode() {
77   delegate_ = nullptr;
78 }
79 
80 }  // namespace
81 
KeywordProvider(AutocompleteProviderClient * client,AutocompleteProviderListener * listener)82 KeywordProvider::KeywordProvider(AutocompleteProviderClient* client,
83                                  AutocompleteProviderListener* listener)
84     : AutocompleteProvider(AutocompleteProvider::TYPE_KEYWORD),
85       listener_(listener),
86       model_(client->GetTemplateURLService()),
87       extensions_delegate_(client->GetKeywordExtensionsDelegate(this)) {}
88 
89 // static
SplitKeywordFromInput(const base::string16 & input,bool trim_leading_whitespace,base::string16 * remaining_input)90 base::string16 KeywordProvider::SplitKeywordFromInput(
91     const base::string16& input,
92     bool trim_leading_whitespace,
93     base::string16* remaining_input) {
94   // Find end of first token.  The AutocompleteController has trimmed leading
95   // whitespace, so we need not skip over that.
96   const size_t first_white(input.find_first_of(base::kWhitespaceUTF16));
97   DCHECK_NE(0U, first_white);
98   if (first_white == base::string16::npos)
99     return input;  // Only one token provided.
100 
101   // Set |remaining_input| to everything after the first token.
102   DCHECK(remaining_input != nullptr);
103   const size_t remaining_start = trim_leading_whitespace ?
104       input.find_first_not_of(base::kWhitespaceUTF16, first_white) :
105       first_white + 1;
106 
107   if (remaining_start < input.length())
108     remaining_input->assign(input.begin() + remaining_start, input.end());
109 
110   // Return first token as keyword.
111   return input.substr(0, first_white);
112 }
113 
114 // static
SplitReplacementStringFromInput(const base::string16 & input,bool trim_leading_whitespace)115 base::string16 KeywordProvider::SplitReplacementStringFromInput(
116     const base::string16& input,
117     bool trim_leading_whitespace) {
118   // The input may contain leading whitespace, strip it.
119   base::string16 trimmed_input;
120   base::TrimWhitespace(input, base::TRIM_LEADING, &trimmed_input);
121 
122   // And extract the replacement string.
123   base::string16 remaining_input;
124   SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
125       &remaining_input);
126   return remaining_input;
127 }
128 
129 // static
GetSubstitutingTemplateURLForInput(TemplateURLService * model,AutocompleteInput * input)130 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput(
131     TemplateURLService* model,
132     AutocompleteInput* input) {
133   if (!input->allow_exact_keyword_match())
134     return nullptr;
135 
136   DCHECK(model);
137   base::string16 keyword, remaining_input;
138   if (!ExtractKeywordFromInput(*input, model, &keyword, &remaining_input))
139     return nullptr;
140 
141   const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
142   if (template_url &&
143       template_url->SupportsReplacement(model->search_terms_data())) {
144     // Adjust cursor position iff it was set before, otherwise leave it as is.
145     size_t cursor_position = base::string16::npos;
146     // The adjustment assumes that the keyword was stripped from the beginning
147     // of the original input.
148     if (input->cursor_position() != base::string16::npos &&
149         !remaining_input.empty() &&
150         base::EndsWith(input->text(), remaining_input,
151                        base::CompareCase::SENSITIVE)) {
152       int offset = input->text().length() - input->cursor_position();
153       // The cursor should never be past the last character or before the
154       // first character.
155       DCHECK_GE(offset, 0);
156       DCHECK_LE(offset, static_cast<int>(input->text().length()));
157       if (offset <= 0) {
158         // Normalize the cursor to be exactly after the last character.
159         cursor_position = remaining_input.length();
160       } else {
161         // If somehow the cursor was before the remaining text, set it to 0,
162         // otherwise adjust it relative to the remaining text.
163         cursor_position = offset > static_cast<int>(remaining_input.length()) ?
164             0u : remaining_input.length() - offset;
165       }
166     }
167     input->UpdateText(remaining_input, cursor_position, input->parts());
168     return template_url;
169   }
170 
171   return nullptr;
172 }
173 
GetKeywordForText(const base::string16 & text) const174 base::string16 KeywordProvider::GetKeywordForText(
175     const base::string16& text) const {
176   TemplateURLService* url_service = GetTemplateURLService();
177   if (!url_service)
178     return base::string16();
179 
180   const base::string16 keyword(CleanUserInputKeyword(url_service, text));
181 
182   if (keyword.empty())
183     return keyword;
184 
185   // Don't provide a keyword if it doesn't support replacement.
186   const TemplateURL* const template_url =
187       url_service->GetTemplateURLForKeyword(keyword);
188   if (!template_url ||
189       !template_url->SupportsReplacement(url_service->search_terms_data()))
190     return base::string16();
191 
192   // Don't provide a keyword for inactive/disabled extension keywords.
193   if ((template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION) &&
194       extensions_delegate_ &&
195       !extensions_delegate_->IsEnabledExtension(template_url->GetExtensionId()))
196     return base::string16();
197 
198   return keyword;
199 }
200 
CreateVerbatimMatch(const base::string16 & text,const base::string16 & keyword,const AutocompleteInput & input)201 AutocompleteMatch KeywordProvider::CreateVerbatimMatch(
202     const base::string16& text,
203     const base::string16& keyword,
204     const AutocompleteInput& input) {
205   // A verbatim match is allowed to be the default match when appropriate.
206   return CreateAutocompleteMatch(
207       GetTemplateURLService()->GetTemplateURLForKeyword(keyword),
208       keyword.length(), input, keyword.length(),
209       SplitReplacementStringFromInput(text, true),
210       input.allow_exact_keyword_match(), 0, false);
211 }
212 
DeleteMatch(const AutocompleteMatch & match)213 void KeywordProvider::DeleteMatch(const AutocompleteMatch& match) {
214   const base::string16& suggestion_text = match.contents;
215 
216   const auto pred = [&match](const AutocompleteMatch& i) {
217     return i.keyword == match.keyword &&
218            i.fill_into_edit == match.fill_into_edit;
219   };
220   base::EraseIf(matches_, pred);
221 
222   base::string16 keyword, remaining_input;
223   if (!ExtractKeywordFromInput(
224           keyword_input_, GetTemplateURLService(), &keyword, &remaining_input))
225     return;
226   const TemplateURL* const template_url =
227       GetTemplateURLService()->GetTemplateURLForKeyword(keyword);
228 
229   if ((template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION) &&
230       extensions_delegate_ &&
231       extensions_delegate_->IsEnabledExtension(
232           template_url->GetExtensionId())) {
233     extensions_delegate_->DeleteSuggestion(template_url, suggestion_text);
234   }
235 }
236 
Start(const AutocompleteInput & input,bool minimal_changes)237 void KeywordProvider::Start(const AutocompleteInput& input,
238                             bool minimal_changes) {
239   TRACE_EVENT0("omnibox", "KeywordProvider::Start");
240   // This object ensures we end keyword mode if we exit the function without
241   // toggling keyword mode to on.
242   ScopedEndExtensionKeywordMode keyword_mode_toggle(extensions_delegate_.get());
243 
244   matches_.clear();
245 
246   if (!minimal_changes) {
247     done_ = true;
248 
249     // Input has changed. Increment the input ID so that we can discard any
250     // stale extension suggestions that may be incoming.
251     if (extensions_delegate_)
252       extensions_delegate_->IncrementInputId();
253   }
254 
255   if (input.focus_type() != OmniboxFocusType::DEFAULT)
256     return;
257 
258   GetTemplateURLService();
259   DCHECK(model_);
260   // Split user input into a keyword and some query input.
261   //
262   // We want to suggest keywords even when users have started typing URLs, on
263   // the assumption that they might not realize they no longer need to go to a
264   // site to be able to search it.  So we call CleanUserInputKeyword() to strip
265   // any initial scheme and/or "www.".  NOTE: Any heuristics or UI used to
266   // automatically/manually create keywords will need to be in sync with
267   // whatever we do here!
268   //
269   // TODO(pkasting): http://crbug/347744 If someday we remember usage frequency
270   // for keywords, we might suggest keywords that haven't even been partially
271   // typed, if the user uses them enough and isn't obviously typing something
272   // else.  In this case we'd consider all input here to be query input.
273   base::string16 keyword, remaining_input;
274   if (!ExtractKeywordFromInput(input, model_, &keyword,
275                                &remaining_input))
276     return;
277 
278   keyword_input_ = input;
279 
280   // Get the best matches for this keyword.
281   //
282   // NOTE: We could cache the previous keywords and reuse them here in the
283   // |minimal_changes| case, but since we'd still have to recalculate their
284   // relevances and we can just recreate the results synchronously anyway, we
285   // don't bother.
286   TemplateURLService::TURLsAndMeaningfulLengths matches;
287   model_->AddMatchingKeywords(keyword, !remaining_input.empty(), &matches);
288 
289   for (auto i(matches.begin()); i != matches.end();) {
290     const TemplateURL* template_url = i->first;
291 
292     // Prune any extension keywords that are disallowed in incognito mode (if
293     // we're incognito), or disabled.
294     if (template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION &&
295         extensions_delegate_ &&
296         !extensions_delegate_->IsEnabledExtension(
297             template_url->GetExtensionId())) {
298       i = matches.erase(i);
299       continue;
300     }
301 
302     // Prune any substituting keywords if there is no substitution.
303     if (template_url->SupportsReplacement(
304             model_->search_terms_data()) &&
305         remaining_input.empty() &&
306         !input.allow_exact_keyword_match()) {
307       i = matches.erase(i);
308       continue;
309     }
310 
311     ++i;
312   }
313   if (matches.empty())
314     return;
315   std::sort(matches.begin(), matches.end(), CompareQuality());
316 
317   // Limit to one exact or three inexact matches, and mark them up for display
318   // in the autocomplete popup.
319   // Any exact match is going to be the highest quality match, and thus at the
320   // front of our vector.
321   if (matches.front().first->keyword() == keyword) {
322     const TemplateURL* template_url = matches.front().first;
323     const size_t meaningful_keyword_length = matches.front().second;
324     const bool is_extension_keyword =
325         template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION;
326 
327     // Only create an exact match if |remaining_input| is empty or if
328     // this is an extension keyword.  If |remaining_input| is a
329     // non-empty non-extension keyword (i.e., a regular keyword that
330     // supports replacement and that has extra text following it),
331     // then SearchProvider creates the exact (a.k.a. verbatim) match.
332     if (!remaining_input.empty() && !is_extension_keyword)
333       return;
334 
335     // TODO(pkasting): We should probably check that if the user explicitly
336     // typed a scheme, that scheme matches the one in |template_url|.
337 
338     // When creating an exact match (either for the keyword itself, no
339     // remaining query or an extension keyword, possibly with remaining
340     // input), allow the match to be the default match when appropriate.
341     // For exactly-typed non-substituting keywords, it's always appropriate.
342     matches_.push_back(CreateAutocompleteMatch(
343         template_url, meaningful_keyword_length, input, keyword.length(),
344         remaining_input,
345         input.allow_exact_keyword_match() ||
346             !template_url->SupportsReplacement(model_->search_terms_data()),
347         -1, false));
348 
349     // Having extension-provided suggestions appear outside keyword mode can
350     // be surprising, so only query for suggestions when in keyword mode.
351     if (is_extension_keyword && extensions_delegate_ &&
352         input.allow_exact_keyword_match()) {
353       if (extensions_delegate_->Start(input, minimal_changes, template_url,
354                                       remaining_input))
355         keyword_mode_toggle.StayInKeywordMode();
356     }
357   } else {
358     for (TemplateURLService::TURLsAndMeaningfulLengths::const_iterator i(
359              matches.begin());
360          (i != matches.end()) && (matches_.size() < provider_max_matches_);
361          ++i) {
362       // Skip keywords that we've already added.  It's possible we may have
363       // retrieved the same keyword twice.  For example, the keyword
364       // "abc.abc.com" may be retrieved for the input "abc" from the full
365       // keyword matching and the domain matching passes.
366       ACMatches::const_iterator duplicate = std::find_if(
367           matches_.begin(), matches_.end(),
368           [&i] (const AutocompleteMatch& m) {
369             return m.keyword == i->first->keyword();
370           });
371       if (duplicate == matches_.end()) {
372         matches_.push_back(CreateAutocompleteMatch(
373             i->first, i->second, input, keyword.length(), remaining_input,
374             false, -1, false));
375       }
376     }
377   }
378 }
379 
Stop(bool clear_cached_results,bool due_to_user_inactivity)380 void KeywordProvider::Stop(bool clear_cached_results,
381                            bool due_to_user_inactivity) {
382   done_ = true;
383   // Only end an extension's request if the user did something to explicitly
384   // cancel it; mere inactivity shouldn't terminate long-running extension
385   // operations since the user likely explicitly requested them.
386   if (extensions_delegate_ && !due_to_user_inactivity)
387     extensions_delegate_->MaybeEndExtensionKeywordMode();
388 }
389 
~KeywordProvider()390 KeywordProvider::~KeywordProvider() {}
391 
392 // static
ExtractKeywordFromInput(const AutocompleteInput & input,const TemplateURLService * template_url_service,base::string16 * keyword,base::string16 * remaining_input)393 bool KeywordProvider::ExtractKeywordFromInput(
394     const AutocompleteInput& input,
395     const TemplateURLService* template_url_service,
396     base::string16* keyword,
397     base::string16* remaining_input) {
398   if ((input.type() == metrics::OmniboxInputType::EMPTY))
399     return false;
400 
401   DCHECK(template_url_service);
402   *keyword = CleanUserInputKeyword(
403       template_url_service,
404       SplitKeywordFromInput(input.text(), true, remaining_input));
405   return !keyword->empty();
406 }
407 
408 // static
CalculateRelevance(metrics::OmniboxInputType type,bool complete,bool sufficiently_complete,bool supports_replacement,bool prefer_keyword,bool allow_exact_keyword_match)409 int KeywordProvider::CalculateRelevance(metrics::OmniboxInputType type,
410                                         bool complete,
411                                         bool sufficiently_complete,
412                                         bool supports_replacement,
413                                         bool prefer_keyword,
414                                         bool allow_exact_keyword_match) {
415   if (!complete) {
416     const int sufficiently_complete_score =
417         OmniboxFieldTrial::KeywordScoreForSufficientlyCompleteMatch();
418     // If we have a special score to apply for sufficiently-complete matches,
419     // do so.
420     if (sufficiently_complete && (sufficiently_complete_score > -1))
421       return sufficiently_complete_score;
422     return (type == metrics::OmniboxInputType::URL) ? 700 : 450;
423   }
424   if (!supports_replacement)
425     return 1500;
426   return SearchProvider::CalculateRelevanceForKeywordVerbatim(
427       type, allow_exact_keyword_match, prefer_keyword);
428 }
429 
CreateAutocompleteMatch(const TemplateURL * template_url,const size_t meaningful_keyword_length,const AutocompleteInput & input,size_t prefix_length,const base::string16 & remaining_input,bool allowed_to_be_default_match,int relevance,bool deletable)430 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch(
431     const TemplateURL* template_url,
432     const size_t meaningful_keyword_length,
433     const AutocompleteInput& input,
434     size_t prefix_length,
435     const base::string16& remaining_input,
436     bool allowed_to_be_default_match,
437     int relevance,
438     bool deletable) {
439   DCHECK(template_url);
440   const bool supports_replacement =
441       template_url->url_ref().SupportsReplacement(
442           GetTemplateURLService()->search_terms_data());
443 
444   // Create an edit entry of "[keyword] [remaining input]".  This is helpful
445   // even when [remaining input] is empty, as the user can select the popup
446   // choice and immediately begin typing in query input.
447   const base::string16& keyword = template_url->keyword();
448   const bool keyword_complete = (prefix_length == keyword.length());
449   const bool sufficiently_complete =
450       (prefix_length >= meaningful_keyword_length);
451   if (relevance < 0) {
452     relevance =
453         CalculateRelevance(input.type(), keyword_complete,
454                            sufficiently_complete,
455                            // When the user wants keyword matches to take
456                            // preference, score them highly regardless of
457                            // whether the input provides query text.
458                            supports_replacement, input.prefer_keyword(),
459                            input.allow_exact_keyword_match());
460   }
461 
462   AutocompleteMatch match(this, relevance, deletable,
463                           supports_replacement
464                               ? AutocompleteMatchType::SEARCH_OTHER_ENGINE
465                               : AutocompleteMatchType::HISTORY_KEYWORD);
466   match.allowed_to_be_default_match = allowed_to_be_default_match;
467   match.fill_into_edit = keyword;
468   if (!remaining_input.empty() || supports_replacement)
469     match.fill_into_edit.push_back(L' ');
470   match.fill_into_edit.append(remaining_input);
471   // If we wanted to set |result.inline_autocompletion| correctly, we'd need
472   // CleanUserInputKeyword() to return the amount of adjustment it's made to
473   // the user's input.  Because right now inexact keyword matches can't score
474   // more highly than a "what you typed" match from one of the other providers,
475   // we just don't bother to do this, and leave inline autocompletion off.
476 
477   // Create destination URL and popup entry content by substituting user input
478   // into keyword templates.
479   FillInURLAndContents(remaining_input, template_url, &match);
480 
481   match.keyword = keyword;
482   match.from_keyword = true;
483   match.transition = ui::PAGE_TRANSITION_KEYWORD;
484 
485   return match;
486 }
487 
FillInURLAndContents(const base::string16 & remaining_input,const TemplateURL * element,AutocompleteMatch * match) const488 void KeywordProvider::FillInURLAndContents(
489     const base::string16& remaining_input,
490     const TemplateURL* element,
491     AutocompleteMatch* match) const {
492   DCHECK(!element->short_name().empty());
493   const TemplateURLRef& element_ref = element->url_ref();
494   DCHECK(element_ref.IsValid(GetTemplateURLService()->search_terms_data()));
495   if (remaining_input.empty()) {
496     // Allow extension keyword providers to accept empty string input. This is
497     // useful to allow extensions to do something in the case where no input is
498     // entered.
499     if (element_ref.SupportsReplacement(
500             GetTemplateURLService()->search_terms_data()) &&
501         (element->type() != TemplateURL::OMNIBOX_API_EXTENSION)) {
502       // No query input; return a generic, no-destination placeholder.
503       match->contents.assign(
504           l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE));
505       match->contents_class.emplace_back(0, ACMatchClassification::DIM);
506     } else {
507       // Keyword or extension that has no replacement text (aka a shorthand for
508       // a URL).
509       match->destination_url = GURL(element->url());
510       match->contents.assign(element->short_name());
511       if (!element->short_name().empty())
512         match->contents_class.emplace_back(0, ACMatchClassification::MATCH);
513     }
514   } else {
515     // Create destination URL by escaping user input and substituting into
516     // keyword template URL.  The escaping here handles whitespace in user
517     // input, but we rely on later canonicalization functions to do more
518     // fixup to make the URL valid if necessary.
519     DCHECK(element_ref.SupportsReplacement(
520         GetTemplateURLService()->search_terms_data()));
521     TemplateURLRef::SearchTermsArgs search_terms_args(remaining_input);
522     search_terms_args.append_extra_query_params_from_command_line =
523         element == GetTemplateURLService()->GetDefaultSearchProvider();
524     match->destination_url = GURL(element_ref.ReplaceSearchTerms(
525         search_terms_args, GetTemplateURLService()->search_terms_data()));
526     match->contents = remaining_input;
527     match->contents_class.emplace_back(0, ACMatchClassification::NONE);
528   }
529 }
530 
GetTemplateURLService() const531 TemplateURLService* KeywordProvider::GetTemplateURLService() const {
532   // Make sure the model is loaded. This is cheap and quickly bails out if
533   // the model is already loaded.
534   model_->Load();
535   return model_;
536 }
537 
538 // static
CleanUserInputKeyword(const TemplateURLService * template_url_service,const base::string16 & keyword)539 base::string16 KeywordProvider::CleanUserInputKeyword(
540     const TemplateURLService* template_url_service,
541     const base::string16& keyword) {
542   DCHECK(template_url_service);
543   base::string16 result(base::i18n::ToLower(keyword));
544   base::TrimWhitespace(result, base::TRIM_ALL, &result);
545   // If this keyword is found with no additional cleaning of input, return it.
546   if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
547     return result;
548 
549   // If keyword is not found, try removing a "http" or "https" scheme if any.
550   url::Component scheme_component;
551   if (url::ExtractScheme(base::UTF16ToUTF8(result).c_str(),
552                          static_cast<int>(result.length()),
553                          &scheme_component) &&
554       (!result.compare(0, scheme_component.end(),
555                        base::ASCIIToUTF16(url::kHttpScheme)) ||
556        !result.compare(0, scheme_component.end(),
557                        base::ASCIIToUTF16(url::kHttpsScheme)))) {
558     // Remove the scheme and the trailing ':'.
559     result.erase(0, scheme_component.end() + 1);
560     if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
561       return result;
562     // Many schemes usually have "//" after them, so strip it too.
563     const base::string16 after_scheme(base::ASCIIToUTF16("//"));
564     if (result.compare(0, after_scheme.length(), after_scheme) == 0)
565       result.erase(0, after_scheme.length());
566     if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
567       return result;
568   }
569 
570   // Remove leading "www.", if any, and again try to find a matching keyword.
571   // The 'www.' stripping is done directly here instead of calling
572   // url_formatter::StripWWW because we're not assuming that the keyword is a
573   // hostname.
574   const base::string16 kWww(base::ASCIIToUTF16("www."));
575   constexpr size_t kWwwLength = 4;
576   result = base::StartsWith(result, kWww, base::CompareCase::SENSITIVE)
577                ? result.substr(kWwwLength)
578                : result;
579   if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
580     return result;
581 
582   // Remove trailing "/", if any.
583   if (!result.empty() && result.back() == '/')
584     result.pop_back();
585   return result;
586 }
587