| // Copyright 2018 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/document_suggestions_service.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/i18n/rtl.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "components/omnibox/browser/document_provider.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/signin/public/identity_manager/account_capabilities.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/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" |
| |
| namespace { |
| |
| // 6 refers to CHROME_OMNIBOX in the ClientId enum. |
| constexpr int chromeOmniboxClientId = 6; |
| |
| // Builds a document search request body. Inputs that affect the request are: |
| // |query|: Current omnibox query text, passed as an argument. |
| // |locale|: Current browser locale as BCP-47, obtained inside the function. |
| // The format of the request is: |
| // { |
| // query: "|query|", |
| // start: 0, |
| // pageSize: 10, |
| // requestOptions: { |
| // searchApplicationId: "searchapplications/chrome", |
| // clientId: 6, |
| // languageCode: "|locale|", |
| // } |
| // } |
| std::string BuildDocumentSuggestionRequest(const std::u16string& query) { |
| base::Value::Dict root; |
| root.Set("query", base::Value(query)); |
| // The API supports pagination. We're always concerned with the first N |
| // results on the first page. |
| root.Set("start", base::Value(0)); |
| root.Set("pageSize", base::Value(10)); |
| |
| base::Value::Dict request_options; |
| request_options.Set("searchApplicationId", |
| base::Value("searchapplications/chrome")); |
| // While the searchApplicationId is a specific config being used by a client |
| // and can be shared among multiple clients in some instances, clientId |
| // identifies a client uniquely. |
| request_options.Set("clientId", base::Value(chromeOmniboxClientId)); |
| request_options.Set("languageCode", |
| base::Value(base::i18n::GetConfiguredLocale())); |
| root.Set("requestOptions", std::move(request_options)); |
| |
| std::string result; |
| base::JSONWriter::Write(root, &result); |
| return result; |
| } |
| |
| } // namespace |
| |
| DocumentSuggestionsService::DocumentSuggestionsService( |
| signin::IdentityManager* identity_manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : url_loader_factory_(url_loader_factory), |
| identity_manager_(identity_manager), |
| account_is_workspace_managed_(IsAccountWorkspaceManaged()), |
| token_fetcher_(nullptr) { |
| if (identity_manager_) { |
| identity_manager_observation_.Observe(identity_manager_); |
| } |
| } |
| |
| DocumentSuggestionsService::~DocumentSuggestionsService() = default; |
| |
| bool DocumentSuggestionsService::HasPrimaryAccount() { |
| if (has_primary_account_for_testing_) { |
| return true; |
| } |
| |
| return identity_manager_ && |
| identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin); |
| } |
| |
| void DocumentSuggestionsService::SetAccountStateForTesting(bool valid) { |
| has_primary_account_for_testing_ = valid; |
| account_is_workspace_managed_for_testing_ = valid; |
| account_is_workspace_managed_ = IsAccountWorkspaceManaged(); |
| } |
| |
| void DocumentSuggestionsService::CreateDocumentSuggestionsRequest( |
| const std::u16string& query, |
| bool is_incognito, |
| CreationCallback creation_callback, |
| StartCallback start_callback, |
| CompletionCallback completion_callback) { |
| std::string endpoint = base::GetFieldTrialParamValueByFeature( |
| omnibox::kDocumentProvider, "DocumentProviderEndpoint"); |
| if (endpoint.empty()) |
| endpoint = "https://cloudsearch.googleapis.com/v1/query/search"; |
| const GURL suggest_url = GURL(endpoint); |
| DCHECK(suggest_url.is_valid()); |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("omnibox_documentsuggest", R"( |
| semantics { |
| sender: "Omnibox" |
| description: |
| "Request for Google Drive document suggestions from the omnibox. " |
| "User must be signed in and have default search provider set to " |
| "Google." |
| trigger: "Signed-in user enters text in the omnibox." |
| data: "The query string from the omnibox." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: "Coupled to Google default search plus signed-in." |
| chrome_policy { |
| SearchSuggestEnabled { |
| policy_options {mode: MANDATORY} |
| SearchSuggestEnabled: false |
| } |
| } |
| })"); |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = suggest_url; |
| request->method = "POST"; |
| std::string request_body = BuildDocumentSuggestionRequest(query); |
| request->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES; |
| // Set the SiteForCookies to the request URL's site to avoid cookie blocking. |
| request->site_for_cookies = net::SiteForCookies::FromUrl(suggest_url); |
| // It is expected that the user is signed in here. But we only care about |
| // experiment IDs from the variations server, which do not require the |
| // signed-in version of this method. |
| variations::AppendVariationsHeaderUnknownSignedIn( |
| request->url, |
| is_incognito ? variations::InIncognito::kYes |
| : variations::InIncognito::kNo, |
| request.get()); |
| |
| std::move(creation_callback).Run(request.get()); |
| |
| // Create and fetch an OAuth2 token. |
| signin::ScopeSet scopes; |
| scopes.insert(GaiaConstants::kCloudSearchQueryOAuth2Scope); |
| token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| "document_suggestions_service", identity_manager_, scopes, |
| base::BindOnce(&DocumentSuggestionsService::AccessTokenAvailable, |
| base::Unretained(this), std::move(request), |
| std::move(request_body), traffic_annotation, |
| std::move(start_callback), std::move(completion_callback)), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| void DocumentSuggestionsService::StopCreatingDocumentSuggestionsRequest() { |
| std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> |
| token_fetcher_deleter(std::move(token_fetcher_)); |
| } |
| |
| signin::Tribool DocumentSuggestionsService::IsAccountWorkspaceManaged() { |
| if (!HasPrimaryAccount()) { |
| return signin::Tribool::kFalse; |
| } |
| |
| if (account_is_workspace_managed_for_testing_) { |
| return signin::Tribool::kTrue; |
| } |
| |
| const auto& account_id = |
| identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin); |
| const auto& account_info = |
| identity_manager_->FindExtendedAccountInfoByAccountId(account_id); |
| return account_info.capabilities.is_subject_to_enterprise_features(); |
| } |
| |
| void DocumentSuggestionsService::AccessTokenAvailable( |
| std::unique_ptr<network::ResourceRequest> request, |
| std::string request_body, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| StartCallback start_callback, |
| CompletionCallback completion_callback, |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo access_token_info) { |
| DCHECK(token_fetcher_); |
| token_fetcher_.reset(); |
| |
| // 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()); |
| request->headers.SetHeader( |
| "Authorization", |
| base::StringPrintf("Bearer %s", access_token_info.token.c_str())); |
| } |
| |
| StartDownloadAndTransferLoader(std::move(request), std::move(request_body), |
| traffic_annotation, std::move(start_callback), |
| std::move(completion_callback)); |
| } |
| |
| void DocumentSuggestionsService::StartDownloadAndTransferLoader( |
| std::unique_ptr<network::ResourceRequest> request, |
| std::string request_body, |
| net::NetworkTrafficAnnotationTag traffic_annotation, |
| StartCallback start_callback, |
| CompletionCallback completion_callback) { |
| // Loader factory may be null in tests. |
| 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(std::move(completion_callback), loader.get())); |
| |
| std::move(start_callback).Run(std::move(loader), request_body); |
| } |
| |
| void DocumentSuggestionsService::OnPrimaryAccountChanged( |
| const signin::PrimaryAccountChangeEvent& event_details) { |
| account_is_workspace_managed_ = IsAccountWorkspaceManaged(); |
| } |
| |
| void DocumentSuggestionsService::OnExtendedAccountInfoUpdated( |
| const AccountInfo& account_info) { |
| account_is_workspace_managed_ = |
| account_info.capabilities.is_subject_to_enterprise_features(); |
| } |
| |
| void DocumentSuggestionsService::OnIdentityManagerShutdown( |
| signin::IdentityManager* identity_manager) { |
| identity_manager_observation_.Reset(); |
| identity_manager_ = nullptr; |
| } |