blob: 3928d6fdbda8aac5fb89ab148adaa18218e135f8 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/spellchecker/spellcheck_service.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include <utility>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/supports_user_data.h"
#include "base/synchronization/waitable_event.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_member.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/browser/spellcheck_host_metrics.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/browser/spelling_service_client.h"
#include "components/spellcheck/common/spellcheck.mojom.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "ui/base/l10n/l10n_util.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#endif
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/spellcheck/browser/windows_spell_checker.h"
#endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
using content::BrowserThread;
namespace {
SpellcheckService::SpellCheckerBinder& GetSpellCheckerBinderOverride() {
static base::NoDestructor<SpellcheckService::SpellCheckerBinder> binder;
return *binder;
}
// Only record spelling-configuration metrics for profiles in which the user
// can configure spelling.
bool RecordSpellingConfigurationMetrics(content::BrowserContext* context) {
return profiles::IsRegularUserProfile(Profile::FromBrowserContext(context));
}
} // namespace
// TODO(rlp): I do not like globals, but keeping these for now during
// transition.
// An event used by browser tests to receive status events from this class and
// its derived classes.
base::WaitableEvent* g_status_event = nullptr;
SpellcheckService::EventType g_status_type =
SpellcheckService::BDICT_NOTINITIALIZED;
SpellcheckService::SpellcheckService(content::BrowserContext* context)
: context_(context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PrefService* prefs = user_prefs::UserPrefs::Get(context);
pref_change_registrar_.Init(prefs);
StringListPrefMember dictionaries_pref;
dictionaries_pref.Init(spellcheck::prefs::kSpellCheckDictionaries, prefs);
std::string first_of_dictionaries;
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
// Ensure that the renderer always knows the platform spellchecking
// language. This language is used for initialization of the text iterator.
// If the iterator is not initialized, then the context menu does not show
// spellcheck suggestions.
// No migration is necessary, because the spellcheck language preference is
// not user visible or modifiable in Chrome on Mac.
dictionaries_pref.SetValue(std::vector<std::string>(
1, spellcheck_platform::GetSpellCheckerLanguage()));
first_of_dictionaries = dictionaries_pref.GetValue().front();
#else
// Migrate preferences from single-language to multi-language schema.
StringPrefMember single_dictionary_pref;
single_dictionary_pref.Init(spellcheck::prefs::kSpellCheckDictionary, prefs);
std::string single_dictionary = single_dictionary_pref.GetValue();
if (!dictionaries_pref.GetValue().empty())
first_of_dictionaries = dictionaries_pref.GetValue().front();
if (first_of_dictionaries.empty() && !single_dictionary.empty()) {
first_of_dictionaries = single_dictionary;
dictionaries_pref.SetValue(
std::vector<std::string>(1, first_of_dictionaries));
}
single_dictionary_pref.SetValue("");
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
if (!spellcheck::UseBrowserSpellChecker()) {
// A user may have disabled the Windows spellcheck feature after adding
// non-Hunspell supported languages on the language settings page. Remove
// preferences for non-Hunspell languages so that there is no attempt to
// load a non-existent Hunspell dictionary, and so that Hunspell
// spellchecking isn't broken because of the failed load.
ScopedListPrefUpdate update(prefs,
spellcheck::prefs::kSpellCheckDictionaries);
update->EraseIf([](const base::Value& entry) {
return spellcheck::GetCorrespondingSpellCheckLanguage(entry.GetString())
.empty();
});
}
#endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
pref_change_registrar_.Add(
spellcheck::prefs::kSpellCheckDictionaries,
base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
spellcheck::prefs::kSpellCheckForcedDictionaries,
base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
spellcheck::prefs::kSpellCheckBlocklistedDictionaries,
base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
spellcheck::prefs::kSpellCheckUseSpellingService,
base::BindRepeating(&SpellcheckService::OnUseSpellingServiceChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
language::prefs::kAcceptLanguages,
base::BindRepeating(&SpellcheckService::OnAcceptLanguagesChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
spellcheck::prefs::kSpellCheckEnable,
base::BindRepeating(&SpellcheckService::InitForAllRenderers,
base::Unretained(this)));
custom_dictionary_ =
std::make_unique<SpellcheckCustomDictionary>(context_->GetPath());
custom_dictionary_->AddObserver(this);
custom_dictionary_->Load();
#if BUILDFLAG(IS_WIN)
if (spellcheck::UseBrowserSpellChecker() &&
base::FeatureList::IsEnabled(
spellcheck::kWinDelaySpellcheckServiceInit)) {
// If initialization of the spellcheck service is on-demand, it is up to the
// instantiator of the spellcheck service to call InitializeDictionaries
// with a callback.
return;
}
#endif // BUILDFLAG(IS_WIN)
InitializeDictionaries(base::DoNothing());
}
SpellcheckService::~SpellcheckService() {
// Remove pref observers
pref_change_registrar_.RemoveAll();
}
base::WeakPtr<SpellcheckService> SpellcheckService::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
#if !BUILDFLAG(IS_MAC)
// static
void SpellcheckService::GetDictionaries(
content::BrowserContext* browser_context,
std::vector<Dictionary>* dictionaries) {
PrefService* prefs = user_prefs::UserPrefs::Get(browser_context);
std::set<std::string> spellcheck_dictionaries;
for (const auto& value :
prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries)) {
const std::string* dictionary = value.GetIfString();
if (dictionary)
spellcheck_dictionaries.insert(*dictionary);
}
dictionaries->clear();
std::vector<std::string> accept_languages =
base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
for (const auto& accept_language : accept_languages) {
Dictionary dictionary;
#if BUILDFLAG(IS_WIN)
if (spellcheck::UseBrowserSpellChecker()) {
SpellcheckService* spellcheck =
SpellcheckServiceFactory::GetForContext(browser_context);
if (spellcheck && spellcheck->UsesWindowsDictionary(accept_language))
dictionary.language = accept_language;
}
if (dictionary.language.empty()) {
dictionary.language =
spellcheck::GetCorrespondingSpellCheckLanguage(accept_language);
}
#else
dictionary.language =
spellcheck::GetCorrespondingSpellCheckLanguage(accept_language);
#endif // BUILDFLAG(IS_WIN)
if (dictionary.language.empty())
continue;
dictionary.used_for_spellcheck =
spellcheck_dictionaries.count(dictionary.language) > 0;
dictionaries->push_back(dictionary);
}
}
#endif // !BUILDFLAG(IS_MAC)
// static
bool SpellcheckService::SignalStatusEvent(
SpellcheckService::EventType status_type) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!g_status_event)
return false;
g_status_type = status_type;
g_status_event->Signal();
return true;
}
// static
std::string SpellcheckService::GetSupportedAcceptLanguageCode(
const std::string& supported_language_full_tag,
bool generic_only) {
// Default to accept language in hardcoded list of Hunspell dictionaries
// (kSupportedSpellCheckerLanguages).
std::string supported_accept_language =
spellcheck::GetCorrespondingSpellCheckLanguage(
supported_language_full_tag);
if (generic_only) {
supported_accept_language = SpellcheckService::GetLanguageAndScriptTag(
supported_accept_language,
/* include_script_tag= */ false);
}
#if BUILDFLAG(IS_WIN)
if (!spellcheck::UseBrowserSpellChecker())
return supported_accept_language;
// Exclude dictionaries that are for private use, such as "ja-Latn-JP-x-ext".
if (SpellcheckService::HasPrivateUseSubTag(supported_language_full_tag))
return "";
// Collect the hardcoded list of accept-languages supported by the browser,
// that is, languages that can be added as preferred languages in the
// languages settings page.
std::vector<std::string> accept_languages;
l10n_util::GetAcceptLanguages(&accept_languages);
if (generic_only) {
return GetSupportedAcceptLanguageCodeGenericOnly(
supported_language_full_tag, accept_languages);
}
// First try exact match. Per BCP47, tags are in ASCII and should be treated
// as case-insensitive (although there are conventions for the capitalization
// of subtags).
auto iter = std::ranges::find_if(
accept_languages,
[supported_language_full_tag](const auto& accept_language) {
return base::EqualsCaseInsensitiveASCII(supported_language_full_tag,
accept_language);
});
if (iter != accept_languages.end())
return *iter;
// Then try matching just the language and (optional) script subtags, but
// not the region subtag. For example, Edge supports sr-Cyrl-RS as an accept
// language, but not sr-Cyrl-CS. Matching language + script subtags assures
// we get the correct script for spellchecking, and not use sr-Latn-RS if
// language packs for both scripts are installed on the system.
if (!base::Contains(supported_language_full_tag, "-"))
return "";
iter = std::ranges::find_if(
accept_languages,
[supported_language_full_tag](const auto& accept_language) {
return base::EqualsCaseInsensitiveASCII(
SpellcheckService::GetLanguageAndScriptTag(
supported_language_full_tag,
/* include_script_tag= */ true),
SpellcheckService::GetLanguageAndScriptTag(
accept_language,
/* include_script_tag= */ true));
});
if (iter != accept_languages.end())
return *iter;
// Then try just matching the leading language subtag. E.g. Edge supports
// kok as an accept language, but if the Konkani language pack is
// installed the Windows spellcheck API reports kok-Deva-IN for the
// dictionary name.
return GetSupportedAcceptLanguageCodeGenericOnly(supported_language_full_tag,
accept_languages);
#else
return supported_accept_language;
#endif // BUILDFLAG(IS_WIN)
}
#if BUILDFLAG(IS_WIN)
// static
void SpellcheckService::EnableFirstUserLanguageForSpellcheck(
PrefService* prefs) {
// Ensure that spellcheck is enabled for the first language in the
// accept languages list.
base::Value::List user_dictionaries =
prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries).Clone();
std::vector<std::string> user_languages =
base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
// Some first run scenarios will add an accept language to preferences that
// is not found in the hard-coded list in kAcceptLanguageList. Only
// languages in kAcceptLanguageList can be spellchecked. An example is an
// installation on a device where Finnish is the Windows display
// language--the initial accept language preferences are observed to be
// "fi-FI,fi,en-US,en". Only "fi" is contained in kAcceptLanguageList.
std::string first_user_language;
std::vector<std::string> accept_languages;
l10n_util::GetAcceptLanguages(&accept_languages);
for (const auto& user_language : user_languages) {
if (base::Contains(accept_languages, user_language)) {
first_user_language = user_language;
break;
}
}
bool first_user_language_spellchecked = false;
for (const auto& dictionary_value : user_dictionaries) {
first_user_language_spellchecked =
base::Contains(dictionary_value.GetString(), first_user_language);
if (first_user_language_spellchecked)
break;
}
if (!first_user_language_spellchecked) {
user_dictionaries.Insert(user_dictionaries.begin(),
base::Value(first_user_language));
prefs->SetList(spellcheck::prefs::kSpellCheckDictionaries,
std::move(user_dictionaries));
}
}
#endif // BUILDFLAG(IS_WIN)
void SpellcheckService::StartRecordingMetrics(bool spellcheck_enabled) {
metrics_ = std::make_unique<SpellCheckHostMetrics>();
auto record_configuration_metrics =
RecordSpellingConfigurationMetrics(context_);
if (record_configuration_metrics) {
metrics_->RecordEnabledStats(spellcheck_enabled);
}
OnUseSpellingServiceChanged();
#if BUILDFLAG(IS_WIN)
if (record_configuration_metrics) {
RecordChromeLocalesStats();
RecordSpellcheckLocalesStats();
}
#endif // BUILDFLAG(IS_WIN)
}
void SpellcheckService::InitForRenderer(content::RenderProcessHost* host) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
content::BrowserContext* context = host->GetBrowserContext();
if (SpellcheckServiceFactory::GetForContext(context) != this)
return;
const bool enable = IsSpellcheckEnabled();
std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries;
std::vector<std::string> custom_words;
if (enable) {
for (const auto& hunspell_dictionary : hunspell_dictionaries_) {
dictionaries.push_back(spellcheck::mojom::SpellCheckBDictLanguage::New(
hunspell_dictionary->GetDictionaryFile().Duplicate(),
hunspell_dictionary->GetLanguage()));
}
custom_words.assign(custom_dictionary_->GetWords().begin(),
custom_dictionary_->GetWords().end());
} else {
// Disabling spell check should also disable spelling service.
user_prefs::UserPrefs::Get(context)->SetBoolean(
spellcheck::prefs::kSpellCheckUseSpellingService, false);
}
GetSpellCheckerForProcess(host)->Initialize(std::move(dictionaries),
custom_words, enable);
}
SpellCheckHostMetrics* SpellcheckService::GetMetrics() const {
return metrics_.get();
}
SpellcheckCustomDictionary* SpellcheckService::GetCustomDictionary() {
return custom_dictionary_.get();
}
void SpellcheckService::LoadDictionaries() {
hunspell_dictionaries_.clear();
PrefService* prefs = user_prefs::UserPrefs::Get(context_);
DCHECK(prefs);
const base::Value::List& user_dictionaries =
prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries);
const base::Value::List& forced_dictionaries =
prefs->GetList(spellcheck::prefs::kSpellCheckForcedDictionaries);
// Build a lookup of blocked dictionaries to skip loading them.
const base::Value::List& blocked_dictionaries =
prefs->GetList(spellcheck::prefs::kSpellCheckBlocklistedDictionaries);
std::unordered_set<std::string> blocked_dictionaries_lookup;
for (const auto& blocked_dict : blocked_dictionaries) {
blocked_dictionaries_lookup.insert(blocked_dict.GetString());
}
// Merge both lists of dictionaries. Use a set to avoid duplicates.
std::set<std::string> dictionaries;
for (const auto& dictionary_value : user_dictionaries) {
if (blocked_dictionaries_lookup.find(dictionary_value.GetString()) ==
blocked_dictionaries_lookup.end())
dictionaries.insert(dictionary_value.GetString());
}
for (const auto& dictionary_value : forced_dictionaries) {
dictionaries.insert(dictionary_value.GetString());
}
for (const auto& dictionary : dictionaries) {
// The spellcheck language passed to platform APIs may differ from the
// accept language.
std::string platform_spellcheck_language;
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
if (spellcheck::UseBrowserSpellChecker()) {
std::string windows_dictionary_name =
GetSupportedWindowsDictionaryLanguage(dictionary);
if (!windows_dictionary_name.empty()) {
platform_spellcheck_language =
SpellcheckService::GetTagToPassToWindowsSpellchecker(
dictionary, windows_dictionary_name);
}
}
#endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
hunspell_dictionaries_.push_back(
std::make_unique<SpellcheckHunspellDictionary>(
dictionary, platform_spellcheck_language, context_, this));
hunspell_dictionaries_.back()->AddObserver(this);
hunspell_dictionaries_.back()->Load();
}
#if BUILDFLAG(IS_WIN)
if (RecordSpellingConfigurationMetrics(context_)) {
RecordSpellcheckLocalesStats();
}
#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
if (base::FeatureList::IsEnabled(
spellcheck::kWinDelaySpellcheckServiceInit) &&
spellcheck::UseBrowserSpellChecker()) {
// Only want to fire the callback on first call to LoadDictionaries
// originating from InitializeDictionaries, since supported platform
// dictionaries are cached throughout the browser session and not
// dynamically updated. LoadDictionaries can be called multiple times in a
// browser session, even before InitializeDictionaries is called, e.g. when
// language settings are changed.
if (!dictionaries_loaded() && dictionaries_loaded_callback_) {
dictionaries_loaded_ = true;
std::move(dictionaries_loaded_callback_).Run();
}
return;
}
#endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#endif // BUILDFLAG(IS_WIN)
dictionaries_loaded_ = true;
}
const std::vector<std::unique_ptr<SpellcheckHunspellDictionary>>&
SpellcheckService::GetHunspellDictionaries() {
return hunspell_dictionaries_;
}
bool SpellcheckService::IsSpellcheckEnabled() const {
const PrefService* prefs = user_prefs::UserPrefs::Get(context_);
bool enable_if_uninitialized = false;
#if BUILDFLAG(IS_WIN)
if (spellcheck::UseBrowserSpellChecker() &&
base::FeatureList::IsEnabled(
spellcheck::kWinDelaySpellcheckServiceInit)) {
// If initialization of the spellcheck service is on-demand, the
// renderer-side SpellCheck object needs to start out as enabled in order
// for a click on editable content to initialize the spellcheck service.
if (!dictionaries_loaded())
enable_if_uninitialized = true;
}
#endif // BUILDFLAG(IS_WIN)
return prefs->GetBoolean(spellcheck::prefs::kSpellCheckEnable) &&
(!hunspell_dictionaries_.empty() || enable_if_uninitialized);
}
void SpellcheckService::OnRenderProcessHostCreated(
content::RenderProcessHost* host) {
InitForRenderer(host);
}
void SpellcheckService::OnCustomDictionaryLoaded() {
InitForAllRenderers();
}
void SpellcheckService::OnCustomDictionaryChanged(
const SpellcheckCustomDictionary::Change& change) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const std::vector<std::string> additions(change.to_add().begin(),
change.to_add().end());
const std::vector<std::string> deletions(change.to_remove().begin(),
change.to_remove().end());
for (content::RenderProcessHost::iterator it(
content::RenderProcessHost::AllHostsIterator());
!it.IsAtEnd(); it.Advance()) {
content::RenderProcessHost* process = it.GetCurrentValue();
if (!process->IsInitializedAndNotDead())
continue;
GetSpellCheckerForProcess(process)->CustomDictionaryChanged(additions,
deletions);
}
}
void SpellcheckService::OnHunspellDictionaryInitialized(
const std::string& language) {
InitForAllRenderers();
}
void SpellcheckService::OnHunspellDictionaryDownloadBegin(
const std::string& language) {
}
void SpellcheckService::OnHunspellDictionaryDownloadSuccess(
const std::string& language) {
}
void SpellcheckService::OnHunspellDictionaryDownloadFailure(
const std::string& language) {
}
void SpellcheckService::InitializeDictionaries(base::OnceClosure done) {
// The dictionaries only need to be initialized once.
if (dictionaries_loaded()) {
std::move(done).Run();
return;
}
#if BUILDFLAG(IS_WIN)
dictionaries_loaded_callback_ = std::move(done);
// Need to initialize the platform spellchecker in order to record platform
// locale stats even if the platform spellcheck feature is disabled.
InitializePlatformSpellchecker();
#endif // BUILDFLAG(IS_WIN)
PrefService* prefs = user_prefs::UserPrefs::Get(context_);
DCHECK(prefs);
// Instantiates Metrics object for spellchecking to use.
StartRecordingMetrics(
prefs->GetBoolean(spellcheck::prefs::kSpellCheckEnable));
#if BUILDFLAG(IS_WIN)
if (spellcheck::UseBrowserSpellChecker() && platform_spell_checker()) {
spellcheck_platform::RetrieveSpellcheckLanguages(
platform_spell_checker(),
base::BindOnce(&SpellcheckService::InitWindowsDictionaryLanguages,
GetWeakPtr()));
return;
}
#endif // BUILDFLAG(IS_WIN)
// Using Hunspell.
LoadDictionaries();
}
#if BUILDFLAG(IS_WIN)
void SpellcheckService::InitWindowsDictionaryLanguages(
const std::vector<std::string>& windows_spellcheck_languages) {
windows_spellcheck_dictionary_map_.clear();
for (const auto& windows_spellcheck_language : windows_spellcheck_languages) {
std::string accept_language =
SpellcheckService::GetSupportedAcceptLanguageCode(
windows_spellcheck_language, /* generic_only */ false);
AddWindowsSpellcheckDictionary(accept_language,
windows_spellcheck_language);
// There is one unfortunate special case (so far the only one known). The
// accept language "sr" is supported, and if you use it as a display
// language you see Cyrillic script. If a Windows language pack is
// installed that supports "sr-Cyrl-*", mark the "sr" accept language
// as having Windows spellcheck support instead of using Hunspell.
if (base::EqualsCaseInsensitiveASCII(
"sr-Cyrl", SpellcheckService::GetLanguageAndScriptTag(
windows_spellcheck_language,
/* include_script_tag= */ true))) {
AddWindowsSpellcheckDictionary("sr", windows_spellcheck_language);
}
// Add the generic language with the region subtag removed too if it exists
// in the list of accept languages, and use it when calling the Windows
// spellcheck APIs. For example, if the preferred language settings include
// just generic Portuguese (pt), but the Portuguese (Brazil) platform
// language pack (pt-BR) is installed, we want an entry for it so that the
// generic Portuguese language can be enabled for spellchecking. The Windows
// platform spellcheck API has logic to load the pt-BR dictionary if only pt
// is specified as the BCP47 language tag. The use of a map in
// AddWindowsSpellcheckDictionary ensures there won't be duplicate entries
// if a generic language was already added above (ar-SA would already be
// mapped to ar since the accept language ar-SA is not recognized by the
// browser e.g.).
accept_language = SpellcheckService::GetSupportedAcceptLanguageCode(
windows_spellcheck_language, /* generic_only */ true);
AddWindowsSpellcheckDictionary(accept_language, accept_language);
}
// A user may have removed a language pack for a non-Hunspell language after
// enabling it for spellcheck on the language settings page. Remove
// preferences for this language so that there is no attempt to load a
// non-existent Hunspell dictionary, and so that Hunspell spellchecking isn't
// broken because of the failed load. This also handles the case where the
// primary preferred language is enabled for spellchecking during first run,
// but it's now determined that there is neither Windows platform nor Hunspell
// dictionary support for that language.
PrefService* prefs = user_prefs::UserPrefs::Get(context_);
DCHECK(prefs);
// When following object goes out of scope, preference change observers will
// be notified (even if there is no preference change).
ScopedListPrefUpdate update(prefs,
spellcheck::prefs::kSpellCheckDictionaries);
update->EraseIf([this](const base::Value& entry) {
const std::string dictionary_name = entry.GetString();
return (!UsesWindowsDictionary(dictionary_name) &&
spellcheck::GetCorrespondingSpellCheckLanguage(dictionary_name)
.empty());
});
// No need to call LoadDictionaries() as when the ScopedListPrefUpdate object
// goes out of scope, the preference change handler will do this.
}
bool SpellcheckService::UsesWindowsDictionary(
std::string accept_language) const {
return !GetSupportedWindowsDictionaryLanguage(accept_language).empty();
}
#endif // BUILDFLAG(IS_WIN)
// static
void SpellcheckService::OverrideBinderForTesting(SpellCheckerBinder binder) {
GetSpellCheckerBinderOverride() = std::move(binder);
}
// static
std::string SpellcheckService::GetLanguageAndScriptTag(
const std::string& full_tag,
bool include_script_tag) {
if (full_tag.empty())
return "";
std::string language_and_script_tag;
std::vector<std::string> subtags = base::SplitString(
full_tag, "-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
// Language subtag is required, all others optional.
DCHECK_GE(subtags.size(), 1ULL);
std::vector<std::string> subtag_tokens_to_pass;
subtag_tokens_to_pass.push_back(subtags.front());
subtags.erase(subtags.begin());
// The optional script subtag always follows the language subtag, and is 4
// characters in length.
if (include_script_tag) {
if (!subtags.empty() && subtags.front().length() == 4) {
subtag_tokens_to_pass.push_back(subtags.front());
}
}
language_and_script_tag = base::JoinString(subtag_tokens_to_pass, "-");
return language_and_script_tag;
}
#if BUILDFLAG(IS_WIN)
// static
std::string SpellcheckService::GetSupportedAcceptLanguageCodeGenericOnly(
const std::string& supported_language_full_tag,
const std::vector<std::string>& accept_languages) {
auto iter = std::ranges::find_if(
accept_languages,
[supported_language_full_tag](const auto& accept_language) {
return base::EqualsCaseInsensitiveASCII(
SpellcheckService::GetLanguageAndScriptTag(
supported_language_full_tag,
/* include_script_tag= */ false),
SpellcheckService::GetLanguageAndScriptTag(
accept_language,
/* include_script_tag= */ false));
});
if (iter != accept_languages.end()) {
// Special case for Serbian--"sr" implies Cyrillic script. Don't mark it as
// supported for sr-Latn*.
if (base::EqualsCaseInsensitiveASCII(
SpellcheckService::GetLanguageAndScriptTag(
supported_language_full_tag,
/* include_script_tag= */ true),
"sr-Latn")) {
return "";
}
return *iter;
}
return "";
}
// static
bool SpellcheckService::HasPrivateUseSubTag(const std::string& full_tag) {
std::vector<std::string> subtags = base::SplitString(
full_tag, "-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
// Private use subtags are separated from the other subtags by the reserved
// single-character subtag 'x'.
return base::Contains(subtags, "x");
}
// static
std::string SpellcheckService::GetTagToPassToWindowsSpellchecker(
const std::string& accept_language,
const std::string& supported_language_full_tag) {
// First try exact match. Per BCP47, tags are in ASCII and should be treated
// as case-insensitive (although there are conventions for the capitalization
// of subtags, they are sometimes broken).
if (base::EqualsCaseInsensitiveASCII(supported_language_full_tag,
accept_language)) {
// Unambiguous spellcheck dictionary to be used.
return supported_language_full_tag;
}
// Accept language does not match script or region subtags.
// If there is a script subtag, include it, to avoid for example passing
// "sr" which is ambiguous as Serbian can use Cyrillic or Latin script.
// There is one unfortunate special case (so far the only one known). The
// accept language "sr" is supported, and if you use it as a display
// language you see Cyrillic script. However, the Windows spellcheck API
// returns "sr-Latn-*" dictionaries if the unqualified language tag is
// passed. The following forces Windows spellchecking to use Cyrillic script
// in this case, and if the language pack is not installed there will be a
// fallback to Hunspell support when spellchecking is performed.
if (base::EqualsCaseInsensitiveASCII("sr", accept_language))
return "sr-Cyrl";
return SpellcheckService::GetLanguageAndScriptTag(
supported_language_full_tag,
/* include_script_tag= */ true);
}
#endif // BUILDFLAG(IS_WIN)
// static
void SpellcheckService::AttachStatusEvent(base::WaitableEvent* status_event) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
g_status_event = status_event;
}
// static
SpellcheckService::EventType SpellcheckService::GetStatusEvent() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return g_status_type;
}
mojo::Remote<spellcheck::mojom::SpellChecker>
SpellcheckService::GetSpellCheckerForProcess(content::RenderProcessHost* host) {
mojo::Remote<spellcheck::mojom::SpellChecker> spellchecker;
auto receiver = spellchecker.BindNewPipeAndPassReceiver();
auto binder = GetSpellCheckerBinderOverride();
if (binder)
binder.Run(std::move(receiver));
else
host->BindReceiver(std::move(receiver));
return spellchecker;
}
void SpellcheckService::InitForAllRenderers() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
for (content::RenderProcessHost::iterator i(
content::RenderProcessHost::AllHostsIterator());
!i.IsAtEnd(); i.Advance()) {
content::RenderProcessHost* process = i.GetCurrentValue();
if (process && process->GetProcess().Handle())
InitForRenderer(process);
}
}
void SpellcheckService::OnSpellCheckDictionariesChanged() {
// If there are hunspell dictionaries, then fire off notifications to the
// renderers after the dictionaries are finished loading.
LoadDictionaries();
// If there are no hunspell dictionaries to load, then immediately let the
// renderers know the new state.
if (hunspell_dictionaries_.empty()) {
#if !BUILDFLAG(IS_MAC)
// Only update non-MacOS platform because basic spell check on Mac OS
// is controlled by OS and doesn't depend on users' dictionaries pref
user_prefs::UserPrefs::Get(context_)->SetBoolean(
spellcheck::prefs::kSpellCheckEnable, false);
#endif // !BUILDFLAG(IS_MAC)
InitForAllRenderers();
}
}
void SpellcheckService::OnUseSpellingServiceChanged() {
bool enabled = pref_change_registrar_.prefs()->GetBoolean(
spellcheck::prefs::kSpellCheckUseSpellingService);
if (metrics_ && RecordSpellingConfigurationMetrics(context_)) {
metrics_->RecordSpellingServiceStats(enabled);
}
}
void SpellcheckService::OnAcceptLanguagesChanged() {
// Accept-Languages and spell check are decoupled on CrOS.
#if !BUILDFLAG(IS_CHROMEOS)
std::vector<std::string> accept_languages = GetNormalizedAcceptLanguages();
StringListPrefMember dictionaries_pref;
dictionaries_pref.Init(spellcheck::prefs::kSpellCheckDictionaries,
user_prefs::UserPrefs::Get(context_));
std::vector<std::string> dictionaries = dictionaries_pref.GetValue();
std::vector<std::string> filtered_dictionaries;
for (const auto& dictionary : dictionaries) {
if (base::Contains(accept_languages, dictionary)) {
filtered_dictionaries.push_back(dictionary);
}
}
dictionaries_pref.SetValue(filtered_dictionaries);
#if BUILDFLAG(IS_WIN)
if (RecordSpellingConfigurationMetrics(context_)) {
RecordChromeLocalesStats();
}
#endif // BUILDFLAG(IS_WIN)
#endif // !BUILDFLAG(IS_CHROMEOS)
}
std::vector<std::string> SpellcheckService::GetNormalizedAcceptLanguages(
bool normalize_for_spellcheck) const {
PrefService* prefs = user_prefs::UserPrefs::Get(context_);
std::vector<std::string> accept_languages =
base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (normalize_for_spellcheck) {
std::ranges::transform(
accept_languages, accept_languages.begin(),
[&](const std::string& language) {
#if BUILDFLAG(IS_WIN)
if (spellcheck::UseBrowserSpellChecker() &&
UsesWindowsDictionary(language))
return language;
#endif // BUILDFLAG(IS_WIN)
return spellcheck::GetCorrespondingSpellCheckLanguage(language);
});
}
return accept_languages;
}
#if BUILDFLAG(IS_WIN)
void SpellcheckService::InitializePlatformSpellchecker() {
// The Windows spell checker must be created before the dictionaries are
// initialized. Note it is instantiated even if only Hunspell is being used
// since metrics on the availability of Windows platform language packs are
// being recorded. Thus method should only be called once, except in test
// code.
if (!platform_spell_checker()) {
platform_spell_checker_ = std::make_unique<WindowsSpellChecker>(
base::ThreadPool::CreateCOMSTATaskRunner({base::MayBlock()}));
}
}
void SpellcheckService::RecordSpellcheckLocalesStats() {
if (metrics_ && platform_spell_checker() && !hunspell_dictionaries_.empty()) {
std::vector<std::string> hunspell_locales;
for (auto& dict : hunspell_dictionaries_) {
hunspell_locales.push_back(dict->GetLanguage());
}
spellcheck_platform::RecordSpellcheckLocalesStats(
platform_spell_checker(), std::move(hunspell_locales));
}
}
void SpellcheckService::RecordChromeLocalesStats() {
if (metrics_ && platform_spell_checker()) {
std::vector<std::string> accept_languages =
GetNormalizedAcceptLanguages(/* normalize_for_spellcheck */ false);
if (!accept_languages.empty()) {
spellcheck_platform::RecordChromeLocalesStats(
platform_spell_checker(), std::move(accept_languages));
}
}
}
void SpellcheckService::AddWindowsSpellcheckDictionary(
const std::string& accept_language,
const std::string& supported_language_full_tag) {
if (!accept_language.empty()) {
windows_spellcheck_dictionary_map_.insert(
{accept_language, supported_language_full_tag});
}
}
std::string SpellcheckService::GetSupportedWindowsDictionaryLanguage(
const std::string& accept_language) const {
// BCP47 language tag used by the Windows spellchecker API.
std::string spellcheck_language;
auto it = windows_spellcheck_dictionary_map_.find(accept_language);
if (it != windows_spellcheck_dictionary_map_.end())
spellcheck_language = it->second;
return spellcheck_language;
}
void SpellcheckService::AddSpellcheckLanguagesForTesting(
const std::vector<std::string>& languages) {
InitializePlatformSpellchecker();
if (platform_spell_checker()) {
spellcheck_platform::AddSpellcheckLanguagesForTesting(
platform_spell_checker(), languages);
}
}
#endif // BUILDFLAG(IS_WIN)