/* vim: set ts=2 sts=2 sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozSpellChecker.h" #include "nsIStringEnumerator.h" #include "nsICategoryManager.h" #include "nsISupportsPrimitives.h" #include "nsISimpleEnumerator.h" #include "mozEnglishWordUtils.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/Logging.h" #include "mozilla/PRemoteSpellcheckEngineChild.h" #include "mozilla/TextServicesDocument.h" #include "nsXULAppAPI.h" #include "RemoteSpellCheckEngineChild.h" using mozilla::AssertedCast; using mozilla::GenericPromise; using mozilla::LogLevel; using mozilla::RemoteSpellcheckEngineChild; using mozilla::TextServicesDocument; using mozilla::dom::ContentChild; #define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1" static mozilla::LazyLogModule sSpellChecker("SpellChecker"); NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, mTextServicesDocument, mPersonalDictionary) mozSpellChecker::mozSpellChecker() : mEngine(nullptr) {} mozSpellChecker::~mozSpellChecker() { if (mPersonalDictionary) { // mPersonalDictionary->Save(); mPersonalDictionary->EndSession(); } mSpellCheckingEngine = nullptr; mPersonalDictionary = nullptr; if (mEngine) { MOZ_ASSERT(XRE_IsContentProcess()); RemoteSpellcheckEngineChild::Send__delete__(mEngine); MOZ_ASSERT(!mEngine); } } nsresult mozSpellChecker::Init() { mSpellCheckingEngine = nullptr; if (XRE_IsContentProcess()) { mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); MOZ_ASSERT(contentChild); mEngine = new RemoteSpellcheckEngineChild(this); contentChild->SendPRemoteSpellcheckEngineConstructor(mEngine); } else { mPersonalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); } return NS_OK; } TextServicesDocument* mozSpellChecker::GetTextServicesDocument() { return mTextServicesDocument; } nsresult mozSpellChecker::SetDocument( TextServicesDocument* aTextServicesDocument, bool aFromStartofDoc) { MOZ_LOG(sSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__)); mTextServicesDocument = aTextServicesDocument; mFromStart = aFromStartofDoc; return NS_OK; } nsresult mozSpellChecker::NextMisspelledWord(nsAString& aWord, nsTArray& aSuggestions) { if (NS_WARN_IF(!mConverter)) { return NS_ERROR_NOT_INITIALIZED; } int32_t selOffset; nsresult result; result = SetupDoc(&selOffset); if (NS_FAILED(result)) return result; bool done; while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { int32_t begin, end; nsAutoString str; mTextServicesDocument->GetCurrentTextBlock(str); while (mConverter->FindNextWord(str, selOffset, &begin, &end)) { const nsDependentSubstring currWord(str, begin, end - begin); bool isMisspelled; result = CheckWord(currWord, &isMisspelled, &aSuggestions); if (NS_WARN_IF(NS_FAILED(result))) { return result; } if (isMisspelled) { aWord = currWord; MOZ_KnownLive(mTextServicesDocument) ->SetSelection(AssertedCast(begin), AssertedCast(end - begin)); // After ScrollSelectionIntoView(), the pending notifications might // be flushed and PresShell/PresContext/Frames may be dead. // See bug 418470. mTextServicesDocument->ScrollSelectionIntoView(); return NS_OK; } selOffset = end; } mTextServicesDocument->NextBlock(); selOffset = 0; } return NS_OK; } RefPtr mozSpellChecker::CheckWords( const nsTArray& aWords) { if (XRE_IsContentProcess()) { return mEngine->CheckWords(aWords); } nsTArray misspells; misspells.SetCapacity(aWords.Length()); for (auto& word : aWords) { bool misspelled; nsresult rv = CheckWord(word, &misspelled, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return mozilla::CheckWordPromise::CreateAndReject(rv, __func__); } misspells.AppendElement(misspelled); } return mozilla::CheckWordPromise::CreateAndResolve(std::move(misspells), __func__); } nsresult mozSpellChecker::CheckWord(const nsAString& aWord, bool* aIsMisspelled, nsTArray* aSuggestions) { if (XRE_IsContentProcess()) { // Use async version (CheckWords or Suggest) on content process return NS_ERROR_FAILURE; } nsresult result; bool correct; if (!mSpellCheckingEngine) { return NS_ERROR_NULL_POINTER; } *aIsMisspelled = false; result = mSpellCheckingEngine->Check(aWord, &correct); NS_ENSURE_SUCCESS(result, result); if (!correct) { if (aSuggestions) { result = mSpellCheckingEngine->Suggest(aWord, *aSuggestions); NS_ENSURE_SUCCESS(result, result); } *aIsMisspelled = true; } return NS_OK; } RefPtr mozSpellChecker::Suggest( const nsAString& aWord, uint32_t aMaxCount) { if (XRE_IsContentProcess()) { return mEngine->SendSuggest(aWord, aMaxCount) ->Then( mozilla::GetCurrentSerialEventTarget(), __func__, [](nsTArray&& aSuggestions) { return mozilla::SuggestionsPromise::CreateAndResolve( std::move(aSuggestions), __func__); }, [](mozilla::ipc::ResponseRejectReason&& aReason) { return mozilla::SuggestionsPromise::CreateAndReject( NS_ERROR_NOT_AVAILABLE, __func__); }); } if (!mSpellCheckingEngine) { return mozilla::SuggestionsPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); } bool correct; nsresult rv = mSpellCheckingEngine->Check(aWord, &correct); if (NS_FAILED(rv)) { return mozilla::SuggestionsPromise::CreateAndReject(rv, __func__); } nsTArray suggestions; if (!correct) { rv = mSpellCheckingEngine->Suggest(aWord, suggestions); if (NS_FAILED(rv)) { return mozilla::SuggestionsPromise::CreateAndReject(rv, __func__); } if (suggestions.Length() > aMaxCount) { suggestions.TruncateLength(aMaxCount); } } return mozilla::SuggestionsPromise::CreateAndResolve(std::move(suggestions), __func__); } nsresult mozSpellChecker::Replace(const nsAString& aOldWord, const nsAString& aNewWord, bool aAllOccurrences) { if (NS_WARN_IF(!mConverter)) { return NS_ERROR_NOT_INITIALIZED; } if (!aAllOccurrences) { MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord); return NS_OK; } int32_t selOffset; int32_t startBlock; int32_t begin, end; bool done; nsresult result; // find out where we are result = SetupDoc(&selOffset); if (NS_WARN_IF(NS_FAILED(result))) { return result; } result = GetCurrentBlockIndex(mTextServicesDocument, &startBlock); if (NS_WARN_IF(NS_FAILED(result))) { return result; } // start at the beginning result = mTextServicesDocument->FirstBlock(); if (NS_WARN_IF(NS_FAILED(result))) { return result; } int32_t currOffset = 0; int32_t currentBlock = 0; int32_t wordLengthDifference = AssertedCast(static_cast(aNewWord.Length()) - static_cast(aOldWord.Length())); while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { nsAutoString str; mTextServicesDocument->GetCurrentTextBlock(str); while (mConverter->FindNextWord(str, currOffset, &begin, &end)) { if (aOldWord.Equals(Substring(str, begin, end - begin))) { // if we are before the current selection point but in the same // block move the selection point forwards if (currentBlock == startBlock && begin < selOffset) { selOffset += wordLengthDifference; if (selOffset < begin) { selOffset = begin; } } // Don't keep running if selecting or inserting text fails because // it may cause infinite loop. if (NS_WARN_IF(NS_FAILED( MOZ_KnownLive(mTextServicesDocument) ->SetSelection(AssertedCast(begin), AssertedCast(end - begin))))) { return NS_ERROR_FAILURE; } if (NS_WARN_IF(NS_FAILED( MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord)))) { return NS_ERROR_FAILURE; } mTextServicesDocument->GetCurrentTextBlock(str); end += wordLengthDifference; // recursion was cute in GEB, not here. } currOffset = end; } mTextServicesDocument->NextBlock(); currentBlock++; currOffset = 0; } // We are done replacing. Put the selection point back where we found it // (or equivalent); result = mTextServicesDocument->FirstBlock(); if (NS_WARN_IF(NS_FAILED(result))) { return result; } currentBlock = 0; while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done && currentBlock < startBlock) { mTextServicesDocument->NextBlock(); } // After we have moved to the block where the first occurrence of replace // was done, put the selection to the next word following it. In case there // is no word following it i.e if it happens to be the last word in that // block, then move to the next block and put the selection to the first // word in that block, otherwise when the Setupdoc() is called, it queries // the LastSelectedBlock() and the selection offset of the last occurrence // of the replaced word is taken instead of the first occurrence and things // get messed up as reported in the bug 244969 if (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) { nsAutoString str; mTextServicesDocument->GetCurrentTextBlock(str); if (mConverter->FindNextWord(str, selOffset, &begin, &end)) { MOZ_KnownLive(mTextServicesDocument) ->SetSelection(AssertedCast(begin), 0); return NS_OK; } mTextServicesDocument->NextBlock(); mTextServicesDocument->GetCurrentTextBlock(str); if (mConverter->FindNextWord(str, 0, &begin, &end)) { MOZ_KnownLive(mTextServicesDocument) ->SetSelection(AssertedCast(begin), 0); } } return NS_OK; } nsresult mozSpellChecker::IgnoreAll(const nsAString& aWord) { if (mPersonalDictionary) { mPersonalDictionary->IgnoreWord(aWord); } return NS_OK; } nsresult mozSpellChecker::AddWordToPersonalDictionary(const nsAString& aWord) { nsresult res; if (NS_WARN_IF(!mPersonalDictionary)) { return NS_ERROR_NOT_INITIALIZED; } res = mPersonalDictionary->AddWord(aWord); return res; } nsresult mozSpellChecker::RemoveWordFromPersonalDictionary( const nsAString& aWord) { nsresult res; if (NS_WARN_IF(!mPersonalDictionary)) { return NS_ERROR_NOT_INITIALIZED; } res = mPersonalDictionary->RemoveWord(aWord); return res; } nsresult mozSpellChecker::GetPersonalDictionary(nsTArray* aWordList) { if (!aWordList || !mPersonalDictionary) return NS_ERROR_NULL_POINTER; nsCOMPtr words; mPersonalDictionary->GetWordList(getter_AddRefs(words)); bool hasMore; nsAutoString word; while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) { words->GetNext(word); aWordList->AppendElement(word); } return NS_OK; } nsresult mozSpellChecker::GetDictionaryList( nsTArray* aDictionaryList) { MOZ_ASSERT(aDictionaryList->IsEmpty()); if (XRE_IsContentProcess()) { ContentChild* child = ContentChild::GetSingleton(); child->GetAvailableDictionaries(*aDictionaryList); return NS_OK; } nsresult rv; // For catching duplicates nsTHashSet dictionaries; nsCOMArray spellCheckingEngines; rv = GetEngineList(&spellCheckingEngines); NS_ENSURE_SUCCESS(rv, rv); for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { nsCOMPtr engine = spellCheckingEngines[i]; nsTArray dictNames; engine->GetDictionaryList(dictNames); for (auto& dictName : dictNames) { // Skip duplicate dictionaries. Only take the first one // for each name. if (!dictionaries.EnsureInserted(dictName)) continue; aDictionaryList->AppendElement(dictName); } } return NS_OK; } nsresult mozSpellChecker::GetCurrentDictionaries( nsTArray& aDictionaries) { if (XRE_IsContentProcess()) { aDictionaries = mCurrentDictionaries.Clone(); return NS_OK; } if (!mSpellCheckingEngine) { aDictionaries.Clear(); return NS_OK; } return mSpellCheckingEngine->GetDictionaries(aDictionaries); } nsresult mozSpellChecker::SetCurrentDictionary(const nsACString& aDictionary) { if (XRE_IsContentProcess()) { mCurrentDictionaries.Clear(); bool isSuccess; mEngine->SendSetDictionary(aDictionary, &isSuccess); if (!isSuccess) { return NS_ERROR_NOT_AVAILABLE; } mCurrentDictionaries.AppendElement(aDictionary); return NS_OK; } // Calls to mozISpellCheckingEngine::SetDictionary might destroy us RefPtr kungFuDeathGrip = this; mSpellCheckingEngine = nullptr; if (aDictionary.IsEmpty()) { return NS_OK; } nsresult rv; nsCOMArray spellCheckingEngines; rv = GetEngineList(&spellCheckingEngines); NS_ENSURE_SUCCESS(rv, rv); nsTArray dictionaries; dictionaries.AppendElement(aDictionary); for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { // We must set mSpellCheckingEngine before we call SetDictionaries, since // SetDictionaries calls back to this spell checker to check if the // dictionary was set mSpellCheckingEngine = spellCheckingEngines[i]; rv = mSpellCheckingEngine->SetDictionaries(dictionaries); if (NS_SUCCEEDED(rv)) { nsCOMPtr personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); mSpellCheckingEngine->SetPersonalDictionary(personalDictionary); mConverter = new mozEnglishWordUtils; return NS_OK; } } mSpellCheckingEngine = nullptr; // We could not find any engine with the requested dictionary return NS_ERROR_NOT_AVAILABLE; } RefPtr mozSpellChecker::SetCurrentDictionaries( const nsTArray& aDictionaries) { if (XRE_IsContentProcess()) { if (!mEngine) { mCurrentDictionaries.Clear(); return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); } // mCurrentDictionaries will be set by RemoteSpellCheckEngineChild return mEngine->SetCurrentDictionaries(aDictionaries); } // Calls to mozISpellCheckingEngine::SetDictionary might destroy us RefPtr kungFuDeathGrip = this; mSpellCheckingEngine = nullptr; if (aDictionaries.IsEmpty()) { return GenericPromise::CreateAndResolve(true, __func__); } nsresult rv; nsCOMArray spellCheckingEngines; rv = GetEngineList(&spellCheckingEngines); if (NS_FAILED(rv)) { return GenericPromise::CreateAndReject(rv, __func__); } for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) { // We must set mSpellCheckingEngine before we call SetDictionaries, since // SetDictionaries calls back to this spell checker to check if the // dictionary was set mSpellCheckingEngine = spellCheckingEngines[i]; rv = mSpellCheckingEngine->SetDictionaries(aDictionaries); if (NS_SUCCEEDED(rv)) { mCurrentDictionaries = aDictionaries.Clone(); nsCOMPtr personalDictionary = do_GetService("@mozilla.org/spellchecker/personaldictionary;1"); mSpellCheckingEngine->SetPersonalDictionary(personalDictionary); mConverter = new mozEnglishWordUtils; return GenericPromise::CreateAndResolve(true, __func__); } } mSpellCheckingEngine = nullptr; // We could not find any engine with the requested dictionary return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); } RefPtr mozSpellChecker::SetCurrentDictionaryFromList( const nsTArray& aList) { if (aList.IsEmpty()) { return GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__); } if (XRE_IsContentProcess()) { if (!mEngine) { mCurrentDictionaries.Clear(); return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); } // mCurrentDictionaries will be set by RemoteSpellCheckEngineChild return mEngine->SetCurrentDictionaryFromList(aList); } for (auto& dictionary : aList) { nsresult rv = SetCurrentDictionary(dictionary); if (NS_SUCCEEDED(rv)) { return GenericPromise::CreateAndResolve(true, __func__); } } // We could not find any engine with the requested dictionary return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); } nsresult mozSpellChecker::SetupDoc(int32_t* outBlockOffset) { nsresult rv; TextServicesDocument::BlockSelectionStatus blockStatus; *outBlockOffset = 0; if (!mFromStart) { uint32_t selOffset, selLength; rv = MOZ_KnownLive(mTextServicesDocument) ->LastSelectedBlock(&blockStatus, &selOffset, &selLength); if (NS_SUCCEEDED(rv) && blockStatus != TextServicesDocument::BlockSelectionStatus::eBlockNotFound) { switch (blockStatus) { // No TB in S, but found one before/after S. case TextServicesDocument::BlockSelectionStatus::eBlockOutside: // S begins or ends in TB but extends outside of TB. case TextServicesDocument::BlockSelectionStatus::eBlockPartial: // the TS doc points to the block we want. if (NS_WARN_IF(selOffset == UINT32_MAX) || NS_WARN_IF(selLength == UINT32_MAX)) { rv = mTextServicesDocument->FirstBlock(); *outBlockOffset = 0; break; } *outBlockOffset = AssertedCast(selOffset + selLength); break; // S extends beyond the start and end of TB. case TextServicesDocument::BlockSelectionStatus::eBlockInside: // we want the block after this one. rv = mTextServicesDocument->NextBlock(); *outBlockOffset = 0; break; // TB contains entire S. case TextServicesDocument::BlockSelectionStatus::eBlockContains: if (NS_WARN_IF(selOffset == UINT32_MAX) || NS_WARN_IF(selLength == UINT32_MAX)) { rv = mTextServicesDocument->FirstBlock(); *outBlockOffset = 0; break; } *outBlockOffset = AssertedCast(selOffset + selLength); break; // There is no text block (TB) in or before the selection (S). case TextServicesDocument::BlockSelectionStatus::eBlockNotFound: default: MOZ_ASSERT_UNREACHABLE("Shouldn't ever get this status"); } } // Failed to get last sel block. Just start at beginning else { rv = mTextServicesDocument->FirstBlock(); *outBlockOffset = 0; } } // We want the first block else { rv = mTextServicesDocument->FirstBlock(); mFromStart = false; } return rv; } // utility method to discover which block we're in. The TSDoc interface doesn't // give us this, because it can't assume a read-only document. shamelessly // stolen from nsTextServicesDocument nsresult mozSpellChecker::GetCurrentBlockIndex( TextServicesDocument* aTextServicesDocument, int32_t* aOutBlockIndex) { int32_t blockIndex = 0; bool isDone = false; nsresult result = NS_OK; do { aTextServicesDocument->PrevBlock(); result = aTextServicesDocument->IsDone(&isDone); if (!isDone) { blockIndex++; } } while (NS_SUCCEEDED(result) && !isDone); *aOutBlockIndex = blockIndex; return result; } nsresult mozSpellChecker::GetEngineList( nsCOMArray* aSpellCheckingEngines) { MOZ_ASSERT(!XRE_IsContentProcess()); nsresult rv; bool hasMoreEngines; nsCOMPtr catMgr = do_GetService(NS_CATEGORYMANAGER_CONTRACTID); if (!catMgr) return NS_ERROR_NULL_POINTER; nsCOMPtr catEntries; // Get contract IDs of registrated external spell-check engines and // append one of HunSpell at the end. rv = catMgr->EnumerateCategory("spell-check-engine", getter_AddRefs(catEntries)); if (NS_FAILED(rv)) return rv; while (NS_SUCCEEDED(catEntries->HasMoreElements(&hasMoreEngines)) && hasMoreEngines) { nsCOMPtr elem; rv = catEntries->GetNext(getter_AddRefs(elem)); nsCOMPtr entry = do_QueryInterface(elem, &rv); if (NS_FAILED(rv)) return rv; nsCString contractId; rv = entry->GetData(contractId); if (NS_FAILED(rv)) return rv; // Try to load spellchecker engine. Ignore errors silently // except for the last one (HunSpell). nsCOMPtr engine = do_GetService(contractId.get(), &rv); if (NS_SUCCEEDED(rv)) { aSpellCheckingEngines->AppendObject(engine); } } // Try to load HunSpell spellchecker engine. nsCOMPtr engine = do_GetService(DEFAULT_SPELL_CHECKER, &rv); if (NS_FAILED(rv)) { // Fail if not succeeded to load HunSpell. Ignore errors // for external spellcheck engines. return rv; } aSpellCheckingEngines->AppendObject(engine); return NS_OK; }