// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/autocomplete/autocomplete_match.h" #include "base/i18n/time_formatting.h" #include "base/logging.h" #include "base/strings/string16.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "chrome/browser/autocomplete/autocomplete_provider.h" #include "chrome/browser/search_engines/template_url.h" #include "chrome/browser/search_engines/template_url_service.h" #include "chrome/browser/search_engines/template_url_service_factory.h" #include "content/public/common/url_constants.h" #include "grit/theme_resources.h" namespace { bool IsTrivialClassification(const ACMatchClassifications& classifications) { return classifications.empty() || ((classifications.size() == 1) && (classifications.back().style == ACMatchClassification::NONE)); } } // namespace // AutocompleteMatch ---------------------------------------------------------- // static const char16 AutocompleteMatch::kInvalidChars[] = { '\n', '\r', '\t', 0x2028, // Line separator 0x2029, // Paragraph separator 0 }; AutocompleteMatch::AutocompleteMatch() : provider(NULL), relevance(0), typed_count(-1), deletable(false), allowed_to_be_default_match(false), transition(content::PAGE_TRANSITION_GENERATED), is_history_what_you_typed_match(false), type(AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED), starred(false), from_previous(false) { } AutocompleteMatch::AutocompleteMatch(AutocompleteProvider* provider, int relevance, bool deletable, Type type) : provider(provider), relevance(relevance), typed_count(-1), deletable(deletable), allowed_to_be_default_match(false), transition(content::PAGE_TRANSITION_TYPED), is_history_what_you_typed_match(false), type(type), starred(false), from_previous(false) { } AutocompleteMatch::AutocompleteMatch(const AutocompleteMatch& match) : provider(match.provider), relevance(match.relevance), typed_count(match.typed_count), deletable(match.deletable), fill_into_edit(match.fill_into_edit), inline_autocompletion(match.inline_autocompletion), allowed_to_be_default_match(match.allowed_to_be_default_match), destination_url(match.destination_url), stripped_destination_url(match.stripped_destination_url), contents(match.contents), contents_class(match.contents_class), description(match.description), description_class(match.description_class), transition(match.transition), is_history_what_you_typed_match(match.is_history_what_you_typed_match), type(match.type), associated_keyword(match.associated_keyword.get() ? new AutocompleteMatch(*match.associated_keyword) : NULL), keyword(match.keyword), starred(match.starred), from_previous(match.from_previous), search_terms_args(match.search_terms_args.get() ? new TemplateURLRef::SearchTermsArgs(*match.search_terms_args) : NULL), additional_info(match.additional_info) { } AutocompleteMatch::~AutocompleteMatch() { } AutocompleteMatch& AutocompleteMatch::operator=( const AutocompleteMatch& match) { if (this == &match) return *this; provider = match.provider; relevance = match.relevance; typed_count = match.typed_count; deletable = match.deletable; fill_into_edit = match.fill_into_edit; inline_autocompletion = match.inline_autocompletion; allowed_to_be_default_match = match.allowed_to_be_default_match; destination_url = match.destination_url; stripped_destination_url = match.stripped_destination_url; contents = match.contents; contents_class = match.contents_class; description = match.description; description_class = match.description_class; transition = match.transition; is_history_what_you_typed_match = match.is_history_what_you_typed_match; type = match.type; associated_keyword.reset(match.associated_keyword.get() ? new AutocompleteMatch(*match.associated_keyword) : NULL); keyword = match.keyword; starred = match.starred; from_previous = match.from_previous; search_terms_args.reset(match.search_terms_args.get() ? new TemplateURLRef::SearchTermsArgs(*match.search_terms_args) : NULL); additional_info = match.additional_info; return *this; } // static int AutocompleteMatch::TypeToIcon(Type type) { int icons[] = { IDR_OMNIBOX_HTTP, IDR_OMNIBOX_HTTP, IDR_OMNIBOX_HTTP, IDR_OMNIBOX_HTTP, IDR_OMNIBOX_HTTP, IDR_OMNIBOX_HTTP, IDR_OMNIBOX_SEARCH, IDR_OMNIBOX_SEARCH, IDR_OMNIBOX_SEARCH, IDR_OMNIBOX_SEARCH, IDR_OMNIBOX_EXTENSION_APP, // ContactProvider isn't used by the omnibox, so this icon is never // displayed. IDR_OMNIBOX_SEARCH, IDR_OMNIBOX_HTTP, }; COMPILE_ASSERT(arraysize(icons) == AutocompleteMatchType::NUM_TYPES, icons_array_must_match_type_enum); return icons[type]; } // static int AutocompleteMatch::TypeToLocationBarIcon(Type type) { int id = TypeToIcon(type); if (id == IDR_OMNIBOX_HTTP) return IDR_LOCATION_BAR_HTTP; return id; } // static bool AutocompleteMatch::MoreRelevant(const AutocompleteMatch& elem1, const AutocompleteMatch& elem2) { // For equal-relevance matches, we sort alphabetically, so that providers // who return multiple elements at the same priority get a "stable" sort // across multiple updates. return (elem1.relevance == elem2.relevance) ? (elem1.contents < elem2.contents) : (elem1.relevance > elem2.relevance); } // static bool AutocompleteMatch::DestinationSortFunc(const AutocompleteMatch& elem1, const AutocompleteMatch& elem2) { // Sort identical destination_urls together. Place the most relevant matches // first, so that when we call std::unique(), these are the ones that get // preserved. if (DestinationsEqual(elem1, elem2) || (elem1.stripped_destination_url.is_empty() && elem2.stripped_destination_url.is_empty())) return MoreRelevant(elem1, elem2); return elem1.stripped_destination_url < elem2.stripped_destination_url; } // static bool AutocompleteMatch::DestinationsEqual(const AutocompleteMatch& elem1, const AutocompleteMatch& elem2) { if (elem1.stripped_destination_url.is_empty() && elem2.stripped_destination_url.is_empty()) return false; return elem1.stripped_destination_url == elem2.stripped_destination_url; } // static void AutocompleteMatch::ClassifyMatchInString( const base::string16& find_text, const base::string16& text, int style, ACMatchClassifications* classification) { ClassifyLocationInString(text.find(find_text), find_text.length(), text.length(), style, classification); } // static void AutocompleteMatch::ClassifyLocationInString( size_t match_location, size_t match_length, size_t overall_length, int style, ACMatchClassifications* classification) { classification->clear(); // Don't classify anything about an empty string // (AutocompleteMatch::Validate() checks this). if (overall_length == 0) return; // Mark pre-match portion of string (if any). if (match_location != 0) { classification->push_back(ACMatchClassification(0, style)); } // Mark matching portion of string. if (match_location == base::string16::npos) { // No match, above classification will suffice for whole string. return; } // Classifying an empty match makes no sense and will lead to validation // errors later. DCHECK_GT(match_length, 0U); classification->push_back(ACMatchClassification(match_location, (style | ACMatchClassification::MATCH) & ~ACMatchClassification::DIM)); // Mark post-match portion of string (if any). const size_t after_match(match_location + match_length); if (after_match < overall_length) { classification->push_back(ACMatchClassification(after_match, style)); } } // static AutocompleteMatch::ACMatchClassifications AutocompleteMatch::MergeClassifications( const ACMatchClassifications& classifications1, const ACMatchClassifications& classifications2) { // We must return the empty vector only if both inputs are truly empty. // The result of merging an empty vector with a single (0, NONE) // classification is the latter one-entry vector. if (IsTrivialClassification(classifications1)) return classifications2.empty() ? classifications1 : classifications2; if (IsTrivialClassification(classifications2)) return classifications1; ACMatchClassifications output; for (ACMatchClassifications::const_iterator i = classifications1.begin(), j = classifications2.begin(); i != classifications1.end();) { AutocompleteMatch::AddLastClassificationIfNecessary(&output, std::max(i->offset, j->offset), i->style | j->style); const size_t next_i_offset = (i + 1) == classifications1.end() ? static_cast(-1) : (i + 1)->offset; const size_t next_j_offset = (j + 1) == classifications2.end() ? static_cast(-1) : (j + 1)->offset; if (next_i_offset >= next_j_offset) ++j; if (next_j_offset >= next_i_offset) ++i; } return output; } // static std::string AutocompleteMatch::ClassificationsToString( const ACMatchClassifications& classifications) { std::string serialized_classifications; for (size_t i = 0; i < classifications.size(); ++i) { if (i) serialized_classifications += ','; serialized_classifications += base::IntToString(classifications[i].offset) + ',' + base::IntToString(classifications[i].style); } return serialized_classifications; } // static ACMatchClassifications AutocompleteMatch::ClassificationsFromString( const std::string& serialized_classifications) { ACMatchClassifications classifications; std::vector tokens; Tokenize(serialized_classifications, ",", &tokens); DCHECK(!(tokens.size() & 1)); // The number of tokens should be even. for (size_t i = 0; i < tokens.size(); i += 2) { int classification_offset = 0; int classification_style = ACMatchClassification::NONE; if (!base::StringToInt(tokens[i], &classification_offset) || !base::StringToInt(tokens[i + 1], &classification_style)) { NOTREACHED(); return classifications; } classifications.push_back(ACMatchClassification(classification_offset, classification_style)); } return classifications; } // static void AutocompleteMatch::AddLastClassificationIfNecessary( ACMatchClassifications* classifications, size_t offset, int style) { DCHECK(classifications); if (classifications->empty() || classifications->back().style != style) { DCHECK(classifications->empty() || (offset > classifications->back().offset)); classifications->push_back(ACMatchClassification(offset, style)); } } // static base::string16 AutocompleteMatch::SanitizeString(const base::string16& text) { // NOTE: This logic is mirrored by |sanitizeString()| in // omnibox_custom_bindings.js. base::string16 result; TrimWhitespace(text, TRIM_LEADING, &result); base::RemoveChars(result, kInvalidChars, &result); return result; } // static bool AutocompleteMatch::IsSearchType(Type type) { return type == AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED || type == AutocompleteMatchType::SEARCH_HISTORY || type == AutocompleteMatchType::SEARCH_SUGGEST || type == AutocompleteMatchType::SEARCH_OTHER_ENGINE; } void AutocompleteMatch::ComputeStrippedDestinationURL(Profile* profile) { stripped_destination_url = destination_url; if (!stripped_destination_url.is_valid()) return; // If the destination URL looks like it was generated from a TemplateURL, // remove all substitutions other than the search terms. This allows us // to eliminate cases like past search URLs from history that differ only // by some obscure query param from each other or from the search/keyword // provider matches. TemplateURL* template_url = GetTemplateURL(profile, true); if (template_url != NULL && template_url->SupportsReplacement()) { base::string16 search_terms; if (template_url->ExtractSearchTermsFromURL(stripped_destination_url, &search_terms)) { stripped_destination_url = GURL(template_url->url_ref().ReplaceSearchTerms( TemplateURLRef::SearchTermsArgs(search_terms))); } } // |replacements| keeps all the substitions we're going to make to // from {destination_url} to {stripped_destination_url}. |need_replacement| // is a helper variable that helps us keep track of whether we need // to apply the replacement. bool needs_replacement = false; GURL::Replacements replacements; // Remove the www. prefix from the host. static const char prefix[] = "www."; static const size_t prefix_len = arraysize(prefix) - 1; std::string host = stripped_destination_url.host(); if (host.compare(0, prefix_len, prefix) == 0) { host = host.substr(prefix_len); replacements.SetHostStr(host); needs_replacement = true; } // Replace https protocol with http protocol. if (stripped_destination_url.SchemeIs(content::kHttpsScheme)) { replacements.SetScheme( content::kHttpScheme, url_parse::Component(0, strlen(content::kHttpScheme))); needs_replacement = true; } if (needs_replacement) stripped_destination_url = stripped_destination_url.ReplaceComponents( replacements); } void AutocompleteMatch::GetKeywordUIState(Profile* profile, base::string16* keyword, bool* is_keyword_hint) const { *is_keyword_hint = associated_keyword.get() != NULL; keyword->assign(*is_keyword_hint ? associated_keyword->keyword : GetSubstitutingExplicitlyInvokedKeyword(profile)); } base::string16 AutocompleteMatch::GetSubstitutingExplicitlyInvokedKeyword( Profile* profile) const { if (transition != content::PAGE_TRANSITION_KEYWORD) return base::string16(); const TemplateURL* t_url = GetTemplateURL(profile, false); return (t_url && t_url->SupportsReplacement()) ? keyword : base::string16(); } TemplateURL* AutocompleteMatch::GetTemplateURL( Profile* profile, bool allow_fallback_to_destination_host) const { DCHECK(profile); TemplateURLService* template_url_service = TemplateURLServiceFactory::GetForProfile(profile); if (template_url_service == NULL) return NULL; TemplateURL* template_url = keyword.empty() ? NULL : template_url_service->GetTemplateURLForKeyword(keyword); if (template_url == NULL && allow_fallback_to_destination_host) { template_url = template_url_service->GetTemplateURLForHost( destination_url.host()); } return template_url; } void AutocompleteMatch::RecordAdditionalInfo(const std::string& property, const std::string& value) { DCHECK(!property.empty()); DCHECK(!value.empty()); additional_info[property] = value; } void AutocompleteMatch::RecordAdditionalInfo(const std::string& property, int value) { RecordAdditionalInfo(property, base::IntToString(value)); } void AutocompleteMatch::RecordAdditionalInfo(const std::string& property, const base::Time& value) { RecordAdditionalInfo(property, UTF16ToUTF8(base::TimeFormatShortDateAndTime(value))); } std::string AutocompleteMatch::GetAdditionalInfo( const std::string& property) const { AdditionalInfo::const_iterator i(additional_info.find(property)); return (i == additional_info.end()) ? std::string() : i->second; } bool AutocompleteMatch::IsVerbatimType() const { const bool is_keyword_verbatim_match = (type == AutocompleteMatchType::SEARCH_OTHER_ENGINE && provider != NULL && provider->type() == AutocompleteProvider::TYPE_SEARCH); return type == AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED || type == AutocompleteMatchType::URL_WHAT_YOU_TYPED || is_keyword_verbatim_match; } #ifndef NDEBUG void AutocompleteMatch::Validate() const { ValidateClassifications(contents, contents_class); ValidateClassifications(description, description_class); } void AutocompleteMatch::ValidateClassifications( const base::string16& text, const ACMatchClassifications& classifications) const { if (text.empty()) { DCHECK(classifications.empty()); return; } // The classifications should always cover the whole string. DCHECK(!classifications.empty()) << "No classification for \"" << text << '"'; DCHECK_EQ(0U, classifications[0].offset) << "Classification misses beginning for \"" << text << '"'; if (classifications.size() == 1) return; // The classifications should always be sorted. size_t last_offset = classifications[0].offset; for (ACMatchClassifications::const_iterator i(classifications.begin() + 1); i != classifications.end(); ++i) { const char* provider_name = provider ? provider->GetName() : "None"; DCHECK_GT(i->offset, last_offset) << " Classification for \"" << text << "\" with offset of " << i->offset << " is unsorted in relation to last offset of " << last_offset << ". Provider: " << provider_name << "."; DCHECK_LT(i->offset, text.length()) << " Classification of [" << i->offset << "," << text.length() << "] is out of bounds for \"" << text << "\". Provider: " << provider_name << "."; last_offset = i->offset; } } #endif