blob: a0770c9c69e6128c2c5a5952dd1f4d2542d243af [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/search_suggestion_parser.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>
#include "base/base64.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/feature_list.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/omnibox/browser/autocomplete_i18n.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match_classification.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/suggestion_group_util.h"
#include "components/omnibox/browser/url_prefix.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_fixer.h"
#include "components/url_formatter/url_formatter.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/omnibox_proto/entity_info.pb.h"
#include "third_party/omnibox_proto/navigational_intent.pb.h"
#include "third_party/omnibox_proto/rich_suggest_template.pb.h"
#include "third_party/omnibox_proto/suggest_template_info.pb.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/url_constants.h"
namespace {
// Converts a suggestion type name found in the JSON response to an equivalent
// omnibox::SuggestType enum value.
omnibox::SuggestType GetSuggestType(const std::string& type) {
if (type == "CALCULATOR") {
return omnibox::TYPE_CALCULATOR;
}
if (type == "ENTITY") {
return omnibox::TYPE_ENTITY;
}
if (type == "TAIL") {
return omnibox::TYPE_TAIL;
}
if (type == "PERSONALIZED_QUERY") {
return omnibox::TYPE_PERSONALIZED_QUERY;
}
if (type == "PROFILE") {
return omnibox::TYPE_PROFILE;
}
if (type == "NAVIGATION") {
return omnibox::TYPE_NAVIGATION;
}
if (type == "PERSONALIZED_NAVIGATION") {
return omnibox::TYPE_PERSONALIZED_NAVIGATION;
}
if (type == "CHROME_QUERY_TILES") {
return omnibox::TYPE_CHROME_QUERY_TILES;
}
if (type == "CATEGORICAL_QUERY") {
return omnibox::TYPE_CATEGORICAL_QUERY;
}
return omnibox::TYPE_QUERY;
}
// Converts an omnibox::SuggestType enum value to an equivalent
// AutocompleteMatchType::Type enum values.
AutocompleteMatchType::Type GetAutocompleteMatchType(
omnibox::SuggestType suggest_type) {
switch (suggest_type) {
case omnibox::TYPE_CALCULATOR:
return AutocompleteMatchType::CALCULATOR;
case omnibox::TYPE_ENTITY:
return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY;
case omnibox::TYPE_TAIL:
return AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
case omnibox::TYPE_PERSONALIZED_QUERY:
return AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED;
case omnibox::TYPE_PROFILE:
return AutocompleteMatchType::SEARCH_SUGGEST_PROFILE;
case omnibox::TYPE_NAVIGATION:
return AutocompleteMatchType::NAVSUGGEST;
case omnibox::TYPE_PERSONALIZED_NAVIGATION:
return AutocompleteMatchType::NAVSUGGEST_PERSONALIZED;
default: {
// Use `ACMatchType::SEARCH_SUGGEST_ENTITY` for categorical suggestions.
if (suggest_type == omnibox::TYPE_CATEGORICAL_QUERY &&
base::FeatureList::IsEnabled(omnibox::kCategoricalSuggestions)) {
return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY;
}
return AutocompleteMatchType::SEARCH_SUGGEST;
}
}
}
// Convert the supplied Json::Value representation of list-of-lists-of-integers
// to a vector-of-vecrors-of-integers, containing (ideally) one vector of
// integers per match.
// The logic here does not validate if the length of top level vector is same as
// number of returned matches and will supply empty vector for any item that is
// either invalid or missing.
// The function will always return a valid and properly sized vector of vectors,
// equal in length to `expected_size`, even if the input `subtypes_list` is not
// valid.
std::vector<std::vector<int>> ParseMatchSubtypes(
const base::Value::List* subtypes_list,
size_t expected_size) {
std::vector<std::vector<int>> result(expected_size);
if (subtypes_list == nullptr) {
return result;
}
if (!subtypes_list->empty() && subtypes_list->size() != expected_size) {
LOG(WARNING) << "The length of reported subtypes (" << subtypes_list->size()
<< ") does not match the expected length (" << expected_size
<< ')';
}
const auto num_items = std::min(expected_size, subtypes_list->size());
for (auto index = 0u; index < num_items; index++) {
const auto& subtypes_item = (*subtypes_list)[index];
// Permissive: ignore subtypes that are not in a form of a list.
if (!subtypes_item.is_list())
continue;
const auto& subtype_list = subtypes_item.GetList();
auto& result_subtypes = result[index];
result_subtypes.reserve(subtype_list.size());
for (const auto& subtype : subtype_list) {
// Permissive: Skip over any item that is not an integer.
if (!subtype.is_int())
continue;
result_subtypes.emplace_back(subtype.GetInt());
}
}
return result;
}
std::string FindStringOrEmpty(const base::Value::Dict& value, std::string key) {
auto* ptr = value.FindString(key);
return ptr ? *ptr : "";
}
// The field number for the experiment stat type specified as an int
// in ExperimentStatsV2.
constexpr char kTypeIntFieldNumber[] = "4";
// The field number for the string value in ExperimentStatsV2.
constexpr char kStringValueFieldNumber[] = "2";
constexpr auto kReservedReservedGroupSectionsMap =
base::MakeFixedFlatMap<int, omnibox::GroupSection>(
{{0, omnibox::SECTION_REMOTE_ZPS_1},
{1, omnibox::SECTION_REMOTE_ZPS_2},
{2, omnibox::SECTION_REMOTE_ZPS_3},
{3, omnibox::SECTION_REMOTE_ZPS_4},
{4, omnibox::SECTION_REMOTE_ZPS_5},
{5, omnibox::SECTION_REMOTE_ZPS_6},
{6, omnibox::SECTION_REMOTE_ZPS_7},
{7, omnibox::SECTION_REMOTE_ZPS_8},
{8, omnibox::SECTION_REMOTE_ZPS_9},
{9, omnibox::SECTION_REMOTE_ZPS_10}});
// Converts the given 0-based index of a group in the server response to a group
// section known to Chrome.
omnibox::GroupSection ChromeGroupSectionForRemoteGroupIndex(
const int group_index) {
if (base::Contains(kReservedReservedGroupSectionsMap, group_index)) {
return kReservedReservedGroupSectionsMap.at(group_index);
} else {
// Return a default section if we don't have any reserved sections left.
return omnibox::SECTION_DEFAULT;
}
}
// Decodes a proto object from its serialized Base64 string representation.
template <typename T>
bool DecodeProtoFromBase64(const std::string* encoded_data, T& result_proto) {
if (!encoded_data || encoded_data->empty()) {
return false;
}
std::string decoded_data;
if (!base::Base64Decode(*encoded_data, &decoded_data)) {
return false;
}
if (decoded_data.empty()) {
return false;
}
if (!result_proto.ParseFromString(decoded_data)) {
return false;
}
return true;
}
// Format template image URLs that do not contain a scheme.
// The call to GetFormattedURL() will return the URL with a scheme added or
// return the same URL if no formatting is necessary.
void FormatAnswerTemplateImageURL(
omnibox::RichAnswerTemplate* answer_template) {
if (!(answer_template->answers_size() > 0)) {
return;
}
std::string* url_string =
answer_template->mutable_answers(0)->mutable_image()->mutable_url();
answer_template->mutable_answers(0)->mutable_image()->set_url(
omnibox::answer_data_parser::GetFormattedURL(url_string).spec());
}
} // namespace
omnibox::SuggestSubtype SuggestSubtypeForNumber(int value) {
// Note that ideally this should first check if `value` is valid by calling
// omnibox::SuggestSubtype_IsValid and return omnibox::SUBTYPE_NONE when there
// is no corresponding enum object. However, that is not possible because the
// current list of subtypes in omnibox::SuggestSubtype is not exhaustive.
// However, casting int values into omnibox::SuggestSubtype without testing
// membership is expected to be safe as omnibox::SuggestSubtype has a fixed
// int underlying type.
return static_cast<omnibox::SuggestSubtype>(value);
}
omnibox::NavigationalIntent NavigationalIntentForNumber(int value) {
if (omnibox::NavigationalIntent_IsValid(value)) {
return static_cast<omnibox::NavigationalIntent>(value);
}
return omnibox::NavigationalIntent::NAV_INTENT_NONE;
}
omnibox::AnswerType AnswerTypeForNumber(int value) {
if (omnibox::AnswerType_IsValid(value)) {
return static_cast<omnibox::AnswerType>(value);
}
return omnibox::ANSWER_TYPE_UNSPECIFIED;
}
// SearchSuggestionParser::Result ----------------------------------------------
SearchSuggestionParser::Result::Result(
bool from_keyword,
int relevance,
bool relevance_from_server,
AutocompleteMatchType::Type type,
omnibox::SuggestType suggest_type,
std::vector<int> subtypes,
const std::string& deletion_url,
omnibox::NavigationalIntent navigational_intent)
: from_keyword_(from_keyword),
type_(type),
suggest_type_(suggest_type),
subtypes_(std::move(subtypes)),
relevance_(relevance),
relevance_from_server_(relevance_from_server),
received_after_last_keystroke_(true),
deletion_url_(deletion_url),
navigational_intent_(navigational_intent) {}
SearchSuggestionParser::Result::Result(const Result& other) = default;
SearchSuggestionParser::Result::~Result() = default;
// SearchSuggestionParser::SuggestResult ---------------------------------------
SearchSuggestionParser::SuggestResult::SuggestResult(
const std::u16string& suggestion,
AutocompleteMatchType::Type type,
omnibox::SuggestType suggest_type,
std::vector<int> subtypes,
bool from_keyword,
omnibox::NavigationalIntent navigational_intent,
int relevance,
bool relevance_from_server,
const std::u16string& input_text)
: SuggestResult(suggestion,
type,
suggest_type,
std::move(subtypes),
suggestion,
/*match_contents_prefix=*/std::u16string(),
/*annotation=*/std::u16string(),
/*entity_info=*/omnibox::EntityInfo(),
/*deletion_url=*/"",
from_keyword,
navigational_intent,
relevance,
relevance_from_server,
/*should_prefetch=*/false,
/*should_prerender=*/false,
input_text) {}
SearchSuggestionParser::SuggestResult::SuggestResult(
const std::u16string& suggestion,
AutocompleteMatchType::Type type,
omnibox::SuggestType suggest_type,
std::vector<int> subtypes,
const std::u16string& match_contents,
const std::u16string& match_contents_prefix,
const std::u16string& annotation,
omnibox::EntityInfo entity_info,
const std::string& deletion_url,
bool from_keyword,
omnibox::NavigationalIntent navigational_intent,
int relevance,
bool relevance_from_server,
bool should_prefetch,
bool should_prerender,
const std::u16string& input_text)
: Result(from_keyword,
relevance,
relevance_from_server,
type,
suggest_type,
std::move(subtypes),
deletion_url,
navigational_intent),
suggestion_(suggestion),
match_contents_prefix_(match_contents_prefix),
entity_info_(std::move(entity_info)),
should_prefetch_(should_prefetch),
should_prerender_(should_prerender) {
annotation_ = !entity_info_.annotation().empty()
? base::UTF8ToUTF16(entity_info_.annotation())
: annotation;
match_contents_ = !entity_info_.name().empty()
? base::UTF8ToUTF16(entity_info_.name())
: match_contents;
match_contents_ = base::CollapseWhitespace(match_contents_, false);
DCHECK(!match_contents_.empty());
ClassifyMatchContents(true, input_text);
}
SearchSuggestionParser::SuggestResult::SuggestResult(
const SuggestResult& result) = default;
SearchSuggestionParser::SuggestResult::~SuggestResult() = default;
SearchSuggestionParser::SuggestResult&
SearchSuggestionParser::SuggestResult::operator=(const SuggestResult& rhs) =
default;
void SearchSuggestionParser::SuggestResult::ClassifyMatchContents(
const bool allow_bolding_all,
const std::u16string& input_text) {
DCHECK(!match_contents_.empty());
// In case of zero-suggest results, do not highlight matches.
if (input_text.empty()) {
match_contents_class_ = {
ACMatchClassification(0, ACMatchClassification::NONE)};
return;
}
std::u16string lookup_text = input_text;
if (type_ == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) {
const size_t contents_index =
suggestion_.length() - match_contents_.length();
// Ensure the query starts with the input text, and ends with the match
// contents, and the input text has an overlap with contents.
if (base::StartsWith(suggestion_, input_text,
base::CompareCase::SENSITIVE) &&
base::EndsWith(suggestion_, match_contents_,
base::CompareCase::SENSITIVE) &&
(input_text.length() > contents_index)) {
lookup_text = input_text.substr(contents_index);
}
}
// Do a case-insensitive search for |lookup_text|.
auto lookup_result = std::ranges::search(match_contents_, lookup_text,
SimpleCaseInsensitiveCompareUCS2());
if (!allow_bolding_all && lookup_result.empty()) {
// Bail if the code below to update the bolding would bold the whole
// string. Note that the string may already be entirely bolded; if
// so, leave it as is.
return;
}
// Note we discard our existing match_contents_class_ with this call.
match_contents_class_ =
ClassifyAllMatchesInString(input_text, match_contents_, true);
}
void SearchSuggestionParser::SuggestResult::SetRichAnswerTemplate(
const omnibox::RichAnswerTemplate& answer_template) {
answer_template_ = answer_template;
}
void SearchSuggestionParser::SuggestResult::SetAnswerType(
const omnibox::AnswerType& answer_type) {
answer_type_ = answer_type;
}
void SearchSuggestionParser::SuggestResult::SetEntityInfo(
const omnibox::EntityInfo& entity_info) {
entity_info_ = entity_info;
}
void SearchSuggestionParser::SuggestResult::SetSuggestTemplateInfo(
const omnibox::SuggestTemplateInfo& suggest_template_info) {
suggest_template_info_ = suggest_template_info;
}
void SearchSuggestionParser::SuggestResult::SetMatchContents(
const std::u16string& match_contents) {
match_contents_ = match_contents;
}
void SearchSuggestionParser::SuggestResult::SetAnnotation(
const std::u16string& annotation) {
annotation_ = annotation;
}
int SearchSuggestionParser::SuggestResult::CalculateRelevance(
const AutocompleteInput& input,
bool keyword_provider_requested) const {
if (!from_keyword_ && keyword_provider_requested)
return 100;
return ((input.type() == metrics::OmniboxInputType::URL) ? 300 : 600);
}
// SearchSuggestionParser::NavigationResult ------------------------------------
SearchSuggestionParser::NavigationResult::NavigationResult(
const AutocompleteSchemeClassifier& scheme_classifier,
const GURL& url,
AutocompleteMatchType::Type match_type,
omnibox::SuggestType suggest_type,
std::vector<int> subtypes,
const std::u16string& description,
const std::string& deletion_url,
bool from_keyword,
omnibox::NavigationalIntent navigational_intent,
int relevance,
bool relevance_from_server,
const std::u16string& input_text)
: Result(from_keyword,
relevance,
relevance_from_server,
match_type,
suggest_type,
std::move(subtypes),
deletion_url,
navigational_intent),
url_(url),
formatted_url_(AutocompleteInput::FormattedStringWithEquivalentMeaning(
url,
url_formatter::FormatUrl(url,
url_formatter::kFormatUrlOmitDefaults &
~url_formatter::kFormatUrlOmitHTTP,
base::UnescapeRule::SPACES,
nullptr,
nullptr,
nullptr),
scheme_classifier,
nullptr)),
description_(description) {
DCHECK(url_.is_valid());
CalculateAndClassifyMatchContents(true, input_text);
ClassifyDescription(input_text);
}
SearchSuggestionParser::NavigationResult::NavigationResult(
const NavigationResult& other) = default;
SearchSuggestionParser::NavigationResult::~NavigationResult() = default;
void SearchSuggestionParser::NavigationResult::
CalculateAndClassifyMatchContents(const bool allow_bolding_nothing,
const std::u16string& input_text) {
// Start with the trivial nothing-bolded classification.
DCHECK(url_.is_valid());
// In case of zero-suggest results, do not highlight matches.
if (input_text.empty()) {
// TODO(tommycli): Maybe this should actually return
// ACMatchClassification::URL. I'm not changing this now because this CL
// is meant to fix a regression only, but we should consider this for
// consistency with other |input_text| that matches nothing.
match_contents_class_ = {
ACMatchClassification(0, ACMatchClassification::NONE)};
return;
}
// Set contents to the formatted URL while ensuring the scheme and subdomain
// are kept if the user text seems to include them. E.g., for the user text
// 'http google.com', the contents should not trim 'http'.
bool match_in_scheme = false;
bool match_in_subdomain = false;
TermMatches term_matches_in_url = FindTermMatches(input_text, formatted_url_);
// Convert TermMatches (offset, length) to MatchPosition (start, end).
std::vector<AutocompleteMatch::MatchPosition> match_positions;
for (auto match : term_matches_in_url)
match_positions.emplace_back(match.offset, match.offset + match.length);
AutocompleteMatch::GetMatchComponents(GURL(formatted_url_), match_positions,
&match_in_scheme, &match_in_subdomain);
auto format_types = AutocompleteMatch::GetFormatTypes(
GURL(input_text).has_scheme(), match_in_subdomain);
// Find matches in the potentially new match_contents
std::u16string match_contents =
url_formatter::FormatUrl(url_, format_types, base::UnescapeRule::SPACES,
nullptr, nullptr, nullptr);
TermMatches term_matches = FindTermMatches(input_text, match_contents);
// Update |match_contents_| and |match_contents_class_| if it's allowed.
if (allow_bolding_nothing || !term_matches.empty()) {
match_contents_ = match_contents;
match_contents_class_ = ClassifyTermMatches(
term_matches, match_contents.size(),
ACMatchClassification::MATCH | ACMatchClassification::URL,
ACMatchClassification::URL);
}
}
int SearchSuggestionParser::NavigationResult::CalculateRelevance(
const AutocompleteInput& input,
bool keyword_provider_requested) const {
return (from_keyword_ || !keyword_provider_requested) ? 800 : 150;
}
void SearchSuggestionParser::NavigationResult::ClassifyDescription(
const std::u16string& input_text) {
TermMatches term_matches = FindTermMatches(input_text, description_);
description_class_ = ClassifyTermMatches(term_matches, description_.size(),
ACMatchClassification::MATCH,
ACMatchClassification::NONE);
}
// SearchSuggestionParser::Results ---------------------------------------------
SearchSuggestionParser::Results::Results()
: verbatim_relevance(-1),
field_trial_triggered(false),
relevances_from_server(false) {}
SearchSuggestionParser::Results::~Results() = default;
void SearchSuggestionParser::Results::Clear() {
suggest_results.clear();
navigation_results.clear();
verbatim_relevance = -1;
metadata.clear();
field_trial_triggered = false;
experiment_stats_v2s.clear();
relevances_from_server = false;
suggestion_groups_map.clear();
}
bool SearchSuggestionParser::Results::HasServerProvidedScores() const {
if (verbatim_relevance >= 0)
return true;
// Right now either all results of one type will be server-scored or they will
// all be locally scored, but in case we change this later, we'll just check
// them all.
for (auto i(suggest_results.begin()); i != suggest_results.end(); ++i) {
if (i->relevance_from_server())
return true;
}
for (auto i(navigation_results.begin()); i != navigation_results.end(); ++i) {
if (i->relevance_from_server())
return true;
}
return false;
}
// SearchSuggestionParser ------------------------------------------------------
// static
std::string SearchSuggestionParser::ExtractJsonData(
const network::SimpleURLLoader* source,
std::unique_ptr<std::string> response_body) {
const net::HttpResponseHeaders* response_headers = nullptr;
if (source && source->ResponseInfo())
response_headers = source->ResponseInfo()->headers.get();
if (!response_body)
return std::string();
std::string json_data = std::move(*response_body);
// JSON is supposed to be UTF-8, but some suggest service providers send
// JSON files in non-UTF-8 encodings. The actual encoding is usually
// specified in the Content-Type header field.
if (response_headers) {
std::string charset;
if (response_headers->GetCharset(&charset)) {
std::u16string data_16;
// TODO(jungshik): Switch to CodePageToUTF8 after it's added.
if (base::CodepageToUTF16(json_data, charset.c_str(),
base::OnStringConversionError::FAIL, &data_16))
json_data = base::UTF16ToUTF8(data_16);
}
}
return json_data;
}
// static
std::optional<base::Value::List> SearchSuggestionParser::DeserializeJsonData(
std::string_view json_data) {
// The JSON response should be an array.
for (size_t response_start_index = json_data.find("["), i = 0;
response_start_index != std::string_view::npos && i < 5;
response_start_index = json_data.find("[", 1), i++) {
// Remove any XSSI guards to allow for JSON parsing.
json_data.remove_prefix(response_start_index);
std::optional<base::Value> data =
base::JSONReader::Read(json_data, base::JSON_ALLOW_TRAILING_COMMAS);
if (data && data->is_list()) {
return std::move(data->GetList());
}
}
return std::nullopt;
}
// static
bool SearchSuggestionParser::ParseSuggestResults(
const base::Value::List& root_list,
const AutocompleteInput& input,
const AutocompleteSchemeClassifier& scheme_classifier,
int default_result_relevance,
bool is_keyword_result,
Results* results) {
const std::u16string input_text = input.IsZeroSuggest() ? u"" : input.text();
// 1st element: query.
if (root_list.empty() || !root_list[0].is_string())
return false;
std::u16string query = base::UTF8ToUTF16(root_list[0].GetString());
if (query != input_text) {
return false;
}
// 2nd element: suggestions list.
if (root_list.size() < 2u || !root_list[1].is_list())
return false;
const auto& results_list = root_list[1].GetList();
// 3rd element: Ignore the optional description list for now.
// 4th element: Disregard the query URL list.
// 5th element: Disregard the optional key-value pairs from the server.
// Reset suggested relevance information.
results->verbatim_relevance = -1;
const base::Value::List* suggest_types = nullptr;
const base::Value::List* suggest_subtypes = nullptr;
const base::Value::List* nav_intents = nullptr;
const base::Value::List* relevances = nullptr;
const base::Value::List* suggestion_details = nullptr;
const base::Value::List* subtype_identifiers = nullptr;
int prefetch_index = -1;
int prerender_index = -1;
omnibox::GroupsInfo groups_info;
if (root_list.size() > 4u && root_list[4].is_dict()) {
const base::Value::Dict& extras = root_list[4].GetDict();
suggest_types = extras.FindList("google:suggesttype");
suggest_subtypes = extras.FindList("google:suggestsubtypes");
nav_intents = extras.FindList("google:suggestnavintents");
relevances = extras.FindList("google:suggestrelevance");
// Discard this list if its size does not match that of the suggestions.
if (relevances && relevances->size() != results_list.size()) {
relevances = nullptr;
}
if (std::optional<int> relevance =
extras.FindInt("google:verbatimrelevance")) {
results->verbatim_relevance = *relevance;
}
if (const std::string* gws_event_id_hash_str =
extras.FindString("google:suggesteventid")) {
int64_t gws_event_id_hash;
if (base::StringToInt64(*gws_event_id_hash_str, &gws_event_id_hash)) {
results->gws_event_id_hashes.push_back(gws_event_id_hash);
}
}
// Check if the active suggest field trial (if any) has triggered either
// for the default provider or keyword provider.
std::optional<bool> field_trial_triggered =
extras.FindBool("google:fieldtrialtriggered");
results->field_trial_triggered = field_trial_triggered.value_or(false);
results->experiment_stats_v2s.clear();
const base::Value::List* experiment_stats_v2s_list =
extras.FindList("google:experimentstats");
if (experiment_stats_v2s_list) {
for (const auto& experiment_stats_v2_value : *experiment_stats_v2s_list) {
const base::Value::Dict* experiment_stats_v2_dict =
experiment_stats_v2_value.GetIfDict();
if (!experiment_stats_v2_dict) {
continue;
}
std::optional<int> type_int =
experiment_stats_v2_dict->FindInt(kTypeIntFieldNumber);
const auto* string_value =
experiment_stats_v2_dict->FindString(kStringValueFieldNumber);
if (!type_int || !string_value) {
continue;
}
omnibox::metrics::ChromeSearchboxStats::ExperimentStatsV2
experiment_stats_v2;
experiment_stats_v2.set_type_int(*type_int);
experiment_stats_v2.set_string_value(*string_value);
results->experiment_stats_v2s.push_back(std::move(experiment_stats_v2));
}
}
const auto* groups_info_string = extras.FindString("google:groupsinfo");
DecodeProtoFromBase64<omnibox::GroupsInfo>(groups_info_string, groups_info);
const base::Value::Dict* client_data = extras.FindDict("google:clientdata");
if (client_data) {
prefetch_index = client_data->FindInt("phi").value_or(-1);
prerender_index = client_data->FindInt("pre").value_or(-1);
}
suggestion_details = extras.FindList("google:suggestdetail");
// Discard this list if its size does not match that of the suggestions.
if (suggestion_details &&
suggestion_details->size() != results_list.size()) {
suggestion_details = nullptr;
}
// Legacy code: Get subtype identifiers.
subtype_identifiers = extras.FindList("google:subtypeid");
// Discard this list if its size does not match that of the suggestions.
if (subtype_identifiers &&
subtype_identifiers->size() != results_list.size()) {
subtype_identifiers = nullptr;
}
// Store the metadata that came with the response in case we need to pass
// it along with the prefetch query to Instant.
base::JSONWriter::Write(extras, &results->metadata);
}
// Processed list of match subtypes, one vector per match.
// Note: ParseMatchSubtypes will handle the cases where the key does not
// exist or contains malformed data.
std::vector<std::vector<int>> subtypes =
ParseMatchSubtypes(suggest_subtypes, results_list.size());
// Clear the previous results now that new results are available.
results->suggest_results.clear();
results->navigation_results.clear();
std::string type;
int relevance = default_result_relevance;
const std::u16string& trimmed_input =
base::CollapseWhitespace(input_text, false);
for (size_t index = 0;
index < results_list.size() && results_list[index].is_string();
++index) {
std::u16string suggestion =
base::UTF8ToUTF16(results_list[index].GetString());
// Google search may return empty suggestions for weird input characters,
// they make no sense at all and can cause problems in our code.
suggestion = base::CollapseWhitespace(suggestion, false);
if (suggestion.empty())
continue;
omnibox::NavigationalIntent nav_intent = omnibox::NAV_INTENT_NONE;
if (nav_intents && index < nav_intents->size() &&
(*nav_intents)[index].is_int()) {
nav_intent = NavigationalIntentForNumber((*nav_intents)[index].GetInt());
}
// Apply valid suggested relevance scores; discard invalid lists.
if (relevances) {
if (!(*relevances)[index].is_int()) {
relevances = nullptr;
} else {
relevance = (*relevances)[index].GetInt();
}
}
AutocompleteMatchType::Type match_type =
AutocompleteMatchType::SEARCH_SUGGEST;
omnibox::SuggestType suggest_type = omnibox::TYPE_QUERY;
// Legacy code: if the server sends us a single subtype ID, place it beside
// other subtypes.
if (subtype_identifiers && index < subtype_identifiers->size() &&
(*subtype_identifiers)[index].is_int()) {
subtypes[index].emplace_back((*subtype_identifiers)[index].GetInt());
}
if (suggest_types && index < suggest_types->size() &&
(*suggest_types)[index].is_string()) {
suggest_type = GetSuggestType((*suggest_types)[index].GetString());
match_type = GetAutocompleteMatchType(suggest_type);
}
std::string deletion_url;
if (suggestion_details && index < suggestion_details->size() &&
(*suggestion_details)[index].is_dict()) {
const base::Value::Dict& suggestion_detail =
(*suggestion_details)[index].GetDict();
deletion_url = FindStringOrEmpty(suggestion_detail, "du");
}
if ((match_type == AutocompleteMatchType::NAVSUGGEST) ||
(match_type == AutocompleteMatchType::NAVSUGGEST_PERSONALIZED)) {
// Do not blindly trust the URL coming from the server to be valid.
GURL url(url_formatter::FixupURL(base::UTF16ToUTF8(suggestion),
std::string()));
if (url.is_valid()) {
std::u16string title;
// 3rd element: optional descriptions list
if (root_list.size() > 2u && root_list[2].is_list()) {
const auto& descriptions = root_list[2].GetList();
if (index < descriptions.size() && descriptions[index].is_string()) {
title = base::UTF8ToUTF16(descriptions[index].GetString());
}
}
results->navigation_results.push_back(NavigationResult(
scheme_classifier, url, match_type, suggest_type, subtypes[index],
title, deletion_url, is_keyword_result, nav_intent, relevance,
relevances != nullptr, input_text));
}
} else {
std::u16string annotation;
std::u16string match_contents = suggestion;
if (match_type == AutocompleteMatchType::CALCULATOR) {
const bool has_equals_prefix = !suggestion.compare(0, 2, u"= ");
if (has_equals_prefix) {
// Calculator results include a "= " prefix but we don't want to
// include this in the search terms.
suggestion.erase(0, 2);
// Unlikely to happen, but better to be safe.
if (base::CollapseWhitespace(suggestion, false).empty())
continue;
}
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_DESKTOP) {
match_contents = l10n_util::GetStringFUTF16(
IDS_OMNIBOX_ONE_LINE_CALCULATOR_SUGGESTION_TEMPLATE, query,
suggestion);
}
}
omnibox::RichSuggestTemplate suggest_template;
omnibox::EntityInfo entity_info;
omnibox::SuggestTemplateInfo suggest_template_info;
std::u16string match_contents_prefix;
std::optional<int> suggestion_group_id;
bool answer_parsed_successfully = false;
omnibox::RichAnswerTemplate answer_template;
omnibox::AnswerType answer_type = omnibox::ANSWER_TYPE_UNSPECIFIED;
bool has_suggest_template = false;
if (suggestion_details && (*suggestion_details)[index].is_dict() &&
!(*suggestion_details)[index].GetDict().empty()) {
const base::Value::Dict& suggestion_detail =
(*suggestion_details)[index].GetDict();
// Rich Suggest Template.
const auto* rich_template_str =
suggestion_detail.FindString("google:templateinfo");
DecodeProtoFromBase64<omnibox::RichSuggestTemplate>(rich_template_str,
suggest_template);
// Entity.
const auto* entity_info_string =
suggestion_detail.FindString("google:entityinfo");
DecodeProtoFromBase64<omnibox::EntityInfo>(entity_info_string,
entity_info);
// Suggest Template Info.
const auto* suggest_info_string =
suggestion_detail.FindString("google:suggesttemplate");
has_suggest_template =
DecodeProtoFromBase64<omnibox::SuggestTemplateInfo>(
suggest_info_string, suggest_template_info);
// Tail Suggest.
std::string match_contents_tail =
FindStringOrEmpty(suggestion_detail, "t");
if (!match_contents_tail.empty()) {
match_contents = base::UTF8ToUTF16(match_contents_tail);
}
match_contents_prefix =
base::UTF8ToUTF16(FindStringOrEmpty(suggestion_detail, "mp"));
// Suggestion group Id.
suggestion_group_id = suggestion_detail.FindInt("zl");
// Answer.
const std::string* answer_type_str =
suggestion_detail.FindString("ansb");
if (answer_type_str) {
// Check that answer type string can be mapped to omnibox::AnswerType.
int numeric_answer_type = 0;
if (base::StringToInt(base::UTF8ToUTF16(*answer_type_str),
&numeric_answer_type)) {
base::UmaHistogramSparse("Omnibox.AnswerParseType",
numeric_answer_type);
answer_type = AnswerTypeForNumber(numeric_answer_type);
}
}
if (answer_type != omnibox::ANSWER_TYPE_UNSPECIFIED) {
// omnibox::RichAnswerTemplate is preferred to "ansa" if available.
if (suggest_template.has_rich_answer_template() &&
!OmniboxFieldTrial::kAnswerActionsCounterfactual.Get()) {
answer_template = suggest_template.rich_answer_template();
FormatAnswerTemplateImageURL(&answer_template);
// Ensure `answer_template` has an answer.
answer_parsed_successfully = answer_template.answers_size() > 0;
} else if (const auto* answer_json =
suggestion_detail.FindDict("ansa")) {
answer_parsed_successfully =
omnibox::answer_data_parser::ParseJsonToAnswerData(
*answer_json, &answer_template);
}
base::UmaHistogramBoolean("Omnibox.AnswerParseSuccess",
answer_parsed_successfully);
}
}
int int_index = static_cast<int>(index);
bool should_prefetch = int_index == prefetch_index;
bool should_prerender = int_index == prerender_index;
results->suggest_results.push_back(
SuggestResult(suggestion, match_type, suggest_type, subtypes[index],
match_contents, match_contents_prefix, annotation,
std::move(entity_info), deletion_url, is_keyword_result,
nav_intent, relevance, relevances != nullptr,
should_prefetch, should_prerender, trimmed_input));
if (answer_parsed_successfully) {
// Ensure `answer_template` has an answer.
DCHECK(answer_template.answers_size() > 0);
results->suggest_results.back().SetAnswerType(answer_type);
results->suggest_results.back().SetRichAnswerTemplate(answer_template);
}
// Update suggest result match contents and annotation to use
// SuggestTemplateInfo if it is sent from server.
if (has_suggest_template) {
results->suggest_results.back().SetSuggestTemplateInfo(
suggest_template_info);
if (!suggest_template_info.primary_text().text().empty()) {
results->suggest_results.back().SetMatchContents(
base::UTF8ToUTF16(suggest_template_info.primary_text().text()));
}
if (!suggest_template_info.secondary_text().text().empty()) {
results->suggest_results.back().SetAnnotation(
base::UTF8ToUTF16(suggest_template_info.secondary_text().text()));
}
}
if (suggestion_group_id) {
results->suggest_results.back().set_suggestion_group_id(
omnibox::GroupIdForNumber(*suggestion_group_id));
}
}
}
results->relevances_from_server = relevances != nullptr;
// Keeps track of the position of the server-provided group IDs.
size_t group_index = 0;
// Adds the given group config to the results for the given group ID. Returns
// true if the entry was added to or was already present in the results.
auto add_group_config = [&](const omnibox::GroupId suggestion_group_id,
const omnibox::GroupConfig& group_config) {
// Do not add the group config if the group ID is invalid or unknown to
// Chrome.
if (suggestion_group_id == omnibox::GROUP_INVALID) {
return false;
}
// There is nothing to do if the group config has been added before.
if (base::Contains(results->suggestion_groups_map, suggestion_group_id)) {
return true;
}
// Store the group config with the appropriate section in the results.
results->suggestion_groups_map[suggestion_group_id].MergeFrom(group_config);
results->suggestion_groups_map[suggestion_group_id].set_section(
ChromeGroupSectionForRemoteGroupIndex(group_index++));
return true;
};
// Add the group configs associated with the suggestions.
for (auto& suggest_result : results->suggest_results) {
if (!suggest_result.suggestion_group_id().has_value()) {
continue;
}
const omnibox::GroupId suggestion_group_id =
suggest_result.suggestion_group_id().value();
// Add the group config associated with the suggestion, if the suggestion
// has a valid group ID and a corresponding group config is found in the
// response.
if (!base::Contains(groups_info.group_configs(), suggestion_group_id) ||
!add_group_config(suggestion_group_id, groups_info.group_configs().at(
suggestion_group_id))) {
continue;
}
}
// Add the remaining group configs without any suggestions in the response.
// The only known use case is the personalized zero-suggest which is also
// produced by Chrome and relies on the server-provided group config to show
// with the appropriate header text, where a header text is applicable.
for (const auto& entry : groups_info.group_configs()) {
add_group_config(omnibox::GroupIdForNumber(entry.first), entry.second);
}
return true;
}