| // 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 "components/spellcheck/renderer/spellcheck_provider.h" |
| |
| #include <algorithm> |
| #include <unordered_map> |
| #include <vector> |
| |
| #include "base/compiler_specific.h" |
| #include "base/containers/to_vector.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "build/build_config.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/common/spellcheck_result.h" |
| #include "components/spellcheck/renderer/spellcheck.h" |
| #include "components/spellcheck/renderer/spellcheck_language.h" |
| #include "components/spellcheck/renderer/spellcheck_renderer_metrics.h" |
| #include "components/spellcheck/spellcheck_buildflags.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "content/public/renderer/render_thread.h" |
| #include "services/service_manager/public/cpp/local_interface_provider.h" |
| #include "third_party/blink/public/platform/browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/web/web_document.h" |
| #include "third_party/blink/public/web/web_element.h" |
| #include "third_party/blink/public/web/web_local_frame.h" |
| #include "third_party/blink/public/web/web_text_checking_completion.h" |
| #include "third_party/blink/public/web/web_text_checking_result.h" |
| #include "third_party/blink/public/web/web_text_decoration_type.h" |
| |
| using blink::WebElement; |
| using blink::WebLocalFrame; |
| using blink::WebString; |
| using blink::WebTextCheckingCompletion; |
| using blink::WebTextCheckingResult; |
| using blink::WebTextDecorationType; |
| |
| static_assert(int(blink::kWebTextDecorationTypeSpelling) == |
| int(SpellCheckResult::SPELLING), |
| "mismatching enums"); |
| static_assert(int(blink::kWebTextDecorationTypeGrammar) == |
| int(SpellCheckResult::GRAMMAR), |
| "mismatching enums"); |
| |
| class SpellCheckProvider::DictionaryUpdateObserverImpl |
| : public DictionaryUpdateObserver { |
| public: |
| explicit DictionaryUpdateObserverImpl(SpellCheckProvider* owner); |
| ~DictionaryUpdateObserverImpl() override; |
| |
| // DictionaryUpdateObserver: |
| void OnDictionaryUpdated(const std::vector<WebString>& words_added) override; |
| |
| private: |
| raw_ptr<SpellCheckProvider> owner_; |
| }; |
| |
| SpellCheckProvider::DictionaryUpdateObserverImpl::DictionaryUpdateObserverImpl( |
| SpellCheckProvider* owner) |
| : owner_(owner) { |
| owner_->spellcheck_->AddDictionaryUpdateObserver(this); |
| } |
| |
| SpellCheckProvider::DictionaryUpdateObserverImpl:: |
| ~DictionaryUpdateObserverImpl() { |
| owner_->spellcheck_->RemoveDictionaryUpdateObserver(this); |
| } |
| |
| void SpellCheckProvider::DictionaryUpdateObserverImpl::OnDictionaryUpdated( |
| const std::vector<WebString>& words_added) { |
| // Clear only cache. Current pending requests should continue as they are. |
| owner_->last_request_.clear(); |
| owner_->last_results_.clear(); |
| |
| // owner_->render_frame() is nullptr in unit tests. |
| if (auto* render_frame = owner_->render_frame()) { |
| DCHECK(render_frame->GetWebFrame()); |
| render_frame->GetWebFrame()->RemoveSpellingMarkersUnderWords(words_added); |
| } |
| } |
| |
| SpellCheckProvider::SpellCheckProvider(content::RenderFrame* render_frame, |
| SpellCheck* spellcheck) |
| : content::RenderFrameObserver(render_frame), spellcheck_(spellcheck) { |
| DCHECK(spellcheck_); |
| if (render_frame) // NULL in unit tests. |
| render_frame->GetWebFrame()->SetTextCheckClient(this); |
| |
| dictionary_update_observer_ = |
| std::make_unique<DictionaryUpdateObserverImpl>(this); |
| } |
| |
| SpellCheckProvider::~SpellCheckProvider() { |
| } |
| |
| void SpellCheckProvider::ResetDictionaryUpdateObserverForTesting() { |
| dictionary_update_observer_.reset(); |
| } |
| |
| void SpellCheckProvider::RequestTextChecking( |
| const std::u16string& text, |
| std::unique_ptr<WebTextCheckingCompletion> completion) { |
| // Ignore invalid requests. |
| if (text.empty() || !HasWordCharacters(text, 0)) { |
| completion->DidCancelCheckingText(); |
| return; |
| } |
| |
| // Try to satisfy check from cache. |
| if (SatisfyRequestFromCache(text, completion.get())) |
| return; |
| |
| // Send this text to a browser. A browser checks the user profile and send |
| // this text to the Spelling service only if a user enables this feature. |
| last_request_.clear(); |
| last_results_.clear(); |
| last_identifier_ = text_check_completions_.Add(std::move(completion)); |
| |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| if (spellcheck::UseBrowserSpellChecker()) { |
| #if BUILDFLAG(IS_WIN) |
| if (base::FeatureList::IsEnabled( |
| spellcheck::kWinDelaySpellcheckServiceInit) && |
| !dictionaries_loaded_) { |
| // Initialize the spellcheck service on demand (this spellcheck request |
| // could be the result of the first click in editable content), then |
| // complete the text check request when the dictionaries are loaded. |
| // The delayed spell check service initialization sequence, starting from |
| // when the user clicks in editable content, is as follows: |
| // - SpellcheckProvider::RequestTextChecking (Renderer, this method) |
| // - SpellCheckHostChromeImpl::InitializeDictionaries (Browser) |
| // - SpellcheckService::InitializeDictionaries (Browser) |
| // - SpellCheckHostChromeImpl::OnDictionariesInitialized (Browser) |
| // - SpellcheckProvider::OnRespondInitializeDictionaries (Renderer) |
| GetSpellCheckHost().InitializeDictionaries( |
| base::BindOnce(&SpellCheckProvider::OnRespondInitializeDictionaries, |
| weak_factory_.GetWeakPtr(), text)); |
| return; |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| RequestTextCheckingFromBrowser(text); |
| } |
| #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| #if BUILDFLAG(USE_RENDERER_SPELLCHECKER) |
| if (!spellcheck::UseBrowserSpellChecker()) { |
| GetSpellCheckHost().CallSpellingService( |
| text, |
| base::BindOnce(&SpellCheckProvider::OnRespondSpellingService, |
| weak_factory_.GetWeakPtr(), last_identifier_, text)); |
| } |
| #endif // BUILDFLAG(USE_RENDERER_SPELLCHECKER) |
| } |
| |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| void SpellCheckProvider::RequestTextCheckingFromBrowser( |
| const std::u16string& text) { |
| DCHECK(spellcheck::UseBrowserSpellChecker()); |
| #if BUILDFLAG(IS_WIN) |
| |
| // Determine whether a hybrid check is needed. |
| bool use_hunspell = spellcheck_->EnabledLanguageCount() > 0; |
| bool use_native = |
| spellcheck_->EnabledLanguageCount() != spellcheck_->LanguageCount(); |
| |
| if (!use_hunspell && !use_native) { |
| OnRespondTextCheck(last_identifier_, text, /*results=*/{}); |
| return; |
| } |
| |
| if (!use_native) { |
| // No language can be handled by the native spell checker. Use the regular |
| // Hunspell code path. |
| GetSpellCheckHost().CallSpellingService( |
| text, |
| base::BindOnce(&SpellCheckProvider::OnRespondSpellingService, |
| weak_factory_.GetWeakPtr(), last_identifier_, text)); |
| return; |
| } |
| |
| // Some languages can be handled by the native spell checker. Use the |
| // regular browser spell check code path. If hybrid spell check is |
| // required (i.e. some locales must be checked by Hunspell), misspellings |
| // from the native spell checker will be double-checked with Hunspell in |
| // the |OnRespondTextCheck| callback. |
| hybrid_requests_info_[last_identifier_] = {/*used_hunspell=*/use_hunspell, |
| /*used_native=*/use_native, |
| base::TimeTicks::Now()}; |
| #endif // BUILDFLAG(IS_WIN) |
| |
| // Text check (unified request for grammar and spell check) is only |
| // available for browser process, so we ask the system spellchecker |
| // over mojo or return an empty result if the checker is not available. |
| GetSpellCheckHost().RequestTextCheck( |
| text, base::BindOnce(&SpellCheckProvider::OnRespondTextCheck, |
| weak_factory_.GetWeakPtr(), last_identifier_, text)); |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| void SpellCheckProvider::OnRespondInitializeDictionaries( |
| const std::u16string& text, |
| std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries, |
| const std::vector<std::string>& custom_words, |
| bool enable) { |
| DCHECK(!dictionaries_loaded_); |
| dictionaries_loaded_ = true; |
| |
| // Because the SpellChecker and SpellCheckHost mojo interfaces use different |
| // channels, there is no guarantee that the SpellChecker::Initialize response |
| // will be received before the SpellCheckHost::InitializeDictionaries |
| // callback. If the order is reversed, no spellcheck will be performed since |
| // the renderer side thinks there are no dictionaries available. Ensure that |
| // the SpellChecker is initialized before performing a spellcheck. |
| spellcheck_->Initialize(std::move(dictionaries), custom_words, enable); |
| |
| RequestTextCheckingFromBrowser(text); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| void SpellCheckProvider::FocusedElementChanged( |
| const blink::WebElement& unused) { |
| #if BUILDFLAG(IS_ANDROID) |
| if (!spell_check_host_.is_bound()) |
| return; |
| |
| WebLocalFrame* frame = render_frame()->GetWebFrame(); |
| WebElement element = frame->GetDocument().IsNull() |
| ? WebElement() |
| : frame->GetDocument().FocusedElement(); |
| bool enabled = !element.IsNull() && element.IsEditable(); |
| if (!enabled) |
| GetSpellCheckHost().DisconnectSessionBridge(); |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| spellcheck::mojom::SpellCheckHost& SpellCheckProvider::GetSpellCheckHost() { |
| if (spell_check_host_) { |
| return *spell_check_host_.get(); |
| } |
| |
| // We shodulnt't get here in tests, `spell_check_host_` should have been set. |
| CHECK(render_frame()); |
| |
| render_frame()->GetBrowserInterfaceBroker().GetInterface( |
| spell_check_host_.BindNewPipeAndPassReceiver()); |
| return *spell_check_host_.get(); |
| } |
| |
| bool SpellCheckProvider::IsSpellCheckingEnabled() const { |
| return spellcheck_->IsSpellcheckEnabled(); |
| } |
| |
| void SpellCheckProvider::CheckSpelling( |
| const WebString& text, |
| size_t& offset, |
| size_t& length, |
| std::vector<blink::WebString>* optional_suggestions) { |
| std::u16string word = text.Utf16(); |
| |
| if (optional_suggestions) { |
| #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| base::TimeTicks suggestions_start = base::TimeTicks::Now(); |
| #endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| // Retrieve suggestions from Hunspell. Windows platform spellchecker |
| // suggestions are retrieved in SpellingMenuObserver::InitMenu on the |
| // browser process side to avoid a blocking IPC. |
| spellcheck::PerLanguageSuggestions per_language_suggestions; |
| spellcheck_->SpellCheckWord(word, GetSpellCheckHost(), &offset, &length, |
| &per_language_suggestions); |
| |
| #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| spellcheck_renderer_metrics::RecordHunspellSuggestionDuration( |
| base::TimeTicks::Now() - suggestions_start); |
| #endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| std::vector<std::u16string> suggestions; |
| spellcheck::FillSuggestions(per_language_suggestions, &suggestions); |
| *optional_suggestions = base::ToVector(suggestions, &WebString::FromUTF16); |
| spellcheck_renderer_metrics::RecordCheckedTextLengthWithSuggestions( |
| base::saturated_cast<int>(word.size())); |
| } else { |
| spellcheck_->SpellCheckWord(word, GetSpellCheckHost(), &offset, &length, |
| /* optional suggestions vector */ nullptr); |
| spellcheck_renderer_metrics::RecordCheckedTextLengthNoSuggestions( |
| base::saturated_cast<int>(word.size())); |
| |
| // If optional_suggestions is not requested, the API is called |
| // for marking. So we use this for counting markable words. |
| GetSpellCheckHost().NotifyChecked(word, 0 < length); |
| } |
| } |
| |
| void SpellCheckProvider::RequestCheckingOfText( |
| const WebString& text, |
| std::unique_ptr<WebTextCheckingCompletion> completion) { |
| RequestTextChecking(text.Utf16(), std::move(completion)); |
| spellcheck_renderer_metrics::RecordAsyncCheckedTextLength( |
| base::saturated_cast<int>(text.length())); |
| } |
| |
| #if BUILDFLAG(USE_RENDERER_SPELLCHECKER) |
| void SpellCheckProvider::OnRespondSpellingService( |
| int identifier, |
| const std::u16string& line, |
| bool success, |
| const std::vector<SpellCheckResult>& results) { |
| if (!text_check_completions_.Lookup(identifier)) |
| return; |
| std::unique_ptr<WebTextCheckingCompletion> completion( |
| text_check_completions_.Replace(identifier, nullptr)); |
| text_check_completions_.Remove(identifier); |
| |
| // If |success| is false, we use local spellcheck as a fallback. |
| if (!success) { |
| spellcheck_->RequestTextChecking(line, std::move(completion), |
| weak_factory_.GetWeakPtr()); |
| return; |
| } |
| |
| // Double-check the returned spellchecking results with Hunspell to visualize |
| // the differences between ours and the enhanced spell checker. |
| std::vector<blink::WebTextCheckingResult> textcheck_results; |
| spellcheck_->CreateTextCheckingResults( |
| SpellCheck::USE_HUNSPELL_FOR_GRAMMAR, GetSpellCheckHost(), |
| /*line_offset=*/0, line, results, &textcheck_results); |
| completion->DidFinishCheckingText(textcheck_results); |
| |
| // Cache the request and the converted results. |
| last_request_ = line; |
| last_results_.swap(textcheck_results); |
| } |
| #endif |
| |
| bool SpellCheckProvider::HasWordCharacters(const std::u16string& text, |
| size_t index) const { |
| const char16_t* data = text.data(); |
| size_t length = text.length(); |
| while (index < length) { |
| uint32_t code = 0; |
| UNSAFE_TODO(U16_NEXT(data, index, length, code)); |
| UErrorCode error = U_ZERO_ERROR; |
| if (uscript_getScript(code, &error) != USCRIPT_COMMON) |
| return true; |
| } |
| return false; |
| } |
| |
| #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| void SpellCheckProvider::OnRespondTextCheck( |
| int identifier, |
| const std::u16string& line, |
| const std::vector<SpellCheckResult>& results) { |
| DCHECK(spellcheck_); |
| if (!text_check_completions_.Lookup(identifier)) |
| return; |
| std::unique_ptr<WebTextCheckingCompletion> completion( |
| text_check_completions_.Replace(identifier, nullptr)); |
| text_check_completions_.Remove(identifier); |
| std::vector<blink::WebTextCheckingResult> textcheck_results; |
| |
| SpellCheck::ResultFilter result_filter = SpellCheck::DO_NOT_MODIFY; |
| |
| #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| const auto& request_info = hybrid_requests_info_.find(identifier); |
| if (spellcheck::UseBrowserSpellChecker() && |
| request_info != hybrid_requests_info_.end() && |
| request_info->second.used_hunspell && request_info->second.used_native) { |
| // Not all locales could be checked by the native spell checker. Verify each |
| // mistake against Hunspell in the locales that weren't checked. |
| result_filter = SpellCheck::USE_HUNSPELL_FOR_HYBRID_CHECK; |
| } |
| #endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| spellcheck_->CreateTextCheckingResults(result_filter, GetSpellCheckHost(), |
| /*line_offset=*/0, line, results, |
| &textcheck_results); |
| |
| #if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| if (request_info != hybrid_requests_info_.end()) { |
| spellcheck_renderer_metrics::RecordSpellcheckDuration( |
| base::TimeTicks::Now() - request_info->second.request_start_ticks, |
| request_info->second.used_hunspell, request_info->second.used_native); |
| hybrid_requests_info_.erase(request_info); |
| } |
| #endif // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| completion->DidFinishCheckingText(textcheck_results); |
| |
| // Cache the request and the converted results. |
| last_request_ = line; |
| last_results_.swap(textcheck_results); |
| } |
| #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) |
| |
| bool SpellCheckProvider::SatisfyRequestFromCache( |
| const std::u16string& text, |
| WebTextCheckingCompletion* completion) { |
| size_t last_length = last_request_.length(); |
| if (!last_length) |
| return false; |
| |
| // Send back the |last_results_| if the |last_request_| is a substring of |
| // |text| and |text| does not have more words to check. Provider cannot cancel |
| // the spellcheck request here, because WebKit might have discarded the |
| // previous spellcheck results and erased the spelling markers in response to |
| // the user editing the text. |
| std::u16string request(text); |
| size_t text_length = request.length(); |
| if (text_length >= last_length && |
| !request.compare(0, last_length, last_request_)) { |
| if (text_length == last_length || !HasWordCharacters(text, last_length)) { |
| completion->DidFinishCheckingText(last_results_); |
| return true; |
| } |
| } |
| |
| // Create a subset of the cached results and return it if the given text is a |
| // substring of the cached text. |
| if (text_length < last_length && |
| !last_request_.compare(0, text_length, request)) { |
| size_t result_size = 0; |
| for (size_t i = 0; i < last_results_.size(); ++i) { |
| size_t start = last_results_[i].location; |
| size_t end = start + last_results_[i].length; |
| if (start <= text_length && end <= text_length) |
| ++result_size; |
| } |
| std::vector<blink::WebTextCheckingResult> results = |
| base::ToVector(base::span(last_results_).first(result_size)); |
| completion->DidFinishCheckingText(results); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void SpellCheckProvider::OnDestruct() { |
| delete this; |
| } |