1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/omnibox/browser/document_suggestions_service.h"
6 
7 #include <memory>
8 #include <utility>
9 
10 #include "base/bind.h"
11 #include "base/feature_list.h"
12 #include "base/i18n/rtl.h"
13 #include "base/json/json_writer.h"
14 #include "base/memory/scoped_refptr.h"
15 #include "base/metrics/field_trial_params.h"
16 #include "base/values.h"
17 #include "components/omnibox/browser/document_provider.h"
18 #include "components/omnibox/common/omnibox_features.h"
19 #include "components/signin/public/identity_manager/identity_manager.h"
20 #include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
21 #include "components/signin/public/identity_manager/scope_set.h"
22 #include "components/variations/net/variations_http_headers.h"
23 #include "components/variations/variations_associated_data.h"
24 #include "net/base/load_flags.h"
25 #include "net/traffic_annotation/network_traffic_annotation.h"
26 #include "services/network/public/cpp/resource_request.h"
27 #include "services/network/public/cpp/shared_url_loader_factory.h"
28 #include "services/network/public/cpp/simple_url_loader.h"
29 
30 namespace {
31 
32 // Builds a document search request body. Inputs that affect the request are:
33 //   |query|: Current omnibox query text, passed as an argument.
34 //   |locale|: Current browser locale as BCP-47, obtained inside the function.
35 // The format of the request is:
36 //     {
37 //       query: "|query|",
38 //       start: 0,
39 //       pageSize: 10,
40 //       requestOptions: {
41 //            searchApplicationId: "searchapplications/chrome",
42 //            languageCode: "|locale|",
43 //       }
44 //     }
BuildDocumentSuggestionRequest(const base::string16 & query)45 std::string BuildDocumentSuggestionRequest(const base::string16& query) {
46   base::Value root(base::Value::Type::DICTIONARY);
47   root.SetKey("query", base::Value(query));
48   // The API supports pagination. We're always concerned with the first N
49   // results on the first page.
50   root.SetKey("start", base::Value(0));
51   root.SetKey("pageSize", base::Value(10));
52 
53   base::Value request_options(base::Value::Type::DICTIONARY);
54   request_options.SetKey("searchApplicationId",
55                          base::Value("searchapplications/chrome"));
56   request_options.SetKey("languageCode",
57                          base::Value(base::i18n::GetConfiguredLocale()));
58   root.SetKey("requestOptions", std::move(request_options));
59 
60   std::string result;
61   base::JSONWriter::Write(root, &result);
62   return result;
63 }
64 
65 }  // namespace
66 
DocumentSuggestionsService(signin::IdentityManager * identity_manager,scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)67 DocumentSuggestionsService::DocumentSuggestionsService(
68     signin::IdentityManager* identity_manager,
69     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
70     : url_loader_factory_(url_loader_factory),
71       identity_manager_(identity_manager),
72       token_fetcher_(nullptr) {
73   DCHECK(url_loader_factory);
74 }
75 
~DocumentSuggestionsService()76 DocumentSuggestionsService::~DocumentSuggestionsService() {}
77 
CreateDocumentSuggestionsRequest(const base::string16 & query,bool is_incognito,StartCallback start_callback,CompletionCallback completion_callback)78 void DocumentSuggestionsService::CreateDocumentSuggestionsRequest(
79     const base::string16& query,
80     bool is_incognito,
81     StartCallback start_callback,
82     CompletionCallback completion_callback) {
83   std::string endpoint = base::GetFieldTrialParamValueByFeature(
84       omnibox::kDocumentProvider, "DocumentProviderEndpoint");
85   if (endpoint.empty())
86     endpoint = "https://cloudsearch.googleapis.com/v1/query/search";
87   const GURL suggest_url = GURL(endpoint);
88   DCHECK(suggest_url.is_valid());
89 
90   net::NetworkTrafficAnnotationTag traffic_annotation =
91       net::DefineNetworkTrafficAnnotation("omnibox_documentsuggest", R"(
92         semantics {
93           sender: "Omnibox"
94           description:
95             "Request for Google Drive document suggestions from the omnibox."
96             "User must be signed in and have default search provider set to "
97             "Google."
98           trigger: "Signed-in user enters text in the omnibox."
99           data: "The query string from the omnibox."
100           destination: GOOGLE_OWNED_SERVICE
101         }
102         policy {
103           cookies_allowed: YES
104           cookies_store: "user"
105           setting:
106             "Coupled to Google default search plus signed-in"
107           chrome_policy {
108             SearchSuggestEnabled {
109                 policy_options {mode: MANDATORY}
110                 SearchSuggestEnabled: false
111             }
112           }
113         })");
114   auto request = std::make_unique<network::ResourceRequest>();
115   request->url = suggest_url;
116   request->method = "POST";
117   std::string request_body = BuildDocumentSuggestionRequest(query);
118   request->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES;
119   // It is expected that the user is signed in here. But we only care about
120   // experiment IDs from the variations server, which do not require the
121   // signed-in version of this method.
122   variations::AppendVariationsHeaderUnknownSignedIn(
123       request->url,
124       is_incognito ? variations::InIncognito::kYes
125                    : variations::InIncognito::kNo,
126       request.get());
127 
128   // Create and fetch an OAuth2 token.
129   std::string scope = "https://www.googleapis.com/auth/cloud_search.query";
130   signin::ScopeSet scopes;
131   scopes.insert(scope);
132   token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
133       "document_suggestions_service", identity_manager_, scopes,
134       base::BindOnce(&DocumentSuggestionsService::AccessTokenAvailable,
135                      base::Unretained(this), std::move(request),
136                      std::move(request_body), traffic_annotation,
137                      std::move(start_callback), std::move(completion_callback)),
138       signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable);
139 }
140 
StopCreatingDocumentSuggestionsRequest()141 void DocumentSuggestionsService::StopCreatingDocumentSuggestionsRequest() {
142   std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher>
143       token_fetcher_deleter(std::move(token_fetcher_));
144 }
145 
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)146 void DocumentSuggestionsService::AccessTokenAvailable(
147     std::unique_ptr<network::ResourceRequest> request,
148     std::string request_body,
149     net::NetworkTrafficAnnotationTag traffic_annotation,
150     StartCallback start_callback,
151     CompletionCallback completion_callback,
152     GoogleServiceAuthError error,
153     signin::AccessTokenInfo access_token_info) {
154   DCHECK(token_fetcher_);
155   token_fetcher_.reset();
156 
157   // If there were no errors obtaining the access token, append it to the
158   // request as a header.
159   if (error.state() == GoogleServiceAuthError::NONE) {
160     DCHECK(!access_token_info.token.empty());
161     request->headers.SetHeader(
162         "Authorization",
163         base::StringPrintf("Bearer %s", access_token_info.token.c_str()));
164   }
165 
166   StartDownloadAndTransferLoader(std::move(request), std::move(request_body),
167                                  traffic_annotation, std::move(start_callback),
168                                  std::move(completion_callback));
169 }
170 
StartDownloadAndTransferLoader(std::unique_ptr<network::ResourceRequest> request,std::string request_body,net::NetworkTrafficAnnotationTag traffic_annotation,StartCallback start_callback,CompletionCallback completion_callback)171 void DocumentSuggestionsService::StartDownloadAndTransferLoader(
172     std::unique_ptr<network::ResourceRequest> request,
173     std::string request_body,
174     net::NetworkTrafficAnnotationTag traffic_annotation,
175     StartCallback start_callback,
176     CompletionCallback completion_callback) {
177   std::unique_ptr<network::SimpleURLLoader> loader =
178       network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
179   if (!request_body.empty()) {
180     loader->AttachStringForUpload(request_body, "application/json");
181   }
182   loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
183       url_loader_factory_.get(),
184       base::BindOnce(std::move(completion_callback), loader.get()));
185 
186   std::move(start_callback).Run(std::move(loader));
187 }
188