blob: 9eabe7489c4ec5da1c52f66650ad46351900ce61 [file] [log] [blame]
// 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;
}