| // 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/spellcheck/browser/spelling_service_client.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/string_escape.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/spellcheck/browser/pref_names.h" |
| #include "components/spellcheck/common/spellcheck_common.h" |
| #include "components/spellcheck/common/spellcheck_features.h" |
| #include "components/spellcheck/common/spellcheck_result.h" |
| #include "components/user_prefs/user_prefs.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/load_flags.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 { |
| |
| // The old JSON-RPC endpoint for requesting spell checking and sending user |
| // feedback. |
| const char kSpellingServiceRpcURL[] = "https://www.googleapis.com/rpc"; |
| |
| // The new REST endpoint for requesting spell checking and sending user |
| // feedback. |
| const char kSpellingServiceRestURL[] = |
| "https://www.googleapis.com/spelling/v%d/spelling/check?key=%s"; |
| |
| // The spellcheck suggestions object key in the JSON response from the spelling |
| // service when using the JSON-RPC endpoint. |
| const char kMisspellingsRpcPath[] = "result.spellingCheckResponse.misspellings"; |
| |
| // The spellcheck suggestions object key in the JSON response from the spelling |
| // service when using the REST endpoint. |
| const char kMisspellingsRestPath[] = "spellingCheckResponse.misspellings"; |
| |
| // The location of error messages in JSON response from spelling service. |
| const char kErrorPath[] = "error"; |
| |
| // Languages currently supported by SPELLCHECK. |
| const char* const kValidLanguages[] = {"en", "es", "fi", "da"}; |
| |
| } // namespace |
| |
| SpellingServiceClient::SpellingServiceClient() = default; |
| |
| SpellingServiceClient::~SpellingServiceClient() = default; |
| |
| bool SpellingServiceClient::RequestTextCheck( |
| content::BrowserContext* context, |
| ServiceType type, |
| const base::string16& text, |
| TextCheckCompleteCallback callback) { |
| DCHECK(type == SUGGEST || type == SPELLCHECK); |
| if (!context || !IsAvailable(context, type)) { |
| std::move(callback).Run(false, text, std::vector<SpellCheckResult>()); |
| return false; |
| } |
| const PrefService* pref = user_prefs::UserPrefs::Get(context); |
| DCHECK(pref); |
| |
| std::string dictionary; |
| pref->GetList(spellcheck::prefs::kSpellCheckDictionaries) |
| ->GetString(0, &dictionary); |
| |
| std::string language_code; |
| std::string country_code; |
| spellcheck::GetISOLanguageCountryCodeFromLocale(dictionary, &language_code, |
| &country_code); |
| |
| // Replace typographical apostrophes with typewriter apostrophes, so that |
| // server word breaker behaves correctly. |
| const base::char16 kApostrophe = 0x27; |
| const base::char16 kRightSingleQuotationMark = 0x2019; |
| base::string16 text_copy = text; |
| std::replace(text_copy.begin(), text_copy.end(), kRightSingleQuotationMark, |
| kApostrophe); |
| |
| std::string api_key = google_apis::GetAPIKey(); |
| std::string encoded_text = base::GetQuotedJSONString(text_copy); |
| std::string request_body; |
| |
| if (base::FeatureList::IsEnabled(spellcheck::kSpellingServiceRestApi)) { |
| static const char kSpellingRequestRestBodyTemplate[] = |
| "{" |
| "\"text\":%s," |
| "\"language\":\"%s\"," |
| "\"originCountry\":\"%s\"" |
| "}"; |
| |
| request_body = base::StringPrintf( |
| kSpellingRequestRestBodyTemplate, encoded_text.c_str(), |
| language_code.c_str(), country_code.c_str()); |
| } else { |
| static const char kSpellingRequestRpcBodyTemplate[] = |
| "{" |
| "\"method\":\"spelling.check\"," |
| "\"apiVersion\":\"v%d\"," |
| "\"params\":{" |
| "\"text\":%s," |
| "\"language\":\"%s\"," |
| "\"originCountry\":\"%s\"," |
| "\"key\":%s" |
| "}" |
| "}"; |
| |
| request_body = base::StringPrintf( |
| kSpellingRequestRpcBodyTemplate, type, encoded_text.c_str(), |
| language_code.c_str(), country_code.c_str(), |
| base::GetQuotedJSONString(api_key).c_str()); |
| } |
| |
| // Create traffic annotation tag. |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("spellcheck_lookup", R"( |
| semantics { |
| sender: "Online Spellcheck" |
| description: |
| "Chromium can provide smarter spell-checking, by sending the text " |
| "that the users type into the browser, to Google's servers. This" |
| "allows users to use the same spell-checking technology used by " |
| "Google products, such as Docs. If the feature is enabled, " |
| "Chromium will send the entire contents of text fields as user " |
| "types them to Google, along with the browser’s default language. " |
| "Google returns a list of suggested spellings, which will be " |
| "displayed in the context menu." |
| trigger: "User types text into a text field or asks to correct a " |
| "misspelled word." |
| data: "Text a user has typed into a text field. No user identifier " |
| "is sent along with the text." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "Users can enable or disable this feature via 'Enhanced spell " |
| "check' in Chromium's settings under 'Sync and Google services'. " |
| "The feature is disabled by default." |
| chrome_policy { |
| SpellCheckServiceEnabled { |
| policy_options {mode: MANDATORY} |
| SpellCheckServiceEnabled: false |
| } |
| } |
| })"); |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = BuildEndpointUrl(type); |
| resource_request->allow_credentials = false; |
| resource_request->method = "POST"; |
| |
| std::unique_ptr<network::SimpleURLLoader> simple_url_loader = |
| network::SimpleURLLoader::Create(std::move(resource_request), |
| traffic_annotation); |
| simple_url_loader->AttachStringForUpload(request_body, "application/json"); |
| |
| auto it = spellcheck_loaders_.insert( |
| spellcheck_loaders_.begin(), |
| std::make_unique<TextCheckCallbackData>(std::move(simple_url_loader), |
| std::move(callback), text)); |
| network::SimpleURLLoader* loader = it->get()->simple_url_loader.get(); |
| auto url_loader_factory = |
| url_loader_factory_for_testing_ |
| ? url_loader_factory_for_testing_ |
| : content::BrowserContext::GetDefaultStoragePartition(context) |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| url_loader_factory.get(), |
| base::BindOnce(&SpellingServiceClient::OnSimpleLoaderComplete, |
| base::Unretained(this), std::move(it), |
| base::TimeTicks::Now())); |
| return true; |
| } |
| |
| bool SpellingServiceClient::IsAvailable(content::BrowserContext* context, |
| ServiceType type) { |
| const PrefService* pref = user_prefs::UserPrefs::Get(context); |
| DCHECK(pref); |
| // If prefs don't allow spellchecking, if the context is off the record, or if |
| // multilingual spellchecking is enabled the spelling service should be |
| // unavailable. |
| if (!pref->GetBoolean(spellcheck::prefs::kSpellCheckEnable) || |
| !pref->GetBoolean(spellcheck::prefs::kSpellCheckUseSpellingService) || |
| context->IsOffTheRecord()) |
| return false; |
| |
| // If the locale for spelling has not been set, the user has not decided to |
| // use spellcheck so we don't do anything remote (suggest or spelling). |
| std::string locale; |
| pref->GetList(spellcheck::prefs::kSpellCheckDictionaries) |
| ->GetString(0, &locale); |
| if (locale.empty()) |
| return false; |
| |
| // Finally, if all options are available, we only enable only SUGGEST |
| // if SPELLCHECK is not available for our language because SPELLCHECK results |
| // are a superset of SUGGEST results. |
| for (const char* language : kValidLanguages) { |
| if (!locale.compare(0, 2, language)) |
| return type == SPELLCHECK; |
| } |
| |
| // Only SUGGEST is allowed. |
| return type == SUGGEST; |
| } |
| |
| void SpellingServiceClient::SetURLLoaderFactoryForTesting( |
| scoped_refptr<network::SharedURLLoaderFactory> |
| url_loader_factory_for_testing) { |
| url_loader_factory_for_testing_ = std::move(url_loader_factory_for_testing); |
| } |
| |
| GURL SpellingServiceClient::BuildEndpointUrl(int type) { |
| if (base::FeatureList::IsEnabled(spellcheck::kSpellingServiceRestApi)) { |
| return GURL(base::StringPrintf(kSpellingServiceRestURL, type, |
| google_apis::GetAPIKey().c_str())); |
| } else { |
| return GURL(kSpellingServiceRpcURL); |
| } |
| } |
| |
| bool SpellingServiceClient::ParseResponse( |
| const std::string& data, |
| std::vector<SpellCheckResult>* results) { |
| // Data is in the following format: |
| // * result: (only in the RPC API; skipped for the REST API) A root object |
| // * spellingCheckResponse: A wrapper object containing the response |
| // * mispellings: (optional Array<object>) A list of mistakes for the |
| // requested text, with the following format: |
| // * charStart: (number) The zero-based start of the misspelled region |
| // * charLength: (number) The length of the misspelled region |
| // * suggestions: (Array<object>) The suggestions for the misspelled |
| // text, with the following format: |
| // * suggestion: (string) the suggestion for the correct text |
| // * canAutoCorrect (optional boolean) Whether we can use the first |
| // suggestion for auto-correction |
| // |
| // Example response for "duck goes quisk": |
| // { |
| // "result": { // (Only in the RPC API) |
| // "spellingCheckResponse": { |
| // "misspellings": [{ |
| // "charStart": 10, |
| // "charLength": 5, |
| // "suggestions": [{ |
| // "suggestion": "quack" |
| // }], |
| // "canAutoCorrect": false |
| // }] |
| // } |
| // } |
| // } |
| // |
| // If the service is not available, the Spelling service returns JSON with an |
| // error: |
| // { |
| // "error": { |
| // "code": 400, |
| // "message": "Bad Request", |
| // "data": [...] |
| // } |
| // } |
| |
| std::unique_ptr<base::DictionaryValue> value( |
| static_cast<base::DictionaryValue*>( |
| base::JSONReader::ReadDeprecated(data, |
| base::JSON_ALLOW_TRAILING_COMMAS) |
| .release())); |
| if (!value || !value->is_dict()) |
| return false; |
| |
| // Check for errors from spelling service. |
| base::DictionaryValue* error = nullptr; |
| if (value->GetDictionary(kErrorPath, &error)) |
| return false; |
| |
| // Retrieve the array of Misspelling objects. When the input text does not |
| // have misspelled words, it returns an empty JSON. (In this case, its HTTP |
| // status is 200.) We just return true for this case. |
| base::ListValue* misspellings = nullptr; |
| std::string mispellingsPath = |
| base::FeatureList::IsEnabled(spellcheck::kSpellingServiceRestApi) |
| ? kMisspellingsRestPath |
| : kMisspellingsRpcPath; |
| if (!value->GetList(mispellingsPath, &misspellings)) |
| return true; |
| |
| for (size_t i = 0; i < misspellings->GetSize(); ++i) { |
| // Retrieve the i-th misspelling region and put it to the given vector. When |
| // the Spelling service sends two or more suggestions, we read only the |
| // first one because SpellCheckResult can store only one suggestion. |
| base::DictionaryValue* misspelling = nullptr; |
| if (!misspellings->GetDictionary(i, &misspelling)) |
| return false; |
| |
| int start = 0; |
| int length = 0; |
| base::ListValue* suggestions = nullptr; |
| if (!misspelling->GetInteger("charStart", &start) || |
| !misspelling->GetInteger("charLength", &length) || |
| !misspelling->GetList("suggestions", &suggestions)) { |
| return false; |
| } |
| |
| base::DictionaryValue* suggestion = nullptr; |
| base::string16 replacement; |
| if (!suggestions->GetDictionary(0, &suggestion) || |
| !suggestion->GetString("suggestion", &replacement)) { |
| return false; |
| } |
| SpellCheckResult result(SpellCheckResult::SPELLING, start, length, |
| replacement); |
| results->push_back(result); |
| } |
| return true; |
| } |
| |
| SpellingServiceClient::TextCheckCallbackData::TextCheckCallbackData( |
| std::unique_ptr<network::SimpleURLLoader> simple_url_loader, |
| TextCheckCompleteCallback callback, |
| base::string16 text) |
| : simple_url_loader(std::move(simple_url_loader)), |
| callback(std::move(callback)), |
| text(text) {} |
| |
| SpellingServiceClient::TextCheckCallbackData::~TextCheckCallbackData() {} |
| |
| void SpellingServiceClient::OnSimpleLoaderComplete( |
| SpellCheckLoaderList::iterator it, |
| base::TimeTicks request_start, |
| std::unique_ptr<std::string> response_body) { |
| UMA_HISTOGRAM_TIMES("SpellCheck.SpellingService.RequestDuration", |
| base::TimeTicks::Now() - request_start); |
| |
| TextCheckCompleteCallback callback = std::move(it->get()->callback); |
| base::string16 text = it->get()->text; |
| bool success = false; |
| std::vector<SpellCheckResult> results; |
| if (response_body) |
| success = ParseResponse(*response_body, &results); |
| |
| int response_code = net::ERR_FAILED; |
| auto* resp_info = it->get()->simple_url_loader->ResponseInfo(); |
| if (resp_info && resp_info->headers) { |
| response_code = resp_info->headers->response_code(); |
| } |
| |
| ServiceRequestResultType result_type = |
| ServiceRequestResultType::kRequestFailure; |
| if (success) { |
| result_type = results.empty() |
| ? ServiceRequestResultType::kSuccessEmpty |
| : ServiceRequestResultType::kSuccessWithSuggestions; |
| } |
| |
| base::UmaHistogramSparse("SpellCheck.SpellingService.RequestHttpResponseCode", |
| response_code); |
| UMA_HISTOGRAM_ENUMERATION("SpellCheck.SpellingService.RequestResultType", |
| result_type); |
| |
| spellcheck_loaders_.erase(it); |
| std::move(callback).Run(success, text, results); |
| } |