// 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/renderer/spellcheck_provider.h" #include #include "base/bind.h" #include "base/metrics/histogram_macros.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/common/service_names.mojom.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/web_vector.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; using blink::WebVector; 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 WebVector& words_added) override; private: 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 WebVector& words_added) { // Clear only cache. Current pending requests should continue as they are. owner_->last_request_.clear(); owner_->last_results_.Assign( blink::WebVector()); // 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, service_manager::LocalInterfaceProvider* embedder_provider) : content::RenderFrameObserver(render_frame), spellcheck_(spellcheck), embedder_provider_(embedder_provider) { DCHECK(spellcheck_); DCHECK(embedder_provider); if (render_frame) // NULL in unit tests. render_frame->GetWebFrame()->SetTextCheckClient(this); dictionary_update_observer_ = std::make_unique(this); } SpellCheckProvider::~SpellCheckProvider() { } void SpellCheckProvider::ResetDictionaryUpdateObserverForTesting() { dictionary_update_observer_.reset(); } spellcheck::mojom::SpellCheckHost& SpellCheckProvider::GetSpellCheckHost() { if (spell_check_host_) return *spell_check_host_; embedder_provider_->GetInterface( spell_check_host_.BindNewPipeAndPassReceiver()); return *spell_check_host_; } void SpellCheckProvider::RequestTextChecking( const base::string16& text, std::unique_ptr 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_.Assign(blink::WebVector()); last_identifier_ = text_check_completions_.Add(std::move(completion)); #if BUILDFLAG(USE_BROWSER_SPELLCHECKER) if (spellcheck::UseBrowserSpellChecker()) { #if defined(OS_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 // defined(OS_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 base::string16& text) { DCHECK(spellcheck::UseBrowserSpellChecker()); #if defined(OS_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 // defined(OS_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, routing_id(), base::BindOnce(&SpellCheckProvider::OnRespondTextCheck, weak_factory_.GetWeakPtr(), last_identifier_, text)); } #if defined(OS_WIN) void SpellCheckProvider::OnRespondInitializeDictionaries( const base::string16& text, std::vector dictionaries, const std::vector& 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 // defined(OS_WIN) #endif // BUILDFLAG(USE_BROWSER_SPELLCHECKER) void SpellCheckProvider::FocusedElementChanged( const blink::WebElement& unused) { #if defined(OS_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 // defined(OS_ANDROID) } bool SpellCheckProvider::IsSpellCheckingEnabled() const { return spellcheck_->IsSpellcheckEnabled(); } void SpellCheckProvider::CheckSpelling( const WebString& text, size_t& offset, size_t& length, WebVector* optional_suggestions) { base::string16 word = text.Utf16(); const int kWordStart = 0; if (optional_suggestions) { #if defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) base::TimeTicks suggestions_start = base::TimeTicks::Now(); #endif // defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) spellcheck::PerLanguageSuggestions per_language_suggestions; spellcheck_->SpellCheckWord(word.c_str(), kWordStart, word.size(), routing_id(), &offset, &length, &per_language_suggestions); #if defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) if (spellcheck::UseBrowserSpellChecker() && spellcheck_->EnabledLanguageCount() < spellcheck_->LanguageCount()) { // Also fetch suggestions from the browser process (native spellchecker). // This is a synchronous Mojo call, because this method must return // synchronously. spellcheck::PerLanguageSuggestions browser_suggestions; GetSpellCheckHost().GetPerLanguageSuggestions(word, &browser_suggestions); per_language_suggestions.reserve(per_language_suggestions.size() + browser_suggestions.size()); per_language_suggestions.insert(per_language_suggestions.end(), browser_suggestions.begin(), browser_suggestions.end()); spellcheck_renderer_metrics::RecordHybridSuggestionDuration( base::TimeTicks::Now() - suggestions_start); } else { spellcheck_renderer_metrics::RecordHunspellSuggestionDuration( base::TimeTicks::Now() - suggestions_start); } #endif // defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) std::vector suggestions; spellcheck::FillSuggestions(per_language_suggestions, &suggestions); WebVector web_suggestions(suggestions.size()); std::transform( suggestions.begin(), suggestions.end(), web_suggestions.begin(), [](const base::string16& s) { return WebString::FromUTF16(s); }); *optional_suggestions = web_suggestions; spellcheck_renderer_metrics::RecordCheckedTextLengthWithSuggestions( base::saturated_cast(word.size())); } else { spellcheck_->SpellCheckWord(word.c_str(), kWordStart, word.size(), routing_id(), &offset, &length, /* optional suggestions vector */ nullptr); spellcheck_renderer_metrics::RecordCheckedTextLengthNoSuggestions( base::saturated_cast(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 completion) { RequestTextChecking(text.Utf16(), std::move(completion)); spellcheck_renderer_metrics::RecordAsyncCheckedTextLength( base::saturated_cast(text.length())); } #if BUILDFLAG(USE_RENDERER_SPELLCHECKER) void SpellCheckProvider::OnRespondSpellingService( int identifier, const base::string16& line, bool success, const std::vector& results) { if (!text_check_completions_.Lookup(identifier)) return; std::unique_ptr 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)); return; } // Double-check the returned spellchecking results with Hunspell to visualize // the differences between ours and the enhanced spell checker. blink::WebVector textcheck_results; spellcheck_->CreateTextCheckingResults(SpellCheck::USE_HUNSPELL_FOR_GRAMMAR, /*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 base::string16& text, size_t index) const { const base::char16* data = text.data(); size_t length = text.length(); while (index < length) { uint32_t code = 0; 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 base::string16& line, const std::vector& results) { DCHECK(spellcheck_); if (!text_check_completions_.Lookup(identifier)) return; std::unique_ptr completion( text_check_completions_.Replace(identifier, nullptr)); text_check_completions_.Remove(identifier); blink::WebVector textcheck_results; SpellCheck::ResultFilter result_filter = SpellCheck::DO_NOT_MODIFY; #if defined(OS_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 // defined(OS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER) spellcheck_->CreateTextCheckingResults(result_filter, /*line_offset=*/0, line, results, &textcheck_results); #if defined(OS_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 // defined(OS_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 base::string16& 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. base::string16 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; } blink::WebVector results(last_results_.Data(), result_size); completion->DidFinishCheckingText(results); return true; } return false; } void SpellCheckProvider::OnDestruct() { delete this; }