| // Copyright 2025 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/enterprise_search_aggregator_suggestions_service.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h" |
| #include "components/signin/public/identity_manager/scope_set.h" |
| #include "components/variations/net/variations_http_headers.h" |
| #include "google_apis/gaia/gaia_constants.h" |
| #include "net/base/load_flags.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "net/http/http_request_headers.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 "url/gurl.h" |
| |
| namespace { |
| // Builds an enterprise search aggregator request body. Inputs that affect the |
| // request are: |
| // `query`: Current omnibox query text, passed as an argument. |
| // `suggestion_types`: Types of suggestions requested. Ex. [1,2] indicates |
| // Query and Person Suggestions respectively. |
| // `experiment_id`: Hardcoded experiment ID expected by the server. |
| // The format of the request is: |
| // { |
| // query: "`query`", |
| // suggestionTypes: `suggestion_types`, |
| // experimentIds: ["`experiment_id`"] |
| // } |
| std::string BuildRequestBody(std::u16string query, |
| std::vector<int>& suggestion_types) { |
| base::Value::Dict root; |
| root.Set("query", query); |
| |
| base::Value::List suggestion_types_list; |
| for (const auto& item : suggestion_types) { |
| suggestion_types_list.Append(item); |
| } |
| root.Set("suggestionTypes", std::move(suggestion_types_list)); |
| |
| base::Value::List experiment_ids_list; |
| experiment_ids_list.Append(kEnterpriseSearchAggregatorExperimentId); |
| root.Set("experimentIds", std::move(experiment_ids_list)); |
| |
| std::string result; |
| base::JSONWriter::Write(root, &result); |
| return result; |
| } |
| } // namespace |
| |
| EnterpriseSearchAggregatorSuggestionsService:: |
| EnterpriseSearchAggregatorSuggestionsService( |
| signin::IdentityManager* identity_manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : url_loader_factory_(url_loader_factory), |
| identity_manager_(identity_manager), |
| token_fetcher_(nullptr) {} |
| |
| EnterpriseSearchAggregatorSuggestionsService:: |
| ~EnterpriseSearchAggregatorSuggestionsService() = default; |
| |
| void EnterpriseSearchAggregatorSuggestionsService:: |
| CreateEnterpriseSearchAggregatorSuggestionsRequest( |
| const std::u16string& query, |
| const GURL& suggest_url, |
| std::vector<int> callback_indexes, |
| std::vector<std::vector<int>> suggestion_types, |
| CreationCallback creation_callback, |
| StartCallback start_callback, |
| CompletionCallback completion_callback) { |
| DCHECK(suggest_url.is_valid()); |
| CHECK_EQ(callback_indexes.size(), suggestion_types.size()); |
| |
| auto requests = std::vector<std::unique_ptr<network::ResourceRequest>>{}; |
| for (size_t i = 0; i < suggestion_types.size(); ++i) { |
| requests.push_back(std::make_unique<network::ResourceRequest>()); |
| } |
| for (size_t i = 0; i < suggestion_types.size(); i++) { |
| requests[i]->url = suggest_url; |
| requests[i]->method = net::HttpRequestHeaders::kPostMethod; |
| requests[i]->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES; |
| |
| requests[i]->site_for_cookies = net::SiteForCookies::FromUrl(suggest_url); |
| variations::AppendVariationsHeaderUnknownSignedIn( |
| requests[i]->url, variations::InIncognito::kNo, requests[i].get()); |
| |
| creation_callback.Run(requests[i].get()); |
| } |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("omnibox_search_aggregator_suggest", |
| R"( |
| semantics { |
| sender: "Omnibox" |
| description: |
| "Request for enterprise suggestions from the omnibox. Enterprise " |
| "suggestions provide enterprise specific documents to enterprise " |
| "users and are configured by enterprise admin." |
| trigger: "Signed-in enterprise user enters text in the omnibox." |
| user_data { |
| type: ACCESS_TOKEN |
| type: USER_CONTENT |
| type: SENSITIVE_URL |
| } |
| data: "The query string from the omnibox." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { email: "chrome-desktop-search@google.com" } |
| } |
| last_reviewed: "2025-01-07" |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "The suggest_url is set by policy through enterprise admin." |
| "Signed-in enterprise users can see this configuration in" |
| "chrome://settings/searchEngines." |
| chrome_policy { |
| SearchSuggestEnabled { |
| policy_options {mode: MANDATORY} |
| SearchSuggestEnabled: false |
| } |
| } |
| })"); |
| |
| token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| signin::OAuthConsumerId::kEnterpriseSearchAggregator, identity_manager_, |
| base::BindOnce( |
| &EnterpriseSearchAggregatorSuggestionsService::AccessTokenAvailable, |
| base::Unretained(this), std::move(requests), std::move(query), |
| std::move(callback_indexes), std::move(suggestion_types), |
| traffic_annotation, std::move(start_callback), |
| std::move(completion_callback)), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| void EnterpriseSearchAggregatorSuggestionsService:: |
| StopCreatingEnterpriseSearchAggregatorSuggestionsRequest() { |
| token_fetcher_.reset(); |
| } |
| |
| void EnterpriseSearchAggregatorSuggestionsService::AccessTokenAvailable( |
| std::vector<std::unique_ptr<network::ResourceRequest>> requests, |
| const std::u16string& query, |
| std::vector<int> callback_indexes, |
| std::vector<std::vector<int>> suggestion_types, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| StartCallback start_callback, |
| CompletionCallback completion_callback, |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo access_token_info) { |
| DCHECK(token_fetcher_); |
| token_fetcher_.reset(); |
| |
| auto request_bodies = std::vector<std::string>{}; |
| for (auto suggestion_type : suggestion_types) { |
| const std::string& request_body = BuildRequestBody(query, suggestion_type); |
| request_bodies.push_back(request_body); |
| } |
| |
| // If there were no errors obtaining the access token, append it to the |
| // request as a header. |
| if (error.state() == GoogleServiceAuthError::NONE) { |
| DCHECK(!access_token_info.token.empty()); |
| for (const auto& request : requests) { |
| request->headers.SetHeader( |
| "Authorization", |
| base::StringPrintf("Bearer %s", access_token_info.token.c_str())); |
| } |
| } |
| |
| for (size_t i = 0; i < requests.size(); ++i) { |
| StartDownloadAndTransferLoader(std::move(requests[i]), |
| std::move(request_bodies[i]), |
| traffic_annotation, callback_indexes[i], |
| start_callback, completion_callback); |
| } |
| } |
| |
| // TODO(crbug.com/385756623): Factor out this method so it can be used across |
| // document_suggestions_service and |
| // enterprise_search_aggregator_suggestions_service. |
| void EnterpriseSearchAggregatorSuggestionsService:: |
| StartDownloadAndTransferLoader( |
| std::unique_ptr<network::ResourceRequest> request, |
| std::string request_body, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| int request_index, |
| StartCallback start_callback, |
| CompletionCallback completion_callback) { |
| if (!url_loader_factory_) { |
| return; |
| } |
| |
| std::unique_ptr<network::SimpleURLLoader> loader = |
| network::SimpleURLLoader::Create(std::move(request), traffic_annotation); |
| if (!request_body.empty()) { |
| loader->AttachStringForUpload(request_body, "application/json"); |
| } |
| loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory_.get(), |
| base::BindOnce(completion_callback, loader.get(), request_index)); |
| |
| std::move(start_callback).Run(request_index, std::move(loader), request_body); |
| } |