// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/history/core/browser/web_history_service.h" #include #include #include "base/bind.h" #include "base/command_line.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/memory/ptr_util.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "components/history/core/browser/history_service_observer.h" #include "components/history/core/browser/web_history_service_observer.h" #include "components/signin/public/identity_manager/access_token_info.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" #if !defined(TOOLKIT_QT) #include "components/sync/base/sync_util.h" #include "components/sync/protocol/history_status.pb.h" #endif #include "google_apis/gaia/gaia_urls.h" #include "google_apis/gaia/google_service_auth_error.h" #include "net/base/load_flags.h" #include "net/base/url_util.h" #include "net/http/http_status_code.h" #include "net/http/http_util.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 "third_party/abseil-cpp/absl/types/optional.h" #include "url/gurl.h" namespace history { namespace { const char kHistoryOAuthScope[] = "https://www.googleapis.com/auth/chromesync"; const char kHistoryQueryHistoryUrl[] = "https://history.google.com/history/api/lookup?client=chrome"; const char kHistoryDeleteHistoryUrl[] = "https://history.google.com/history/api/delete?client=chrome"; const char kHistoryAudioHistoryUrl[] = "https://history.google.com/history/api/lookup?client=audio"; const char kHistoryAudioHistoryChangeUrl[] = "https://history.google.com/history/api/change"; const char kQueryWebAndAppActivityUrl[] = "https://history.google.com/history/api/lookup?client=web_app"; const char kQueryOtherFormsOfBrowsingHistoryUrlSuffix[] = "/historystatus"; const char kPostDataMimeType[] = "text/plain"; const char kSyncProtoMimeType[] = "application/octet-stream"; // The maximum number of retries for the SimpleURLLoader requests. const size_t kMaxRetries = 1; class RequestImpl : public WebHistoryService::Request { public: ~RequestImpl() override {} // Returns the response code received from the server, which will only be // valid if the request succeeded. int GetResponseCode() override { return response_code_; } // Returns the contents of the response body received from the server. const std::string& GetResponseBody() override { return response_body_; } bool IsPending() override { return is_pending_; } private: friend class history::WebHistoryService; RequestImpl( signin::IdentityManager* identity_manager, scoped_refptr url_loader_factory, const GURL& url, WebHistoryService::CompletionCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) : identity_manager_(identity_manager), url_loader_factory_(std::move(url_loader_factory)), url_(url), post_data_mime_type_(kPostDataMimeType), response_code_(0), auth_retry_count_(0), callback_(std::move(callback)), is_pending_(false), partial_traffic_annotation_(partial_traffic_annotation) { DCHECK(identity_manager_); DCHECK(url_loader_factory_); } void OnAccessTokenFetchComplete(GoogleServiceAuthError error, signin::AccessTokenInfo access_token_info) { access_token_fetcher_.reset(); if (error.state() != GoogleServiceAuthError::NONE) { is_pending_ = false; UMA_HISTOGRAM_BOOLEAN("WebHistory.OAuthTokenCompletion", false); std::move(callback_).Run(this, false); // It is valid for the callback to delete `this`, so do not access any // members below here. return; } DCHECK(!access_token_info.token.empty()); access_token_ = access_token_info.token; UMA_HISTOGRAM_BOOLEAN("WebHistory.OAuthTokenCompletion", true); // Got an access token -- start the actual API request. net::NetworkTrafficAnnotationTag traffic_annotation = net::CompleteNetworkTrafficAnnotation("web_history_service", partial_traffic_annotation_, R"( semantics { sender: "Web History" destination: GOOGLE_OWNED_SERVICE } policy { cookies_allowed: NO setting: "To disable this feature, users can either sign out or disable " "history sync via unchecking 'History' setting under 'Advanced " "sync settings." })"); auto resource_request = std::make_unique(); resource_request->url = url_; resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; resource_request->method = post_data_ ? "POST" : "GET"; resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization, "Bearer " + access_token_info.token); resource_request->headers.SetHeader( "X-Developer-Key", GaiaUrls::GetInstance()->oauth2_chrome_client_id()); if (!user_agent_.empty()) { resource_request->headers.SetHeader(net::HttpRequestHeaders::kUserAgent, user_agent_); } simple_url_loader_ = network::SimpleURLLoader::Create( std::move(resource_request), traffic_annotation); if (post_data_) { simple_url_loader_->AttachStringForUpload(post_data_.value(), post_data_mime_type_); } simple_url_loader_->SetRetryOptions(kMaxRetries, network::SimpleURLLoader::RETRY_ON_5XX); simple_url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( url_loader_factory_.get(), base::BindOnce(&RequestImpl::OnSimpleLoaderComplete, base::Unretained(this))); } // Tells the request to do its thang. void Start() override { signin::ScopeSet oauth_scopes; oauth_scopes.insert(kHistoryOAuthScope); access_token_fetcher_ = std::make_unique( "web_history", identity_manager_, oauth_scopes, base::BindOnce(&RequestImpl::OnAccessTokenFetchComplete, base::Unretained(this)), signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate); is_pending_ = true; } void OnSimpleLoaderComplete(std::unique_ptr response_body) { response_code_ = -1; if (simple_url_loader_->ResponseInfo() && simple_url_loader_->ResponseInfo()->headers) { response_code_ = simple_url_loader_->ResponseInfo()->headers->response_code(); } simple_url_loader_.reset(); UMA_HISTOGRAM_CUSTOM_ENUMERATION("WebHistory.OAuthTokenResponseCode", net::HttpUtil::MapStatusCodeForHistogram(response_code_), net::HttpUtil::GetStatusCodesForHistogram()); // If the response code indicates that the token might not be valid, // invalidate the token and try again. if (response_code_ == net::HTTP_UNAUTHORIZED && ++auth_retry_count_ <= 1) { signin::ScopeSet oauth_scopes; oauth_scopes.insert(kHistoryOAuthScope); identity_manager_->RemoveAccessTokenFromCache( identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync), oauth_scopes, access_token_); access_token_.clear(); Start(); return; } if (response_body) { response_body_ = std::move(*response_body); } else { response_body_.clear(); } is_pending_ = false; std::move(callback_).Run(this, true); // It is valid for the callback to delete `this`, so do not access any // members below here. } void SetPostData(const std::string& post_data) override { SetPostDataAndType(post_data, kPostDataMimeType); } void SetPostDataAndType(const std::string& post_data, const std::string& mime_type) override { post_data_ = post_data; post_data_mime_type_ = mime_type; } void SetUserAgent(const std::string& user_agent) override { user_agent_ = user_agent; } signin::IdentityManager* identity_manager_; scoped_refptr url_loader_factory_; // The URL of the API endpoint. GURL url_; // POST data to be sent with the request (may be empty). absl::optional post_data_; // MIME type of the post requests. Defaults to text/plain. std::string post_data_mime_type_; // The user agent header used with this request. std::string user_agent_; std::unique_ptr access_token_fetcher_; // The current OAuth2 access token. std::string access_token_; // Handles the actual API requests after the OAuth token is acquired. std::unique_ptr simple_url_loader_; // Holds the response code received from the server. int response_code_; // Holds the response body received from the server. std::string response_body_; // The number of times this request has already been retried due to // authorization problems. int auth_retry_count_; // The callback to execute when the query is complete. WebHistoryService::CompletionCallback callback_; // True if the request was started and has not yet completed, otherwise false. bool is_pending_; // Partial Network traffic annotation used to create SimpleURLLoader for this // request. const net::PartialNetworkTrafficAnnotationTag partial_traffic_annotation_; }; // Converts a time into a string for use as a parameter in a request to the // history server. std::string ServerTimeString(base::Time time) { if (time < base::Time::UnixEpoch()) return base::NumberToString(0); return base::NumberToString( (time - base::Time::UnixEpoch()).InMicroseconds()); } // Returns a URL for querying the history server for a query specified by // `options`. `version_info`, if not empty, should be a token that was received // from the server in response to a write operation. It is used to help ensure // read consistency after a write. GURL GetQueryUrl(const std::u16string& text_query, const QueryOptions& options, const std::string& version_info) { GURL url = GURL(kHistoryQueryHistoryUrl); url = net::AppendQueryParameter(url, "titles", "1"); // Take `begin_time`, `end_time`, and `max_count` from the original query // options, and convert them to the equivalent URL parameters. Note that // QueryOptions uses exclusive `end_time` while the history.google.com API // uses it inclusively, so we subtract 1us during conversion. base::Time end_time = options.end_time.is_null() ? base::Time::Now() : std::min(options.end_time - base::TimeDelta::FromMicroseconds(1), base::Time::Now()); url = net::AppendQueryParameter(url, "max", ServerTimeString(end_time)); if (!options.begin_time.is_null()) { url = net::AppendQueryParameter( url, "min", ServerTimeString(options.begin_time)); } if (options.max_count) { url = net::AppendQueryParameter(url, "num", base::NumberToString(options.max_count)); } if (!text_query.empty()) url = net::AppendQueryParameter(url, "q", base::UTF16ToUTF8(text_query)); if (!version_info.empty()) url = net::AppendQueryParameter(url, "kvi", version_info); return url; } // Creates a DictionaryValue to hold the parameters for a deletion. // Ownership is passed to the caller. // `url` may be empty, indicating a time-range deletion. std::unique_ptr CreateDeletion( const std::string& min_time, const std::string& max_time, const GURL& url) { std::unique_ptr deletion(new base::DictionaryValue); deletion->SetString("type", "CHROME_HISTORY"); if (url.is_valid()) deletion->SetString("url", url.spec()); deletion->SetString("min_timestamp_usec", min_time); deletion->SetString("max_timestamp_usec", max_time); return deletion; } } // namespace WebHistoryService::Request::Request() { } WebHistoryService::Request::~Request() { } WebHistoryService::WebHistoryService( signin::IdentityManager* identity_manager, scoped_refptr url_loader_factory) : identity_manager_(identity_manager), url_loader_factory_(std::move(url_loader_factory)) {} WebHistoryService::~WebHistoryService() { } void WebHistoryService::AddObserver(WebHistoryServiceObserver* observer) { observer_list_.AddObserver(observer); } void WebHistoryService::RemoveObserver(WebHistoryServiceObserver* observer) { observer_list_.RemoveObserver(observer); } WebHistoryService::Request* WebHistoryService::CreateRequest( const GURL& url, CompletionCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { return new RequestImpl(identity_manager_, url_loader_factory_, url, std::move(callback), partial_traffic_annotation); } // static std::unique_ptr WebHistoryService::ReadResponse( WebHistoryService::Request* request) { std::unique_ptr result; if (request->GetResponseCode() == net::HTTP_OK) { std::unique_ptr value = base::JSONReader::ReadDeprecated(request->GetResponseBody()); if (value && value->is_dict()) result.reset(static_cast(value.release())); else DLOG(WARNING) << "Non-JSON response received from history server."; } return result; } std::unique_ptr WebHistoryService::QueryHistory( const std::u16string& text_query, const QueryOptions& options, WebHistoryService::QueryWebHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce( &WebHistoryService::QueryHistoryCompletionCallback, std::move(callback)); GURL url = GetQueryUrl(text_query, options, server_version_info_); std::unique_ptr request(CreateRequest( url, std::move(completion_callback), partial_traffic_annotation)); request->Start(); return request; } void WebHistoryService::ExpireHistory( const std::vector& expire_list, ExpireWebHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { base::DictionaryValue delete_request; std::unique_ptr deletions(new base::ListValue); base::Time now = base::Time::Now(); for (const auto& expire : expire_list) { // Convert the times to server timestamps. std::string min_timestamp = ServerTimeString(expire.begin_time); // TODO(dubroy): Use sane time (crbug.com/146090) here when it's available. base::Time end_time = expire.end_time; if (end_time.is_null() || end_time > now) end_time = now; std::string max_timestamp = ServerTimeString(end_time); for (const auto& url : expire.urls) { deletions->Append( CreateDeletion(min_timestamp, max_timestamp, url)); } // If no URLs were specified, delete everything in the time range. if (expire.urls.empty()) deletions->Append(CreateDeletion(min_timestamp, max_timestamp, GURL())); } delete_request.Set("del", std::move(deletions)); std::string post_data; base::JSONWriter::Write(delete_request, &post_data); GURL url(kHistoryDeleteHistoryUrl); // Append the version info token, if it is available, to help ensure // consistency with any previous deletions. if (!server_version_info_.empty()) url = net::AppendQueryParameter(url, "kvi", server_version_info_); // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce(&WebHistoryService::ExpireHistoryCompletionCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); std::unique_ptr request(CreateRequest( url, std::move(completion_callback), partial_traffic_annotation)); request->SetPostData(post_data); Request* request_ptr = request.get(); pending_expire_requests_[request_ptr] = std::move(request); request_ptr->Start(); } void WebHistoryService::ExpireHistoryBetween( const std::set& restrict_urls, base::Time begin_time, base::Time end_time, ExpireWebHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { std::vector expire_list(1); expire_list.back().urls = restrict_urls; expire_list.back().begin_time = begin_time; expire_list.back().end_time = end_time; ExpireHistory(expire_list, std::move(callback), partial_traffic_annotation); } void WebHistoryService::GetAudioHistoryEnabled( AudioWebHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce(&WebHistoryService::AudioHistoryCompletionCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); GURL url(kHistoryAudioHistoryUrl); std::unique_ptr request(CreateRequest( url, std::move(completion_callback), partial_traffic_annotation)); request->Start(); Request* request_ptr = request.get(); pending_audio_history_requests_[request_ptr] = std::move(request); } void WebHistoryService::SetAudioHistoryEnabled( bool new_enabled_value, AudioWebHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce(&WebHistoryService::AudioHistoryCompletionCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); GURL url(kHistoryAudioHistoryChangeUrl); std::unique_ptr request(CreateRequest( url, std::move(completion_callback), partial_traffic_annotation)); base::DictionaryValue enable_audio_history; enable_audio_history.SetBoolean("enable_history_recording", new_enabled_value); enable_audio_history.SetString("client", "audio"); std::string post_data; base::JSONWriter::Write(enable_audio_history, &post_data); request->SetPostData(post_data); request->Start(); Request* request_ptr = request.get(); pending_audio_history_requests_[request_ptr] = std::move(request); } size_t WebHistoryService::GetNumberOfPendingAudioHistoryRequests() { return pending_audio_history_requests_.size(); } void WebHistoryService::QueryWebAndAppActivity( QueryWebAndAppActivityCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce( &WebHistoryService::QueryWebAndAppActivityCompletionCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); GURL url(kQueryWebAndAppActivityUrl); Request* request = CreateRequest(url, std::move(completion_callback), partial_traffic_annotation); pending_web_and_app_activity_requests_[request] = base::WrapUnique(request); request->Start(); } #if !defined(TOOLKIT_QT) void WebHistoryService::QueryOtherFormsOfBrowsingHistory( version_info::Channel channel, QueryOtherFormsOfBrowsingHistoryCallback callback, const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) { // Wrap the original callback into a generic completion callback. CompletionCallback completion_callback = base::BindOnce( &WebHistoryService::QueryOtherFormsOfBrowsingHistoryCompletionCallback, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); // Find the Sync request URL. GURL url = syncer::GetSyncServiceURL(*base::CommandLine::ForCurrentProcess(), channel); GURL::Replacements replace_path; std::string new_path = url.path() + kQueryOtherFormsOfBrowsingHistoryUrlSuffix; replace_path.SetPathStr(new_path); url = url.ReplaceComponents(replace_path); DCHECK(url.is_valid()); Request* request = CreateRequest(url, std::move(completion_callback), partial_traffic_annotation); // Set the Sync-specific user agent. request->SetUserAgent(syncer::MakeUserAgentForSync(channel)); pending_other_forms_of_browsing_history_requests_[request] = base::WrapUnique(request); // Set the request protobuf. sync_pb::HistoryStatusRequest request_proto; std::string post_data; request_proto.SerializeToString(&post_data); request->SetPostDataAndType(post_data, kSyncProtoMimeType); request->Start(); } #endif // !defined(TOOLKIT_QT) // static void WebHistoryService::QueryHistoryCompletionCallback( WebHistoryService::QueryWebHistoryCallback callback, WebHistoryService::Request* request, bool success) { std::unique_ptr response_value; if (success) response_value = ReadResponse(request); std::move(callback).Run(request, response_value.get()); } void WebHistoryService::ExpireHistoryCompletionCallback( WebHistoryService::ExpireWebHistoryCallback callback, WebHistoryService::Request* request, bool success) { std::unique_ptr request_ptr = std::move(pending_expire_requests_[request]); pending_expire_requests_.erase(request); std::unique_ptr response_value; if (success) { response_value = ReadResponse(request); if (response_value) response_value->GetString("version_info", &server_version_info_); } // Inform the observers about the history deletion. if (response_value.get() && success) { for (WebHistoryServiceObserver& observer : observer_list_) observer.OnWebHistoryDeleted(); } std::move(callback).Run(response_value.get() && success); } void WebHistoryService::AudioHistoryCompletionCallback( WebHistoryService::AudioWebHistoryCallback callback, WebHistoryService::Request* request, bool success) { std::unique_ptr request_ptr = std::move(pending_audio_history_requests_[request]); pending_audio_history_requests_.erase(request); std::unique_ptr response_value; bool enabled_value = false; if (success) { response_value = ReadResponse(request); if (response_value) response_value->GetBoolean("history_recording_enabled", &enabled_value); } // If there is no response_value, then for our purposes, the request has // failed, despite receiving a true `success` value. This can happen if // the user is offline. std::move(callback).Run(success && response_value, enabled_value); } void WebHistoryService::QueryWebAndAppActivityCompletionCallback( WebHistoryService::QueryWebAndAppActivityCallback callback, WebHistoryService::Request* request, bool success) { std::unique_ptr request_ptr = std::move(pending_web_and_app_activity_requests_[request]); pending_web_and_app_activity_requests_.erase(request); std::unique_ptr response_value; bool web_and_app_activity_enabled = false; if (success) { response_value = ReadResponse(request); if (response_value) { response_value->GetBoolean("history_recording_enabled", &web_and_app_activity_enabled); } } std::move(callback).Run(web_and_app_activity_enabled); } #if !defined(TOOLKIT_QT) void WebHistoryService::QueryOtherFormsOfBrowsingHistoryCompletionCallback( WebHistoryService::QueryOtherFormsOfBrowsingHistoryCallback callback, WebHistoryService::Request* request, bool success) { std::unique_ptr request_ptr = std::move(pending_other_forms_of_browsing_history_requests_[request]); pending_other_forms_of_browsing_history_requests_.erase(request); bool has_other_forms_of_browsing_history = false; if (success && request->GetResponseCode() == net::HTTP_OK) { sync_pb::HistoryStatusResponse history_status; if (history_status.ParseFromString(request->GetResponseBody())) has_other_forms_of_browsing_history = history_status.has_derived_data(); } std::move(callback).Run(has_other_forms_of_browsing_history); } #endif // !defined(TOOLKIT_QT) } // namespace history