blob: dbdff2f77127935db6bb98bd73eb010934810615 [file] [log] [blame]
// Copyright 2020 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/windows_spell_checker.h"
#include <objidl.h>
#include <spellcheck.h>
#include <windows.foundation.collections.h>
#include <windows.globalization.h>
#include <windows.system.userprofile.h>
#include <winnls.h> // ResolveLocaleName
#include <wrl/client.h>
#include <algorithm>
#include <codecvt>
#include <locale>
#include <string>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/single_thread_task_runner_thread_mode.h"
#include "base/task_runner_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/win/com_init_util.h"
#include "base/win/core_winrt_util.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_hstring.h"
#include "build/build_config.h"
#include "components/spellcheck/browser/spellcheck_host_metrics.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/common/spellcheck_result.h"
#include "components/spellcheck/spellcheck_buildflags.h"
WindowsSpellChecker::BackgroundHelper::BackgroundHelper(
scoped_refptr<base::SingleThreadTaskRunner> background_task_runner)
: background_task_runner_(std::move(background_task_runner)) {}
WindowsSpellChecker::BackgroundHelper::~BackgroundHelper() {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
}
void WindowsSpellChecker::BackgroundHelper::CreateSpellCheckerFactory() {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
base::win::AssertComApartmentType(base::win::ComApartmentType::STA);
if (!spellcheck::WindowsVersionSupportsSpellchecker() ||
FAILED(::CoCreateInstance(__uuidof(::SpellCheckerFactory), nullptr,
(CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER),
IID_PPV_ARGS(&spell_checker_factory_)))) {
spell_checker_factory_ = nullptr;
}
}
bool WindowsSpellChecker::BackgroundHelper::CreateSpellChecker(
const std::string& lang_tag) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
if (!IsSpellCheckerFactoryInitialized())
return false;
if (SpellCheckerReady(lang_tag))
return true;
if (!IsLanguageSupported(lang_tag))
return false;
Microsoft::WRL::ComPtr<ISpellChecker> spell_checker;
std::wstring bcp47_language_tag = base::UTF8ToWide(lang_tag);
HRESULT hr = spell_checker_factory_->CreateSpellChecker(
bcp47_language_tag.c_str(), &spell_checker);
if (SUCCEEDED(hr)) {
spell_checker_map_.insert({lang_tag, spell_checker});
return true;
}
return false;
}
void WindowsSpellChecker::BackgroundHelper::DisableSpellChecker(
const std::string& lang_tag) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
if (!IsSpellCheckerFactoryInitialized())
return;
auto it = spell_checker_map_.find(lang_tag);
if (it != spell_checker_map_.end()) {
spell_checker_map_.erase(it);
}
}
std::vector<SpellCheckResult>
WindowsSpellChecker::BackgroundHelper::RequestTextCheckForAllLanguages(
int document_tag,
const base::string16& text) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
// Construct a map to store spellchecking results. The key of the map is a
// tuple which contains the start index and the word length of the misspelled
// word. The value of the map is a vector which contains suggestion lists for
// each available language. This allows to quickly see if all languages agree
// about a misspelling, and makes it easier to evenly pick suggestions from
// all the different languages.
std::map<std::tuple<ULONG, ULONG>, spellcheck::PerLanguageSuggestions>
result_map;
std::wstring word_to_check_wide(base::UTF16ToWide(text));
for (auto it = spell_checker_map_.begin(); it != spell_checker_map_.end();
++it) {
Microsoft::WRL::ComPtr<IEnumSpellingError> spelling_errors;
HRESULT hr = it->second->ComprehensiveCheck(word_to_check_wide.c_str(),
&spelling_errors);
if (SUCCEEDED(hr) && spelling_errors) {
do {
Microsoft::WRL::ComPtr<ISpellingError> spelling_error;
ULONG start_index = 0;
ULONG error_length = 0;
CORRECTIVE_ACTION action = CORRECTIVE_ACTION_NONE;
hr = spelling_errors->Next(&spelling_error);
if (SUCCEEDED(hr) && spelling_error &&
SUCCEEDED(spelling_error->get_StartIndex(&start_index)) &&
SUCCEEDED(spelling_error->get_Length(&error_length)) &&
SUCCEEDED(spelling_error->get_CorrectiveAction(&action)) &&
(action == CORRECTIVE_ACTION_GET_SUGGESTIONS ||
action == CORRECTIVE_ACTION_REPLACE)) {
std::vector<base::string16> suggestions;
FillSuggestionList(it->first, text.substr(start_index, error_length),
&suggestions);
result_map[std::tuple<ULONG, ULONG>(start_index, error_length)]
.push_back(suggestions);
}
} while (hr == S_OK);
}
}
std::vector<SpellCheckResult> final_results;
for (auto it = result_map.begin(); it != result_map.end();) {
if (it->second.size() < spell_checker_map_.size()) {
// Some languages considered this correctly spelled, so ignore this
// result.
it = result_map.erase(it);
} else {
std::vector<base::string16> evenly_filled_suggestions;
spellcheck::FillSuggestions(/*suggestions_list=*/it->second,
&evenly_filled_suggestions);
final_results.push_back(SpellCheckResult(
SpellCheckResult::Decoration::SPELLING, std::get<0>(it->first),
std::get<1>(it->first), evenly_filled_suggestions));
++it;
}
}
return final_results;
}
spellcheck::PerLanguageSuggestions
WindowsSpellChecker::BackgroundHelper::GetPerLanguageSuggestions(
const base::string16& word) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
spellcheck::PerLanguageSuggestions suggestions;
std::vector<base::string16> language_suggestions;
for (auto it = spell_checker_map_.begin(); it != spell_checker_map_.end();
++it) {
language_suggestions.clear();
FillSuggestionList(it->first, word, &language_suggestions);
suggestions.push_back(language_suggestions);
}
return suggestions;
}
void WindowsSpellChecker::BackgroundHelper::FillSuggestionList(
const std::string& lang_tag,
const base::string16& wrong_word,
std::vector<base::string16>* optional_suggestions) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
std::wstring word_wide(base::UTF16ToWide(wrong_word));
Microsoft::WRL::ComPtr<IEnumString> suggestions;
HRESULT hr =
GetSpellChecker(lang_tag)->Suggest(word_wide.c_str(), &suggestions);
// Populate the vector of WideStrings.
while (hr == S_OK) {
base::win::ScopedCoMem<wchar_t> suggestion;
hr = suggestions->Next(1, &suggestion, nullptr);
if (hr == S_OK) {
base::string16 utf16_suggestion;
if (base::WideToUTF16(suggestion.get(), wcslen(suggestion),
&utf16_suggestion)) {
optional_suggestions->push_back(utf16_suggestion);
}
}
}
}
void WindowsSpellChecker::BackgroundHelper::AddWordForAllLanguages(
const base::string16& word) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
for (auto it = spell_checker_map_.begin(); it != spell_checker_map_.end();
++it) {
std::wstring word_to_add_wide(base::UTF16ToWide(word));
it->second->Add(word_to_add_wide.c_str());
}
}
void WindowsSpellChecker::BackgroundHelper::RemoveWordForAllLanguages(
const base::string16& word) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
for (auto it = spell_checker_map_.begin(); it != spell_checker_map_.end();
++it) {
std::wstring word_to_remove_wide(base::UTF16ToWide(word));
Microsoft::WRL::ComPtr<ISpellChecker2> spell_checker_2;
it->second->QueryInterface(IID_PPV_ARGS(&spell_checker_2));
if (spell_checker_2 != nullptr) {
spell_checker_2->Remove(word_to_remove_wide.c_str());
}
}
}
void WindowsSpellChecker::BackgroundHelper::IgnoreWordForAllLanguages(
const base::string16& word) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
for (auto it = spell_checker_map_.begin(); it != spell_checker_map_.end();
++it) {
std::wstring word_to_ignore_wide(base::UTF16ToWide(word));
it->second->Ignore(word_to_ignore_wide.c_str());
}
}
bool WindowsSpellChecker::BackgroundHelper::IsLanguageSupported(
const std::string& lang_tag) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
if (!IsSpellCheckerFactoryInitialized()) {
// The native spellchecker creation failed; no language is supported.
return false;
}
BOOL is_language_supported = (BOOL) false;
std::wstring bcp47_language_tag = base::UTF8ToWide(lang_tag);
HRESULT hr = spell_checker_factory_->IsSupported(bcp47_language_tag.c_str(),
&is_language_supported);
return SUCCEEDED(hr) && is_language_supported;
}
LocalesSupportInfo
WindowsSpellChecker::BackgroundHelper::DetermineLocalesSupport(
const std::vector<std::string>& locales) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
size_t locales_supported_by_hunspell_and_native = 0;
size_t locales_supported_by_hunspell_only = 0;
size_t locales_supported_by_native_only = 0;
size_t unsupported_locales = 0;
for (const auto& lang : locales) {
bool hunspell_support =
!spellcheck::GetCorrespondingSpellCheckLanguage(lang).empty();
bool native_support = this->IsLanguageSupported(lang);
if (hunspell_support && native_support) {
locales_supported_by_hunspell_and_native++;
} else if (hunspell_support && !native_support) {
locales_supported_by_hunspell_only++;
} else if (!hunspell_support && native_support) {
locales_supported_by_native_only++;
} else {
unsupported_locales++;
}
}
return LocalesSupportInfo{locales_supported_by_hunspell_and_native,
locales_supported_by_hunspell_only,
locales_supported_by_native_only,
unsupported_locales};
}
bool WindowsSpellChecker::BackgroundHelper::IsSpellCheckerFactoryInitialized() {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
return spell_checker_factory_ != nullptr;
}
bool WindowsSpellChecker::BackgroundHelper::SpellCheckerReady(
const std::string& lang_tag) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
return spell_checker_map_.find(lang_tag) != spell_checker_map_.end();
}
Microsoft::WRL::ComPtr<ISpellChecker>
WindowsSpellChecker::BackgroundHelper::GetSpellChecker(
const std::string& lang_tag) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
DCHECK(SpellCheckerReady(lang_tag));
return spell_checker_map_.find(lang_tag)->second;
}
void WindowsSpellChecker::BackgroundHelper::RecordChromeLocalesStats(
const std::vector<std::string> chrome_locales,
SpellCheckHostMetrics* metrics) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
DCHECK(metrics);
if (!IsSpellCheckerFactoryInitialized()) {
// The native spellchecker creation failed. Do not record any metrics.
return;
}
const auto& locales_info = DetermineLocalesSupport(chrome_locales);
metrics->RecordAcceptLanguageStats(locales_info);
}
void WindowsSpellChecker::BackgroundHelper::RecordSpellcheckLocalesStats(
const std::vector<std::string> spellcheck_locales,
SpellCheckHostMetrics* metrics) {
DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
DCHECK(metrics);
if (!IsSpellCheckerFactoryInitialized()) {
// The native spellchecker creation failed. Do not record any metrics.
return;
}
const auto& locales_info = DetermineLocalesSupport(spellcheck_locales);
metrics->RecordSpellcheckLanguageStats(locales_info);
}
WindowsSpellChecker::WindowsSpellChecker(
scoped_refptr<base::SingleThreadTaskRunner> background_task_runner)
: background_task_runner_(background_task_runner) {
background_helper_ = std::make_unique<WindowsSpellChecker::BackgroundHelper>(
std::move(background_task_runner));
background_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&BackgroundHelper::CreateSpellCheckerFactory,
base::Unretained(background_helper_.get())));
}
WindowsSpellChecker::~WindowsSpellChecker() {
background_task_runner_->DeleteSoon(FROM_HERE, std::move(background_helper_));
}
void WindowsSpellChecker::CreateSpellChecker(
const std::string& lang_tag,
base::OnceCallback<void(bool)> callback) {
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(&BackgroundHelper::CreateSpellChecker,
base::Unretained(background_helper_.get()), lang_tag),
std::move(callback));
}
void WindowsSpellChecker::DisableSpellChecker(const std::string& lang_tag) {
background_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&BackgroundHelper::DisableSpellChecker,
base::Unretained(background_helper_.get()), lang_tag));
}
void WindowsSpellChecker::RequestTextCheck(
int document_tag,
const base::string16& text,
spellcheck_platform::TextCheckCompleteCallback callback) {
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(&BackgroundHelper::RequestTextCheckForAllLanguages,
base::Unretained(background_helper_.get()), document_tag,
text),
std::move(callback));
}
void WindowsSpellChecker::GetPerLanguageSuggestions(
const base::string16& word,
spellcheck_platform::GetSuggestionsCallback callback) {
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(&BackgroundHelper::GetPerLanguageSuggestions,
base::Unretained(background_helper_.get()), word),
std::move(callback));
}
void WindowsSpellChecker::AddWordForAllLanguages(const base::string16& word) {
background_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&BackgroundHelper::AddWordForAllLanguages,
base::Unretained(background_helper_.get()), word));
}
void WindowsSpellChecker::RemoveWordForAllLanguages(
const base::string16& word) {
background_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&BackgroundHelper::RemoveWordForAllLanguages,
base::Unretained(background_helper_.get()), word));
}
void WindowsSpellChecker::IgnoreWordForAllLanguages(
const base::string16& word) {
background_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&BackgroundHelper::IgnoreWordForAllLanguages,
base::Unretained(background_helper_.get()), word));
}
void WindowsSpellChecker::IsLanguageSupported(
const std::string& lang_tag,
base::OnceCallback<void(bool)> callback) {
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(&BackgroundHelper::IsLanguageSupported,
base::Unretained(background_helper_.get()), lang_tag),
std::move(callback));
}
void WindowsSpellChecker::RecordChromeLocalesStats(
const std::vector<std::string> chrome_locales,
SpellCheckHostMetrics* metrics) {
background_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&BackgroundHelper::RecordChromeLocalesStats,
base::Unretained(background_helper_.get()),
std::move(chrome_locales), metrics));
}
void WindowsSpellChecker::RecordSpellcheckLocalesStats(
const std::vector<std::string> spellcheck_locales,
SpellCheckHostMetrics* metrics) {
background_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&BackgroundHelper::RecordSpellcheckLocalesStats,
base::Unretained(background_helper_.get()),
std::move(spellcheck_locales), metrics));
}