| // 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_hunspell_dictionary.h" |
| |
| #include <stddef.h> |
| |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/no_destructor.h" |
| #include "base/notreached.h" |
| #include "base/observer_list.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/spellchecker/spellcheck_service.h" |
| #include "chrome/common/chrome_paths.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/spellcheck_buildflags.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 "net/base/load_flags.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "url/gurl.h" |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| #include "base/files/memory_mapped_file.h" |
| #include "third_party/hunspell/google/bdict.h" // nogncheck crbug.com/1125897 |
| #endif |
| |
| using content::BrowserThread; |
| |
| namespace { |
| |
| GURL& GetDownloadUrlForTesting() { |
| static base::NoDestructor<GURL> download_url_for_testing; |
| return *download_url_for_testing; |
| } |
| |
| // Close the file. |
| void CloseDictionary(base::File file) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| file.Close(); |
| } |
| |
| // Saves |data| to file at |path|. Returns true on successful save, otherwise |
| // returns false. |
| bool SaveDictionaryData(std::unique_ptr<std::string> data, |
| const base::FilePath& path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| if (!base::WriteFile(path, *data)) { |
| bool success = false; |
| #if BUILDFLAG(IS_WIN) |
| base::FilePath dict_dir; |
| base::PathService::Get(chrome::DIR_USER_DATA, &dict_dir); |
| base::FilePath fallback_file_path = |
| dict_dir.Append(path.BaseName()); |
| if (base::WriteFile(fallback_file_path, *data)) { |
| success = true; |
| } |
| #endif |
| |
| if (!success) { |
| base::DeleteFile(path); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| SpellcheckHunspellDictionary::DictionaryFile::DictionaryFile( |
| base::TaskRunner* task_runner) : task_runner_(task_runner) {} |
| |
| SpellcheckHunspellDictionary::DictionaryFile::~DictionaryFile() { |
| if (file.IsValid()) { |
| task_runner_->PostTask(FROM_HERE, |
| base::BindOnce(&CloseDictionary, std::move(file))); |
| } |
| } |
| |
| SpellcheckHunspellDictionary::DictionaryFile::DictionaryFile( |
| DictionaryFile&& other) |
| : path(other.path), |
| file(std::move(other.file)), |
| task_runner_(std::move(other.task_runner_)) {} |
| |
| SpellcheckHunspellDictionary::DictionaryFile& |
| SpellcheckHunspellDictionary::DictionaryFile::operator=( |
| DictionaryFile&& other) { |
| path = other.path; |
| file = std::move(other.file); |
| task_runner_ = std::move(other.task_runner_); |
| return *this; |
| } |
| |
| SpellcheckHunspellDictionary::SpellcheckHunspellDictionary( |
| const std::string& language, |
| const std::string& platform_spellcheck_language, |
| content::BrowserContext* browser_context, |
| SpellcheckService* spellcheck_service) |
| : task_runner_( |
| base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})), |
| language_(language), |
| platform_spellcheck_language_(platform_spellcheck_language), |
| use_browser_spellchecker_(false), |
| browser_context_(browser_context), |
| spellcheck_service_(spellcheck_service), |
| download_status_(DOWNLOAD_NONE), |
| dictionary_file_(task_runner_.get()) {} |
| |
| SpellcheckHunspellDictionary::~SpellcheckHunspellDictionary() { |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| // Disable the language from platform spellchecker. |
| if (spellcheck::UseBrowserSpellChecker() && HasPlatformSupport()) { |
| spellcheck_platform::DisableLanguage( |
| spellcheck_service_->platform_spell_checker(), |
| GetPlatformSpellcheckLanguage()); |
| } |
| #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| } |
| |
| void SpellcheckHunspellDictionary::Load() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| if (spellcheck::UseBrowserSpellChecker() && |
| spellcheck_platform::SpellCheckerAvailable() && HasPlatformSupport()) { |
| spellcheck_platform::PlatformSupportsLanguage( |
| spellcheck_service_->platform_spell_checker(), |
| GetPlatformSpellcheckLanguage(), |
| base::BindOnce( |
| &SpellcheckHunspellDictionary::PlatformSupportsLanguageComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| #endif // USE_BROWSER_SPELLCHECKER |
| |
| // Platform spellchecker isn't enabled, so the language is unsupported. |
| PlatformSupportsLanguageComplete(false); |
| } |
| |
| void SpellcheckHunspellDictionary::RetryDownloadDictionary( |
| content::BrowserContext* browser_context) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (dictionary_file_.file.IsValid()) { |
| NOTREACHED(); |
| } |
| browser_context_ = browser_context; |
| DownloadDictionary(GetDictionaryURL()); |
| } |
| |
| bool SpellcheckHunspellDictionary::IsReady() const { |
| return GetDictionaryFile().IsValid() || IsUsingPlatformChecker(); |
| } |
| |
| const base::File& SpellcheckHunspellDictionary::GetDictionaryFile() const { |
| return dictionary_file_.file; |
| } |
| |
| const std::string& SpellcheckHunspellDictionary::GetLanguage() const { |
| return language_; |
| } |
| |
| const std::string& SpellcheckHunspellDictionary::GetPlatformSpellcheckLanguage() |
| const { |
| #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| // Currently the platform spellcheck language is only distinguished for |
| // Windows. |
| return platform_spellcheck_language_; |
| #else |
| return language_; |
| #endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| } |
| |
| bool SpellcheckHunspellDictionary::HasPlatformSupport() const { |
| return !GetPlatformSpellcheckLanguage().empty(); |
| } |
| |
| bool SpellcheckHunspellDictionary::IsUsingPlatformChecker() const { |
| return use_browser_spellchecker_; |
| } |
| |
| void SpellcheckHunspellDictionary::AddObserver(Observer* observer) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| observers_.AddObserver(observer); |
| } |
| |
| void SpellcheckHunspellDictionary::RemoveObserver(Observer* observer) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool SpellcheckHunspellDictionary::IsDownloadInProgress() { |
| return download_status_ == DOWNLOAD_IN_PROGRESS; |
| } |
| |
| bool SpellcheckHunspellDictionary::IsDownloadFailure() { |
| return download_status_ == DOWNLOAD_FAILED; |
| } |
| |
| void SpellcheckHunspellDictionary::OnSimpleLoaderComplete( |
| std::unique_ptr<std::string> data) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| bool is_success = simple_loader_->NetError() == net::OK; |
| int response_code = -1; |
| if (simple_loader_->ResponseInfo() && simple_loader_->ResponseInfo()->headers) |
| response_code = simple_loader_->ResponseInfo()->headers->response_code(); |
| |
| if (!is_success || ((response_code / 100) != 2)) { |
| // Initialize will not try to download the file a second time. |
| InformListenersOfDownloadFailure(); |
| return; |
| } |
| |
| // We don't need the loader anymore. |
| simple_loader_.reset(); |
| |
| // Basic sanity check on the dictionary. There's a small chance of 200 status |
| // code for a body that represents some form of failure. |
| if (!data || data->size() < 4 || data->compare(0, 4, "BDic") != 0) { |
| InformListenersOfDownloadFailure(); |
| return; |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // To prevent corrupted dictionary data from causing a renderer crash, scan |
| // the dictionary data and verify it is sane before save it to a file. |
| if (!hunspell::BDict::Verify(base::as_byte_span(*data))) { |
| // Let PostTaskAndReply caller send to InformListenersOfInitialization |
| // through SaveDictionaryDataComplete(). |
| SaveDictionaryDataComplete(false); |
| return; |
| } |
| #endif |
| |
| task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&SaveDictionaryData, std::move(data), |
| dictionary_file_.path), |
| base::BindOnce(&SpellcheckHunspellDictionary::SaveDictionaryDataComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void SpellcheckHunspellDictionary::SetDownloadURLForTesting(const GURL url) { |
| GetDownloadUrlForTesting() = url; |
| } |
| |
| GURL SpellcheckHunspellDictionary::GetDictionaryURL() { |
| if (GetDownloadUrlForTesting() != GURL()) { |
| return GetDownloadUrlForTesting(); |
| } |
| |
| std::string bdict_file = dictionary_file_.path.BaseName().MaybeAsASCII(); |
| DCHECK(!bdict_file.empty()); |
| |
| static const char kDownloadServerUrl[] = |
| "https://redirector.gvt1.com/edgedl/chrome/dict/"; |
| |
| return GURL(std::string(kDownloadServerUrl) + |
| base::ToLowerASCII(bdict_file)); |
| } |
| |
| void SpellcheckHunspellDictionary::DownloadDictionary(GURL url) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(browser_context_); |
| |
| download_status_ = DOWNLOAD_IN_PROGRESS; |
| for (Observer& observer : observers_) |
| observer.OnHunspellDictionaryDownloadBegin(language_); |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("spellcheck_hunspell_dictionary", R"( |
| semantics { |
| sender: "Spellcheck Dictionary Downloader" |
| description: |
| "When user selects a new language for spell checking in Google " |
| "Chrome, a new dictionary is downloaded for it." |
| trigger: "User selects a new language for spell checking." |
| data: |
| "The spell checking language identifier. No user identifier is " |
| "sent." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "You can prevent downloading dictionaries by not selecting 'Use " |
| "this language for spell checking.' in Chrome's settings under " |
| "Lanugagues -> 'Language and input settings...'." |
| policy_exception_justification: |
| "Not implemented, considered not useful." |
| })"); |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = url; |
| resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| simple_loader_ = network::SimpleURLLoader::Create(std::move(resource_request), |
| traffic_annotation); |
| network::mojom::URLLoaderFactory* loader_factory = |
| browser_context_->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess() |
| .get(); |
| simple_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| loader_factory, |
| base::BindOnce(&SpellcheckHunspellDictionary::OnSimpleLoaderComplete, |
| base::Unretained(this))); |
| |
| // Attempt downloading the dictionary only once. |
| browser_context_ = nullptr; |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // static |
| SpellcheckHunspellDictionary::DictionaryFile |
| SpellcheckHunspellDictionary::OpenDictionaryFile(base::TaskRunner* task_runner, |
| const base::FilePath& path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| // The default_dictionary_file can either come from the standard list of |
| // hunspell dictionaries (determined in InitializeDictionaryLocation), or it |
| // can be passed in via an extension. In either case, the file is checked for |
| // existence so that it's not re-downloaded. |
| // For systemwide installations on Windows, the default directory may not |
| // have permissions for download. In that case, the alternate directory for |
| // download is chrome::DIR_USER_DATA. |
| DictionaryFile dictionary(task_runner); |
| |
| #if BUILDFLAG(IS_WIN) |
| // Check if the dictionary exists in the fallback location. If so, use it |
| // rather than downloading anew. |
| base::FilePath user_dir; |
| base::PathService::Get(chrome::DIR_USER_DATA, &user_dir); |
| base::FilePath fallback = user_dir.Append(path.BaseName()); |
| if (!base::PathExists(path) && base::PathExists(fallback)) |
| dictionary.path = fallback; |
| else |
| dictionary.path = path; |
| #else |
| dictionary.path = path; |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // Open the dictionary file and verify there is no corruption. If verification |
| // fails the file must be deleted. |
| |
| dictionary.file.Initialize(dictionary.path, |
| base::File::FLAG_READ | base::File::FLAG_OPEN); |
| if (!dictionary.file.IsValid()) { |
| dictionary.file.Close(); |
| base::DeleteFile(dictionary.path); |
| return dictionary; |
| } |
| |
| std::vector<uint8_t> data; |
| data.resize(dictionary.file.GetLength()); |
| if (!dictionary.file.ReadAndCheck(0, data) || |
| !hunspell::BDict::Verify(data)) { |
| dictionary.file.Close(); |
| base::DeleteFile(dictionary.path); |
| } |
| |
| return dictionary; |
| } |
| |
| // static |
| SpellcheckHunspellDictionary::DictionaryFile |
| SpellcheckHunspellDictionary::InitializeDictionaryLocation( |
| base::TaskRunner* task_runner, const std::string& language) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| // The default place where the spellcheck dictionary resides is |
| // chrome::DIR_APP_DICTIONARIES. |
| // |
| // Initialize the BDICT path. Initialization should be on the blocking |
| // sequence because it checks if there is a "Dictionaries" directory and |
| // create it. |
| base::FilePath dict_dir; |
| base::PathService::Get(chrome::DIR_APP_DICTIONARIES, &dict_dir); |
| base::FilePath dict_path = |
| spellcheck::GetVersionedFileName(language, dict_dir); |
| |
| return OpenDictionaryFile(task_runner, dict_path); |
| } |
| |
| void SpellcheckHunspellDictionary::InitializeDictionaryLocationComplete( |
| DictionaryFile file) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| dictionary_file_ = std::move(file); |
| |
| if (!dictionary_file_.file.IsValid()) { |
| // Notify browser tests that this dictionary is corrupted. Skip downloading |
| // the dictionary in browser tests. |
| // TODO(rouslan): Remove this test-only case. |
| if (spellcheck_service_->SignalStatusEvent( |
| SpellcheckService::BDICT_CORRUPTED)) { |
| browser_context_ = nullptr; |
| } |
| |
| if (browser_context_) { |
| // Download from the UI thread to check that |browser_context_| is |
| // still valid. |
| DownloadDictionary(GetDictionaryURL()); |
| return; |
| } |
| } |
| |
| InformListenersOfInitialization(); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| void SpellcheckHunspellDictionary::SaveDictionaryDataComplete( |
| bool dictionary_saved) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (dictionary_saved) { |
| download_status_ = DOWNLOAD_NONE; |
| for (Observer& observer : observers_) |
| observer.OnHunspellDictionaryDownloadSuccess(language_); |
| Load(); |
| } else { |
| InformListenersOfDownloadFailure(); |
| InformListenersOfInitialization(); |
| } |
| } |
| |
| void SpellcheckHunspellDictionary::InformListenersOfInitialization() { |
| for (Observer& observer : observers_) |
| observer.OnHunspellDictionaryInitialized(language_); |
| } |
| |
| void SpellcheckHunspellDictionary::InformListenersOfDownloadFailure() { |
| download_status_ = DOWNLOAD_FAILED; |
| for (Observer& observer : observers_) |
| observer.OnHunspellDictionaryDownloadFailure(language_); |
| } |
| |
| void SpellcheckHunspellDictionary::PlatformSupportsLanguageComplete( |
| bool platform_supports_language) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (platform_supports_language) { |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| if (spellcheck::UseBrowserSpellChecker() && HasPlatformSupport()) { |
| spellcheck_platform::SetLanguage( |
| spellcheck_service_->platform_spell_checker(), |
| GetPlatformSpellcheckLanguage(), |
| base::BindOnce(&SpellcheckHunspellDictionary:: |
| SpellCheckPlatformSetLanguageComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| NOTREACHED(); |
| } else { |
| // Either the platform spellchecker is unavailable / disabled, or it doesn't |
| // support this language. In either case, we must use Hunspell for this |
| // language, unless we are on Android, which doesn't support Hunspell. |
| #if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(USE_RENDERER_SPELLCHECKER) |
| task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&InitializeDictionaryLocation, |
| base::RetainedRef(task_runner_.get()), language_), |
| base::BindOnce( |
| &SpellcheckHunspellDictionary::InitializeDictionaryLocationComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| #endif // !BUILDFLAG(IS_ANDROID) && BUILDFLAG(USE_RENDERER_SPELLCHECKER) |
| } |
| } |
| |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| void SpellcheckHunspellDictionary::SpellCheckPlatformSetLanguageComplete( |
| bool result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (!result) |
| return; |
| |
| use_browser_spellchecker_ = true; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &SpellcheckHunspellDictionary::InformListenersOfInitialization, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| #endif |