blob: ef5ebbe58df944d837e6c15f2ba4aa3e3a76c48d [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/base_search_provider.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/omnibox/browser/actions/omnibox_action_in_suggest.h"
#include "components/omnibox/browser/actions/omnibox_answer_action.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/remote_suggestions_service.h"
#include "components/omnibox/browser/search_scoring_signals_annotator.h"
#include "components/omnibox/browser/suggestion_answer.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search/search.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/metrics_proto/omnibox_input_type.pb.h"
#include "third_party/omnibox_proto/navigational_intent.pb.h"
#include "third_party/omnibox_proto/rich_answer_template.pb.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace {
constexpr bool is_android = !!BUILDFLAG(IS_ANDROID);
constexpr bool is_ios = !!BUILDFLAG(IS_IOS);
bool MatchTypeAndContentsAreEqual(const AutocompleteMatch& lhs,
const AutocompleteMatch& rhs) {
return lhs.contents == rhs.contents && lhs.type == rhs.type;
}
std::u16string GetMatchContentsForOnDeviceTailSuggestion(
const std::u16string& input_text,
const std::u16string& sanitized_suggestion) {
std::u16string sanitized_input;
base::TrimWhitespace(input_text, base::TRIM_TRAILING, &sanitized_input);
sanitized_input = AutocompleteMatch::SanitizeString(sanitized_input);
if (!base::StartsWith(sanitized_suggestion, sanitized_input,
base::CompareCase::SENSITIVE)) {
return sanitized_suggestion;
}
// If there is no space inside the suggestion, show the entire suggestion in
// UI. Otherwise replace the completed prefix of the suggestion with tail UI
// symbols e.g. "...".
// Examples (input/suggestion -> result):
// 1. [googl]/[google] -> [google]
// 2. [google]/[google map] -> [google map]
// 3. [google ma]/[google map login] -> [...map login]
// 4. [google map ]/[google map login] -> [...map login]
size_t suggestion_last_space_index =
sanitized_suggestion.find_last_of(base::kWhitespaceUTF16);
size_t input_last_space_index =
sanitized_input.find_last_of(base::kWhitespaceUTF16);
if (suggestion_last_space_index == std::u16string::npos ||
input_last_space_index == std::u16string::npos) {
return sanitized_suggestion;
}
size_t start_index = input_last_space_index + 1;
return sanitized_suggestion.substr(start_index);
}
} // namespace
using OEP = metrics::OmniboxEventProto;
BaseSearchProvider::BaseSearchProvider(AutocompleteProvider::Type type,
AutocompleteProviderClient* client)
: AutocompleteProvider(type), client_(client) {}
// static
bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) {
// TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
// non-debugging purposes.
return match.GetAdditionalInfoForDebugging(kShouldPrefetchKey) == kTrue;
}
// static
bool BaseSearchProvider::ShouldPrerender(const AutocompleteMatch& match) {
// TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
// non-debugging purposes.
return match.GetAdditionalInfoForDebugging(kShouldPrerenderKey) == kTrue;
}
// static
AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion(
AutocompleteProvider* autocomplete_provider,
const AutocompleteInput& input,
const bool in_keyword_mode,
const SearchSuggestionParser::SuggestResult& suggestion,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
int accepted_suggestion,
bool append_extra_query_params_from_command_line) {
AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false,
suggestion.type());
if (!template_url)
return match;
match.keyword = template_url->keyword();
// If SuggestTemplateInfo is available, use it. Otherwise, continue
// populating information from EntityInfo.
const auto& suggest_template_info = suggestion.suggest_template_info();
if (suggest_template_info) {
match.suggest_template = suggest_template_info;
if (suggest_template_info->has_image()) {
match.image_dominant_color =
suggest_template_info->image().dominant_color();
match.image_url = GURL(suggest_template_info->image().url());
}
} else {
match.image_dominant_color = suggestion.entity_info().dominant_color();
match.image_url = GURL(suggestion.entity_info().image_url());
}
match.entity_id = suggestion.entity_info().entity_id();
match.website_uri = suggestion.entity_info().website_uri();
match.contents = suggestion.match_contents();
match.contents_class = suggestion.match_contents_class();
if (OmniboxFieldTrial::kAnswerActionsShowRichCard.Get() &&
suggestion.answer_template() &&
suggestion.answer_template()->enhancements().enhancements().size() > 0) {
match.suggestion_group_id = omnibox::GROUP_MOBILE_RICH_ANSWER;
} else {
match.suggestion_group_id = suggestion.suggestion_group_id();
}
match.answer_template = suggestion.answer_template();
match.answer_type = suggestion.answer_type();
match.suggest_type = suggestion.suggest_type();
for (const int subtype : suggestion.subtypes()) {
match.subtypes.insert(SuggestSubtypeForNumber(subtype));
}
if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) {
match.RecordAdditionalInfo(kACMatchPropertySuggestionText,
suggestion.suggestion());
match.RecordAdditionalInfo(kACMatchPropertyContentsPrefix,
suggestion.match_contents_prefix());
match.RecordAdditionalInfo(
kACMatchPropertyContentsStartIndex,
static_cast<int>(suggestion.suggestion().length() -
match.contents.length()));
}
if (!suggestion.annotation().empty()) {
match.description = suggestion.annotation();
// Descriptions should always have dimmed text.
AutocompleteMatch::AddLastClassificationIfNecessary(
&match.description_class, 0, ACMatchClassification::DIM);
}
const std::u16string input_text = input.IsZeroSuggest() ? u"" : input.text();
const std::u16string input_lower = base::i18n::ToLower(input_text);
// suggestion.match_contents() should have already been collapsed.
match.allowed_to_be_default_match =
(!in_keyword_mode || suggestion.from_keyword()) &&
(base::CollapseWhitespace(input_lower, false) ==
base::i18n::ToLower(suggestion.match_contents()));
if (suggestion.from_keyword())
match.from_keyword = true;
// We only allow inlinable navsuggestions that were received before the
// last keystroke because we don't want asynchronous inline autocompletions.
if (!input.prevent_inline_autocomplete() &&
!suggestion.received_after_last_keystroke() &&
(!in_keyword_mode || suggestion.from_keyword()) &&
!input.IsZeroSuggest() &&
base::StartsWith(base::i18n::ToLower(suggestion.suggestion()),
input_lower, base::CompareCase::SENSITIVE)) {
match.inline_autocompletion =
suggestion.suggestion().substr(input_text.length());
match.allowed_to_be_default_match = true;
}
const TemplateURLRef& search_url = template_url->url_ref();
DCHECK(search_url.SupportsReplacement(search_terms_data));
std::u16string query(suggestion.suggestion());
std::u16string original_query(input_text);
if (suggestion.type() == AutocompleteMatchType::CALCULATOR) {
// Use query text, rather than the calculator answer suggestion, to search.
query = original_query;
original_query.clear();
}
match.fill_into_edit = GetFillIntoEdit(suggestion, template_url);
match.search_terms_args =
std::make_unique<TemplateURLRef::SearchTermsArgs>(query);
match.search_terms_args->request_source = input.request_source();
match.search_terms_args->original_query = original_query;
match.search_terms_args->accepted_suggestion = accepted_suggestion;
if (suggest_template_info) {
match.search_terms_args->additional_query_params =
CreateQueryParamStringFromMap(
suggest_template_info->default_search_parameters());
} else {
match.search_terms_args->additional_query_params =
suggestion.entity_info().suggest_search_parameters();
}
match.search_terms_args->append_extra_query_params_from_command_line =
append_extra_query_params_from_command_line;
// Must be set for deduplication and navigation. AutocompleteController will
// ultimately overwrite this with the searchbox stats before navigation.
match.destination_url = GURL(search_url.ReplaceSearchTerms(
*match.search_terms_args, search_terms_data));
// Search results don't look like URLs.
match.transition = suggestion.from_keyword() ? ui::PAGE_TRANSITION_KEYWORD
: ui::PAGE_TRANSITION_GENERATED;
bool is_google = search::TemplateURLIsGoogle(template_url, search_terms_data);
// Attach Actions in Suggest to the newly created match on Android if Google
// is the default search engine.
if ((is_android || is_ios) && is_google) {
if (suggest_template_info &&
suggest_template_info->action_suggestions_size() > 0) {
for (const omnibox::SuggestTemplateInfo_TemplateAction& action :
suggest_template_info->action_suggestions()) {
match.actions.emplace_back(CreateActionInSuggest(
action, search_url, *match.search_terms_args, search_terms_data));
}
} else {
// TODO(crbug.com/417745802): Remove once actions are migrated from
// EntityInfo to SuggestTemplateInfo.
for (const omnibox::ActionInfo& action_info :
suggestion.entity_info().action_suggestions()) {
omnibox::SuggestTemplateInfo::TemplateAction template_action;
template_action.set_action_uri(action_info.action_uri());
template_action.set_logs_action_type(action_info.logs_action_type());
template_action.set_action_type(
static_cast<omnibox::SuggestTemplateInfo_TemplateAction_ActionType>(
action_info.action_type()));
*template_action.mutable_search_parameters() =
action_info.search_parameters();
match.actions.emplace_back(
CreateActionInSuggest(template_action, search_url,
*match.search_terms_args, search_terms_data));
}
}
}
if (is_android && is_google && suggestion.answer_template()) {
std::ranges::transform(
suggestion.answer_template()->enhancements().enhancements(),
std::back_inserter(match.actions),
[&](const omnibox::SuggestionEnhancement& enhancement) {
return CreateAnswerAction(enhancement, *match.search_terms_args,
suggestion.answer_type());
});
}
match.navigational_intent = suggestion.navigational_intent();
return match;
}
scoped_refptr<OmniboxAction> BaseSearchProvider::CreateActionInSuggest(
omnibox::SuggestTemplateInfo::TemplateAction template_action,
const TemplateURLRef& search_url,
const TemplateURLRef::SearchTermsArgs& original_search_terms_args,
const SearchTermsData& search_terms_data) {
std::optional<TemplateURLRef::SearchTermsArgs> action_search_terms_args;
// If the Action's URL is empty, but the Action supplies additional search
// parameters, compute new URL based on the base URL (that is specific to
// the entire suggestion).
if (template_action.action_uri().empty() &&
!template_action.search_parameters().empty()) {
action_search_terms_args = original_search_terms_args;
action_search_terms_args->additional_query_params =
CreateQueryParamStringFromMap(template_action.search_parameters());
}
return base::MakeRefCounted<OmniboxActionInSuggest>(
std::move(template_action), std::move(action_search_terms_args));
}
// static
scoped_refptr<OmniboxAction> BaseSearchProvider::CreateAnswerAction(
omnibox::SuggestionEnhancement enhancement,
TemplateURLRef::SearchTermsArgs search_terms_args,
omnibox::AnswerType answer_type) {
// Define actions destination URL.
search_terms_args.additional_query_params =
CreateQueryParamStringFromMap(enhancement.query_cgi_params());
search_terms_args.search_terms = base::UTF8ToUTF16(enhancement.query());
return base::MakeRefCounted<OmniboxAnswerAction>(
std::move(enhancement), search_terms_args, answer_type);
}
// static
std::string BaseSearchProvider::CreateQueryParamStringFromMap(
const google::protobuf::Map<std::string, std::string>& query_param_map) {
std::string query_params_string;
for (const auto& param : query_param_map) {
// Supply additional Query Parameters as instructed by the provider.
if (!query_params_string.empty()) {
query_params_string += '&';
}
query_params_string += param.first + "=" + param.second;
}
return query_params_string;
}
// static
AutocompleteMatch BaseSearchProvider::CreateShortcutSearchSuggestion(
const std::u16string& suggestion,
AutocompleteMatchType::Type type,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data) {
// These calls use a number of default values. For instance, they assume the
// caller knows what it's doing and we set this match to look as if it was
// received/created synchronously.
SearchSuggestionParser::SuggestResult suggest_result(
suggestion, type, /*suggest_type=*/omnibox::TYPE_NATIVE_CHROME,
/*subtypes=*/{}, /*from_keyword=*/false,
/*navigational_intent=*/omnibox::NAV_INTENT_NONE,
/*relevance=*/0, /*relevance_from_server=*/false,
/*input_text=*/std::u16string());
suggest_result.set_received_after_last_keystroke(false);
return CreateSearchSuggestion(nullptr, AutocompleteInput(),
/*in_keyword_mode=*/false, suggest_result,
template_url, search_terms_data, 0, false);
}
// static
AutocompleteMatch BaseSearchProvider::CreateOnDeviceSearchSuggestion(
AutocompleteProvider* autocomplete_provider,
const AutocompleteInput& input,
const std::u16string& suggestion,
int relevance,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
int accepted_suggestion,
bool is_tail_suggestion) {
AutocompleteMatchType::Type match_type;
omnibox::SuggestType suggest_type = omnibox::TYPE_NATIVE_CHROME;
std::u16string match_contents, match_contents_prefix;
if (is_tail_suggestion) {
match_type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
suggest_type = omnibox::TYPE_TAIL;
std::u16string sanitized_suggestion =
AutocompleteMatch::SanitizeString(suggestion);
match_contents = GetMatchContentsForOnDeviceTailSuggestion(
input.text(), sanitized_suggestion);
DCHECK_GE(sanitized_suggestion.size(), match_contents.size());
match_contents_prefix = sanitized_suggestion.substr(
0, sanitized_suggestion.size() - match_contents.size());
} else {
match_type = AutocompleteMatchType::SEARCH_SUGGEST;
suggest_type = omnibox::TYPE_QUERY;
match_contents = suggestion;
}
SearchSuggestionParser::SuggestResult suggest_result(
suggestion, match_type, suggest_type,
/*subtypes=*/{omnibox::SUBTYPE_SUGGEST_2G_LITE}, match_contents,
match_contents_prefix,
/*annotation=*/std::u16string(),
/*entity_info=*/omnibox::EntityInfo(),
/*deletion_url=*/"",
/*from_keyword=*/false,
/*navigational_intent=*/omnibox::NAV_INTENT_NONE, relevance,
/*relevance_from_server=*/false,
/*should_prefetch=*/false,
/*should_prerender=*/false,
base::CollapseWhitespace(input.text(), false));
// On device providers are asynchronous.
suggest_result.set_received_after_last_keystroke(true);
return CreateSearchSuggestion(
autocomplete_provider, input, /*in_keyword_mode=*/false, suggest_result,
template_url, search_terms_data, accepted_suggestion,
/*append_extra_query_params_from_command_line=*/true);
}
// static
bool BaseSearchProvider::PageURLIsEligibleForSuggestRequest(
const GURL& page_url,
metrics::OmniboxEventProto::PageClassification page_classification) {
return page_url.is_valid() && page_url.SchemeIsHTTPOrHTTPS() &&
!omnibox::IsNTPPage(page_classification);
}
// static
bool BaseSearchProvider::CanSendSuggestRequest(
metrics::OmniboxEventProto::PageClassification page_classification,
const TemplateURL* template_url,
const AutocompleteProviderClient* client) {
if (!template_url || template_url->suggestions_url().empty()) {
return false;
}
// Setting SuggestUrl the same as SearchUrl is a typical misconfiguration.
// It's not possible for a URL to both provide a search results page and
// suggested queries response (at least they have different format). Most
// like the user set the search URL correctly; it would be obvious if they did
// not. Thus, it's likely that the suggest URL is wrong. Because it would not
// give a valid query suggestion response, don't bother sending queries to it
// (otherwise user will quickly hit rate-limit for search queries, that will
// harm valid search queries as well).
if (template_url->suggestions_url() == template_url->url()) {
return false;
}
// Don't make a suggest request if in incognito mode; unless for the Lens
// searchboxes.
if (client->IsOffTheRecord() &&
!omnibox::IsLensSearchbox(page_classification)) {
return false;
}
// Don't make a suggest request if suggest is not enabled; unless for the Lens
// searchboxes.
if (!client->SearchSuggestEnabled() &&
!omnibox::IsLensSearchbox(page_classification)) {
return false;
}
return true;
}
// static
bool BaseSearchProvider::CanSendSecureSuggestRequest(
metrics::OmniboxEventProto::PageClassification page_classification,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
const AutocompleteProviderClient* client) {
if (!CanSendSuggestRequest(page_classification, template_url, client)) {
return false;
}
// Make sure we are sending the suggest request through a cryptographically
// secure channel to prevent exposing the current page URL or personalized
// results without encryption.
const GURL& suggest_url =
template_url->GenerateSuggestionURL(search_terms_data);
if (!suggest_url.is_valid() || !suggest_url.SchemeIsCryptographic()) {
return false;
}
// Don't make a suggest request if Google is not the default search engine.
// Note that currently only the pre-populated Google search provider supports
// zero-prefix suggestions. If other pre-populated search engines decide to
// support it, revise this test accordingly.
if (!search::TemplateURLIsGoogle(template_url, search_terms_data)) {
return false;
}
return true;
}
// static
bool BaseSearchProvider::CanSendSuggestRequestWithPageURL(
const GURL& current_page_url,
metrics::OmniboxEventProto::PageClassification page_classification,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
const AutocompleteProviderClient* client) {
if (!CanSendSecureSuggestRequest(page_classification, template_url,
search_terms_data, client)) {
return false;
}
// Forbid sending the current page URL to the suggest endpoint if
// URL data collection is off; unless the current page is the provider's
// Search Results Page; or for the Lens searchboxes.
if (!client->IsUrlDataCollectionActive() &&
!template_url->IsSearchURL(current_page_url, search_terms_data) &&
!omnibox::IsLensSearchbox(page_classification)) {
return false;
}
return true;
}
void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) {
DCHECK(match.deletable);
// TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
// non-debugging purposes.
if (!match.GetAdditionalInfoForDebugging(BaseSearchProvider::kDeletionUrlKey)
.empty()) {
// Remote personalized suggestions in OTR contexts are not OK.
DCHECK(!client_->IsOffTheRecord());
deletion_loaders_.push_back(
client()
->GetRemoteSuggestionsService(/*create_if_necessary=*/true)
->StartDeletionRequest(
match.GetAdditionalInfoForDebugging(
BaseSearchProvider::kDeletionUrlKey),
/*is_off_the_record=*/false,
base::BindOnce(&BaseSearchProvider::OnDeletionComplete,
base::Unretained(this))));
}
const TemplateURL* template_url =
match.GetTemplateURL(client_->GetTemplateURLService(), false);
// This may be nullptr if the template corresponding to the keyword has been
// deleted or there is no keyword set.
if (template_url != nullptr) {
client_->DeleteMatchingURLsForKeywordFromHistory(template_url->id(),
match.contents);
}
// Immediately update the list of matches to show the match was deleted,
// regardless of whether the server request actually succeeds.
DeleteMatchFromMatches(match);
}
void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
new_entry.set_provider(AsOmniboxEventProviderType());
new_entry.set_provider_done(done_);
}
// static
const char BaseSearchProvider::kRelevanceFromServerKey[] =
"relevance_from_server";
const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch";
const char BaseSearchProvider::kShouldPrerenderKey[] = "should_prerender";
const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url";
const char BaseSearchProvider::kTrue[] = "true";
const char BaseSearchProvider::kFalse[] = "false";
BaseSearchProvider::~BaseSearchProvider() = default;
// static
std::u16string BaseSearchProvider::GetFillIntoEdit(
const SearchSuggestionParser::SuggestResult& suggest_result,
const TemplateURL* template_url) {
std::u16string fill_into_edit;
if (suggest_result.from_keyword())
fill_into_edit.append(template_url->keyword() + u' ');
fill_into_edit.append(suggest_result.suggestion());
return fill_into_edit;
}
void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url,
AutocompleteMatch* match) {
if (deletion_url.empty())
return;
TemplateURLService* template_url_service = client_->GetTemplateURLService();
if (!template_url_service ||
!template_url_service->GetDefaultSearchProvider())
return;
GURL url =
template_url_service->GetDefaultSearchProvider()->GenerateSearchURL(
template_url_service->search_terms_data());
url = url.DeprecatedGetOriginAsURL().Resolve(deletion_url);
if (url.is_valid()) {
match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey,
url.spec());
match->deletable = true;
}
}
void BaseSearchProvider::AddMatchToMap(
const SearchSuggestionParser::SuggestResult& result,
const AutocompleteInput& input,
const TemplateURL* template_url,
const SearchTermsData& search_terms_data,
int accepted_suggestion,
bool mark_as_deletable,
bool in_keyword_mode,
MatchMap* map) {
AutocompleteMatch match = CreateSearchSuggestion(
this, input, in_keyword_mode, result, template_url, search_terms_data,
accepted_suggestion, ShouldAppendExtraParams(result));
if (!match.destination_url.is_valid())
return;
if (match.IsSearchAimSuggestion() &&
(!omnibox_feature_configs::AiMode::Get().allow_ai_mode_matches ||
!omnibox::IsAimAllowedByPolicy(client_->GetPrefs()))) {
return;
}
match.RecordAdditionalInfo(kRelevanceFromServerKey,
result.relevance_from_server() ? kTrue : kFalse);
match.RecordAdditionalInfo(kShouldPrefetchKey,
result.should_prefetch() ? kTrue : kFalse);
match.RecordAdditionalInfo(kShouldPrerenderKey,
result.should_prerender() ? kTrue : kFalse);
SetDeletionURL(result.deletion_url(), &match);
if (mark_as_deletable)
match.deletable = true;
// Only set scoring signals for eligible matches.
if (match.IsMlSignalLoggingEligible()) {
// Initialize the ML scoring signals for this suggestion if needed.
if (!match.scoring_signals) {
match.scoring_signals = std::make_optional<ScoringSignals>();
}
if (result.relevance_from_server()) {
match.scoring_signals->set_search_suggest_relevance(result.relevance());
}
SearchScoringSignalsAnnotator::UpdateMatchTypeScoringSignals(match,
input.text());
}
// Try to add `match` to `map`.
// NOTE: Keep this ToLower() call in sync with url_database.cc.
MatchKey match_key(
std::make_tuple(base::i18n::ToLower(result.suggestion()),
match.search_terms_args->additional_query_params));
const std::pair<MatchMap::iterator, bool> i(
map->insert(std::make_pair(match_key, match)));
if (i.second) {
auto& added_match = i.first->second;
// If the newly added match has non-empty additional query params and
// another match with the same search terms and a unique non-empty
// additional query params is already present in the map, proactively set
// `stripped_destination_url` to be the same as `destination_url`.
// Otherwise, `stripped_destination_url` will later be set by
// `AutocompleteResult::ComputeStrippedDestinationURL()` which strips away
// the additional query params from `destination_url` leaving only the
// search terms. That would result in these matches to be erroneously
// deduped despite having unique additional query params.
// Note that the match previously added to the map will continue to get the
// typical `stripped_destination_url` allowing it to be deduped with the
// plain-text matches (i.e., with no additional query params) as expected.
const auto& added_match_query = std::get<0>(match_key);
const auto& added_match_query_params = std::get<1>(match_key);
if (!added_match_query_params.empty()) {
for (const auto& entry : *map) {
const auto& existing_match_query = std::get<0>(entry.first);
const auto& existing_match_query_params = std::get<1>(entry.first);
if (existing_match_query == added_match_query &&
!existing_match_query_params.empty() &&
existing_match_query_params != added_match_query_params) {
added_match.stripped_destination_url = added_match.destination_url;
break;
}
}
}
} else {
auto& existing_match = i.first->second;
// If a duplicate match is already in the map, replace it with `match` if it
// is more relevant.
// NOTE: We purposefully do a direct relevance comparison here instead of
// using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items
// added first" rather than "items alphabetically first" when the scores
// are equal. The only case this matters is when a user has results with
// the same score that differ only by capitalization; because the history
// system returns results sorted by recency, this means we'll pick the most
// recent such result even if the precision of our relevance score is too
// low to distinguish the two.
if (match.relevance > existing_match.relevance) {
match.duplicate_matches.insert(match.duplicate_matches.end(),
existing_match.duplicate_matches.begin(),
existing_match.duplicate_matches.end());
existing_match.duplicate_matches.clear();
match.duplicate_matches.push_back(existing_match);
existing_match = std::move(match);
} else {
if (match.keyword == existing_match.keyword) {
// Old and new matches are from the same search provider. It is okay to
// record one match's prefetch/prerender data onto a different match
// (for the same query string) for the following reasons:
// 1. Because the suggest server only sends down a query string from
// which we construct a URL, rather than sending a full URL, and because
// we construct URLs from query strings in the same way every time, the
// URLs for the two matches will be the same. Therefore, we won't end up
// prefetching/prerendering something the server didn't intend.
// 2. Presumably the server sets the prefetch/prerender bit on a match
// it thinks is sufficiently relevant that the user is likely to choose
// it. Surely setting the prefetch/prerender bit on a match of even
// higher relevance won't violate this assumption.
const bool should_prefetch =
result.should_prefetch() || ShouldPrefetch(existing_match);
existing_match.RecordAdditionalInfo(kShouldPrefetchKey,
should_prefetch ? kTrue : kFalse);
const bool should_prerender =
result.should_prerender() || ShouldPrerender(existing_match);
existing_match.RecordAdditionalInfo(kShouldPrerenderKey,
should_prerender ? kTrue : kFalse);
}
existing_match.duplicate_matches.push_back(std::move(match));
}
// Copy over necessary fields from the lower-ranking duplicate. Note that
// this requires the lower-ranking duplicate being added last. See the use
// of push_back above:
// This is to avoid losing the Answers in Suggest information.
const auto& less_relevant_duplicate_match =
existing_match.duplicate_matches.back();
if (less_relevant_duplicate_match.answer_template &&
!existing_match.answer_template) {
existing_match.actions = less_relevant_duplicate_match.actions;
existing_match.answer_template =
less_relevant_duplicate_match.answer_template;
existing_match.answer_type = less_relevant_duplicate_match.answer_type;
if (OmniboxFieldTrial::kAnswerActionsShowRichCard.Get()) {
existing_match.suggestion_group_id =
less_relevant_duplicate_match.suggestion_group_id;
}
}
// This is to avoid having shopping categorical queries lose their images to
// higher-relevance local history and verbatim matches. This works for the
// shopping categorical queries because they only provide images at the
// moment. That assumption may not hold in the future.
// Ideally the entire `entity_info`, when available on a suggestion, should
// be copied over. However `entity_info` is broken down to its constituents
// in the constructor of SearchSuggestionParser::SuggestResult and used to
// set individual fields on the AutocompleteMatch. This is in contrast to
// Answers in Suggest which is kept on the match in its entirety. This is
// partly because the entity name is used to set and classify the match
// contents. Ideally `entity_info` should also be kept on the match in its
// entirety so it can be carried over when deduplicating the matches here or
// later in the Autocomplete process.
// TODO(crbug.com/40276602): rework how `entity_info` is used in the match.
if (base::FeatureList::IsEnabled(omnibox::kCategoricalSuggestions)) {
if (!less_relevant_duplicate_match.image_url.is_empty() &&
existing_match.image_url.is_empty()) {
existing_match.image_url = less_relevant_duplicate_match.image_url;
}
}
// This is to avoid having shopping categorical queries lose their subtypes
// to higher-relevance local history and verbatim matches. The subtypes are
// sent to the backend in the ChromeSearchboxStats proto via the gs_lcrp=
// param when the user selects a suggestion. The subtypes may be used to
// identify what the user selected so they can be suggested the next time,
// i.e., if the user selects a decorated suggestion - which is accompanied
// by specific subtypes - we want to show a decorated suggestion next time.
if (base::FeatureList::IsEnabled(omnibox::kCategoricalSuggestions)) {
existing_match.subtypes.insert(
less_relevant_duplicate_match.subtypes.begin(),
less_relevant_duplicate_match.subtypes.end());
}
// This is to avoid having `stripped_destination_url` being later set by
// `AutocompleteResult::ComputeStrippedDestinationURL()` which strips away
// the additional query params from `destination_url` leaving only the
// search terms. That would result in these matches to be erroneously
// deduped despite having unique additional query params.
if (!less_relevant_duplicate_match.stripped_destination_url.is_empty() &&
existing_match.stripped_destination_url.is_empty()) {
existing_match.stripped_destination_url =
less_relevant_duplicate_match.stripped_destination_url;
}
}
}
void BaseSearchProvider::DeleteMatchFromMatches(
const AutocompleteMatch& match) {
for (auto i(matches_.begin()); i != matches_.end(); ++i) {
// Find the desired match to delete by checking the type and contents.
// We can't check the destination URL, because the autocomplete controller
// may have reformulated that. Not that while checking for matching
// contents works for personalized suggestions, if more match types gain
// deletion support, this algorithm may need to be re-examined.
if (MatchTypeAndContentsAreEqual(match, *i)) {
matches_.erase(i);
break;
}
// Handle the case where the deleted match is only found within the
// duplicate_matches sublist.
std::vector<AutocompleteMatch>& duplicates = i->duplicate_matches;
auto it =
std::remove_if(duplicates.begin(), duplicates.end(),
[&match](const AutocompleteMatch& duplicate) {
return MatchTypeAndContentsAreEqual(match, duplicate);
});
if (it != duplicates.end()) {
duplicates.erase(it, duplicates.end());
break;
}
}
}
void BaseSearchProvider::OnDeletionComplete(
const network::SimpleURLLoader* source,
const int response_code,
std::unique_ptr<std::string> response_body) {
RecordDeletionResult(response_code == 200);
std::erase_if(
deletion_loaders_,
[source](const std::unique_ptr<network::SimpleURLLoader>& loader) {
return loader.get() == source;
});
}