1 /* vim: set ts=2 sts=2 sw=2 tw=80: */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "mozSpellChecker.h"
7 #include "nsIStringEnumerator.h"
8 #include "nsICategoryManager.h"
9 #include "nsISupportsPrimitives.h"
10 #include "nsISimpleEnumerator.h"
11 #include "mozEnglishWordUtils.h"
12 #include "mozilla/dom/ContentChild.h"
13 #include "mozilla/Logging.h"
14 #include "mozilla/PRemoteSpellcheckEngineChild.h"
15 #include "mozilla/TextServicesDocument.h"
16 #include "nsXULAppAPI.h"
17 #include "RemoteSpellCheckEngineChild.h"
18 
19 using mozilla::AssertedCast;
20 using mozilla::GenericPromise;
21 using mozilla::LogLevel;
22 using mozilla::PRemoteSpellcheckEngineChild;
23 using mozilla::RemoteSpellcheckEngineChild;
24 using mozilla::TextServicesDocument;
25 using mozilla::dom::ContentChild;
26 
27 #define DEFAULT_SPELL_CHECKER "@mozilla.org/spellchecker/engine;1"
28 
29 static mozilla::LazyLogModule sSpellChecker("SpellChecker");
30 
NS_IMPL_CYCLE_COLLECTION(mozSpellChecker,mTextServicesDocument,mPersonalDictionary)31 NS_IMPL_CYCLE_COLLECTION(mozSpellChecker, mTextServicesDocument,
32                          mPersonalDictionary)
33 
34 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(mozSpellChecker, AddRef)
35 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(mozSpellChecker, Release)
36 
37 mozSpellChecker::mozSpellChecker() : mEngine(nullptr) {}
38 
~mozSpellChecker()39 mozSpellChecker::~mozSpellChecker() {
40   if (mPersonalDictionary) {
41     //    mPersonalDictionary->Save();
42     mPersonalDictionary->EndSession();
43   }
44   mSpellCheckingEngine = nullptr;
45   mPersonalDictionary = nullptr;
46 
47   if (mEngine) {
48     MOZ_ASSERT(XRE_IsContentProcess());
49     RemoteSpellcheckEngineChild::Send__delete__(mEngine);
50     MOZ_ASSERT(!mEngine);
51   }
52 }
53 
Init()54 nsresult mozSpellChecker::Init() {
55   mSpellCheckingEngine = nullptr;
56   if (XRE_IsContentProcess()) {
57     mozilla::dom::ContentChild* contentChild =
58         mozilla::dom::ContentChild::GetSingleton();
59     MOZ_ASSERT(contentChild);
60     mEngine = new RemoteSpellcheckEngineChild(this);
61     contentChild->SendPRemoteSpellcheckEngineConstructor(mEngine);
62   } else {
63     mPersonalDictionary =
64         do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
65   }
66 
67   return NS_OK;
68 }
69 
GetTextServicesDocument()70 TextServicesDocument* mozSpellChecker::GetTextServicesDocument() {
71   return mTextServicesDocument;
72 }
73 
SetDocument(TextServicesDocument * aTextServicesDocument,bool aFromStartofDoc)74 nsresult mozSpellChecker::SetDocument(
75     TextServicesDocument* aTextServicesDocument, bool aFromStartofDoc) {
76   MOZ_LOG(sSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));
77 
78   mTextServicesDocument = aTextServicesDocument;
79   mFromStart = aFromStartofDoc;
80   return NS_OK;
81 }
82 
NextMisspelledWord(nsAString & aWord,nsTArray<nsString> & aSuggestions)83 nsresult mozSpellChecker::NextMisspelledWord(nsAString& aWord,
84                                              nsTArray<nsString>& aSuggestions) {
85   if (NS_WARN_IF(!mConverter)) {
86     return NS_ERROR_NOT_INITIALIZED;
87   }
88 
89   int32_t selOffset;
90   nsresult result;
91   result = SetupDoc(&selOffset);
92   if (NS_FAILED(result)) return result;
93 
94   bool done;
95   while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) {
96     int32_t begin, end;
97     nsAutoString str;
98     mTextServicesDocument->GetCurrentTextBlock(str);
99     while (mConverter->FindNextWord(str, selOffset, &begin, &end)) {
100       const nsDependentSubstring currWord(str, begin, end - begin);
101       bool isMisspelled;
102       result = CheckWord(currWord, &isMisspelled, &aSuggestions);
103       if (NS_WARN_IF(NS_FAILED(result))) {
104         return result;
105       }
106       if (isMisspelled) {
107         aWord = currWord;
108         MOZ_KnownLive(mTextServicesDocument)
109             ->SetSelection(AssertedCast<uint32_t>(begin),
110                            AssertedCast<uint32_t>(end - begin));
111         // After ScrollSelectionIntoView(), the pending notifications might
112         // be flushed and PresShell/PresContext/Frames may be dead.
113         // See bug 418470.
114         mTextServicesDocument->ScrollSelectionIntoView();
115         return NS_OK;
116       }
117       selOffset = end;
118     }
119     mTextServicesDocument->NextBlock();
120     selOffset = 0;
121   }
122   return NS_OK;
123 }
124 
CheckWords(const nsTArray<nsString> & aWords)125 RefPtr<mozilla::CheckWordPromise> mozSpellChecker::CheckWords(
126     const nsTArray<nsString>& aWords) {
127   if (XRE_IsContentProcess()) {
128     return mEngine->CheckWords(aWords);
129   }
130 
131   nsTArray<bool> misspells;
132   misspells.SetCapacity(aWords.Length());
133   for (auto& word : aWords) {
134     bool misspelled;
135     nsresult rv = CheckWord(word, &misspelled, nullptr);
136     if (NS_WARN_IF(NS_FAILED(rv))) {
137       return mozilla::CheckWordPromise::CreateAndReject(rv, __func__);
138     }
139     misspells.AppendElement(misspelled);
140   }
141   return mozilla::CheckWordPromise::CreateAndResolve(std::move(misspells),
142                                                      __func__);
143 }
144 
CheckWord(const nsAString & aWord,bool * aIsMisspelled,nsTArray<nsString> * aSuggestions)145 nsresult mozSpellChecker::CheckWord(const nsAString& aWord, bool* aIsMisspelled,
146                                     nsTArray<nsString>* aSuggestions) {
147   nsresult result;
148   bool correct;
149 
150   if (XRE_IsContentProcess()) {
151     MOZ_ASSERT(aSuggestions, "Use CheckWords if content process");
152     if (!mEngine->SendCheckAndSuggest(nsString(aWord), aIsMisspelled,
153                                       aSuggestions)) {
154       return NS_ERROR_NOT_AVAILABLE;
155     }
156     return NS_OK;
157   }
158 
159   if (!mSpellCheckingEngine) {
160     return NS_ERROR_NULL_POINTER;
161   }
162   *aIsMisspelled = false;
163   result = mSpellCheckingEngine->Check(aWord, &correct);
164   NS_ENSURE_SUCCESS(result, result);
165   if (!correct) {
166     if (aSuggestions) {
167       result = mSpellCheckingEngine->Suggest(aWord, *aSuggestions);
168       NS_ENSURE_SUCCESS(result, result);
169     }
170     *aIsMisspelled = true;
171   }
172   return NS_OK;
173 }
174 
Replace(const nsAString & aOldWord,const nsAString & aNewWord,bool aAllOccurrences)175 nsresult mozSpellChecker::Replace(const nsAString& aOldWord,
176                                   const nsAString& aNewWord,
177                                   bool aAllOccurrences) {
178   if (NS_WARN_IF(!mConverter)) {
179     return NS_ERROR_NOT_INITIALIZED;
180   }
181 
182   if (!aAllOccurrences) {
183     MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord);
184     return NS_OK;
185   }
186 
187   int32_t selOffset;
188   int32_t startBlock;
189   int32_t begin, end;
190   bool done;
191   nsresult result;
192 
193   // find out where we are
194   result = SetupDoc(&selOffset);
195   if (NS_WARN_IF(NS_FAILED(result))) {
196     return result;
197   }
198   result = GetCurrentBlockIndex(mTextServicesDocument, &startBlock);
199   if (NS_WARN_IF(NS_FAILED(result))) {
200     return result;
201   }
202 
203   // start at the beginning
204   result = mTextServicesDocument->FirstBlock();
205   if (NS_WARN_IF(NS_FAILED(result))) {
206     return result;
207   }
208   int32_t currOffset = 0;
209   int32_t currentBlock = 0;
210   while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) {
211     nsAutoString str;
212     mTextServicesDocument->GetCurrentTextBlock(str);
213     while (mConverter->FindNextWord(str, currOffset, &begin, &end)) {
214       if (aOldWord.Equals(Substring(str, begin, end - begin))) {
215         // if we are before the current selection point but in the same
216         // block move the selection point forwards
217         if (currentBlock == startBlock && begin < selOffset) {
218           selOffset += int32_t(aNewWord.Length()) - int32_t(aOldWord.Length());
219           if (selOffset < begin) {
220             selOffset = begin;
221           }
222         }
223         MOZ_KnownLive(mTextServicesDocument)
224             ->SetSelection(AssertedCast<uint32_t>(begin),
225                            AssertedCast<uint32_t>(end - begin));
226         MOZ_KnownLive(mTextServicesDocument)->InsertText(aNewWord);
227         mTextServicesDocument->GetCurrentTextBlock(str);
228         end += (aNewWord.Length() -
229                 aOldWord.Length());  // recursion was cute in GEB, not here.
230       }
231       currOffset = end;
232     }
233     mTextServicesDocument->NextBlock();
234     currentBlock++;
235     currOffset = 0;
236   }
237 
238   // We are done replacing.  Put the selection point back where we found  it
239   // (or equivalent);
240   result = mTextServicesDocument->FirstBlock();
241   if (NS_WARN_IF(NS_FAILED(result))) {
242     return result;
243   }
244   currentBlock = 0;
245   while (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done &&
246          currentBlock < startBlock) {
247     mTextServicesDocument->NextBlock();
248   }
249 
250   // After we have moved to the block where the first occurrence of replace
251   // was done, put the selection to the next word following it. In case there
252   // is no word following it i.e if it happens to be the last word in that
253   // block, then move to the next block and put the selection to the first
254   // word in that block, otherwise when the Setupdoc() is called, it queries
255   // the LastSelectedBlock() and the selection offset of the last occurrence
256   // of the replaced word is taken instead of the first occurrence and things
257   // get messed up as reported in the bug 244969
258 
259   if (NS_SUCCEEDED(mTextServicesDocument->IsDone(&done)) && !done) {
260     nsAutoString str;
261     mTextServicesDocument->GetCurrentTextBlock(str);
262     if (mConverter->FindNextWord(str, selOffset, &begin, &end)) {
263       MOZ_KnownLive(mTextServicesDocument)
264           ->SetSelection(AssertedCast<uint32_t>(begin), 0);
265       return NS_OK;
266     }
267     mTextServicesDocument->NextBlock();
268     mTextServicesDocument->GetCurrentTextBlock(str);
269     if (mConverter->FindNextWord(str, 0, &begin, &end)) {
270       MOZ_KnownLive(mTextServicesDocument)
271           ->SetSelection(AssertedCast<uint32_t>(begin), 0);
272     }
273   }
274   return NS_OK;
275 }
276 
IgnoreAll(const nsAString & aWord)277 nsresult mozSpellChecker::IgnoreAll(const nsAString& aWord) {
278   if (mPersonalDictionary) {
279     mPersonalDictionary->IgnoreWord(aWord);
280   }
281   return NS_OK;
282 }
283 
AddWordToPersonalDictionary(const nsAString & aWord)284 nsresult mozSpellChecker::AddWordToPersonalDictionary(const nsAString& aWord) {
285   nsresult res;
286   if (NS_WARN_IF(!mPersonalDictionary)) {
287     return NS_ERROR_NOT_INITIALIZED;
288   }
289   res = mPersonalDictionary->AddWord(aWord);
290   return res;
291 }
292 
RemoveWordFromPersonalDictionary(const nsAString & aWord)293 nsresult mozSpellChecker::RemoveWordFromPersonalDictionary(
294     const nsAString& aWord) {
295   nsresult res;
296   if (NS_WARN_IF(!mPersonalDictionary)) {
297     return NS_ERROR_NOT_INITIALIZED;
298   }
299   res = mPersonalDictionary->RemoveWord(aWord);
300   return res;
301 }
302 
GetPersonalDictionary(nsTArray<nsString> * aWordList)303 nsresult mozSpellChecker::GetPersonalDictionary(nsTArray<nsString>* aWordList) {
304   if (!aWordList || !mPersonalDictionary) return NS_ERROR_NULL_POINTER;
305 
306   nsCOMPtr<nsIStringEnumerator> words;
307   mPersonalDictionary->GetWordList(getter_AddRefs(words));
308 
309   bool hasMore;
310   nsAutoString word;
311   while (NS_SUCCEEDED(words->HasMore(&hasMore)) && hasMore) {
312     words->GetNext(word);
313     aWordList->AppendElement(word);
314   }
315   return NS_OK;
316 }
317 
GetDictionaryList(nsTArray<nsCString> * aDictionaryList)318 nsresult mozSpellChecker::GetDictionaryList(
319     nsTArray<nsCString>* aDictionaryList) {
320   MOZ_ASSERT(aDictionaryList->IsEmpty());
321   if (XRE_IsContentProcess()) {
322     ContentChild* child = ContentChild::GetSingleton();
323     child->GetAvailableDictionaries(*aDictionaryList);
324     return NS_OK;
325   }
326 
327   nsresult rv;
328 
329   // For catching duplicates
330   nsTHashSet<nsCString> dictionaries;
331 
332   nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
333   rv = GetEngineList(&spellCheckingEngines);
334   NS_ENSURE_SUCCESS(rv, rv);
335 
336   for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) {
337     nsCOMPtr<mozISpellCheckingEngine> engine = spellCheckingEngines[i];
338 
339     nsTArray<nsCString> dictNames;
340     engine->GetDictionaryList(dictNames);
341     for (auto& dictName : dictNames) {
342       // Skip duplicate dictionaries. Only take the first one
343       // for each name.
344       if (!dictionaries.EnsureInserted(dictName)) continue;
345 
346       aDictionaryList->AppendElement(dictName);
347     }
348   }
349 
350   return NS_OK;
351 }
352 
GetCurrentDictionary(nsACString & aDictionary)353 nsresult mozSpellChecker::GetCurrentDictionary(nsACString& aDictionary) {
354   if (XRE_IsContentProcess()) {
355     aDictionary = mCurrentDictionary;
356     return NS_OK;
357   }
358 
359   if (!mSpellCheckingEngine) {
360     aDictionary.Truncate();
361     return NS_OK;
362   }
363 
364   return mSpellCheckingEngine->GetDictionary(aDictionary);
365 }
366 
SetCurrentDictionary(const nsACString & aDictionary)367 nsresult mozSpellChecker::SetCurrentDictionary(const nsACString& aDictionary) {
368   if (XRE_IsContentProcess()) {
369     nsCString wrappedDict = nsCString(aDictionary);
370     bool isSuccess;
371     mEngine->SendSetDictionary(wrappedDict, &isSuccess);
372     if (!isSuccess) {
373       mCurrentDictionary.Truncate();
374       return NS_ERROR_NOT_AVAILABLE;
375     }
376 
377     mCurrentDictionary = wrappedDict;
378     return NS_OK;
379   }
380 
381   // Calls to mozISpellCheckingEngine::SetDictionary might destroy us
382   RefPtr<mozSpellChecker> kungFuDeathGrip = this;
383 
384   mSpellCheckingEngine = nullptr;
385 
386   if (aDictionary.IsEmpty()) {
387     return NS_OK;
388   }
389 
390   nsresult rv;
391   nsCOMArray<mozISpellCheckingEngine> spellCheckingEngines;
392   rv = GetEngineList(&spellCheckingEngines);
393   NS_ENSURE_SUCCESS(rv, rv);
394 
395   for (int32_t i = 0; i < spellCheckingEngines.Count(); i++) {
396     // We must set mSpellCheckingEngine before we call SetDictionary, since
397     // SetDictionary calls back to this spell checker to check if the
398     // dictionary was set
399     mSpellCheckingEngine = spellCheckingEngines[i];
400 
401     rv = mSpellCheckingEngine->SetDictionary(aDictionary);
402 
403     if (NS_SUCCEEDED(rv)) {
404       nsCOMPtr<mozIPersonalDictionary> personalDictionary =
405           do_GetService("@mozilla.org/spellchecker/personaldictionary;1");
406       mSpellCheckingEngine->SetPersonalDictionary(personalDictionary.get());
407 
408       mConverter = new mozEnglishWordUtils;
409       return NS_OK;
410     }
411   }
412 
413   mSpellCheckingEngine = nullptr;
414 
415   // We could not find any engine with the requested dictionary
416   return NS_ERROR_NOT_AVAILABLE;
417 }
418 
SetCurrentDictionaryFromList(const nsTArray<nsCString> & aList)419 RefPtr<GenericPromise> mozSpellChecker::SetCurrentDictionaryFromList(
420     const nsTArray<nsCString>& aList) {
421   if (aList.IsEmpty()) {
422     return GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
423   }
424 
425   if (XRE_IsContentProcess()) {
426     // mCurrentDictionary will be set by RemoteSpellCheckEngineChild
427     return mEngine->SetCurrentDictionaryFromList(aList);
428   }
429 
430   for (auto& dictionary : aList) {
431     nsresult rv = SetCurrentDictionary(dictionary);
432     if (NS_SUCCEEDED(rv)) {
433       return GenericPromise::CreateAndResolve(true, __func__);
434     }
435   }
436   // We could not find any engine with the requested dictionary
437   return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
438 }
439 
SetupDoc(int32_t * outBlockOffset)440 nsresult mozSpellChecker::SetupDoc(int32_t* outBlockOffset) {
441   nsresult rv;
442 
443   TextServicesDocument::BlockSelectionStatus blockStatus;
444   *outBlockOffset = 0;
445 
446   if (!mFromStart) {
447     uint32_t selOffset, selLength;
448     rv = MOZ_KnownLive(mTextServicesDocument)
449              ->LastSelectedBlock(&blockStatus, &selOffset, &selLength);
450     if (NS_SUCCEEDED(rv) &&
451         blockStatus !=
452             TextServicesDocument::BlockSelectionStatus::eBlockNotFound) {
453       switch (blockStatus) {
454         // No TB in S, but found one before/after S.
455         case TextServicesDocument::BlockSelectionStatus::eBlockOutside:
456         // S begins or ends in TB but extends outside of TB.
457         case TextServicesDocument::BlockSelectionStatus::eBlockPartial:
458           // the TS doc points to the block we want.
459           MOZ_ASSERT(selOffset != UINT32_MAX || selLength != UINT32_MAX);
460           *outBlockOffset = AssertedCast<int32_t>(selOffset + selLength);
461           break;
462 
463         // S extends beyond the start and end of TB.
464         case TextServicesDocument::BlockSelectionStatus::eBlockInside:
465           // we want the block after this one.
466           rv = mTextServicesDocument->NextBlock();
467           *outBlockOffset = 0;
468           break;
469 
470         // TB contains entire S.
471         case TextServicesDocument::BlockSelectionStatus::eBlockContains:
472           MOZ_ASSERT(selOffset != UINT32_MAX || selLength != UINT32_MAX);
473           *outBlockOffset = AssertedCast<int32_t>(selOffset + selLength);
474           break;
475 
476         // There is no text block (TB) in or before the selection (S).
477         case TextServicesDocument::BlockSelectionStatus::eBlockNotFound:
478         default:
479           MOZ_ASSERT_UNREACHABLE("Shouldn't ever get this status");
480       }
481     }
482     // Failed to get last sel block. Just start at beginning
483     else {
484       rv = mTextServicesDocument->FirstBlock();
485       *outBlockOffset = 0;
486     }
487 
488   }
489   // We want the first block
490   else {
491     rv = mTextServicesDocument->FirstBlock();
492     mFromStart = false;
493   }
494   return rv;
495 }
496 
497 // utility method to discover which block we're in. The TSDoc interface doesn't
498 // give us this, because it can't assume a read-only document. shamelessly
499 // stolen from nsTextServicesDocument
GetCurrentBlockIndex(TextServicesDocument * aTextServicesDocument,int32_t * aOutBlockIndex)500 nsresult mozSpellChecker::GetCurrentBlockIndex(
501     TextServicesDocument* aTextServicesDocument, int32_t* aOutBlockIndex) {
502   int32_t blockIndex = 0;
503   bool isDone = false;
504   nsresult result = NS_OK;
505 
506   do {
507     aTextServicesDocument->PrevBlock();
508     result = aTextServicesDocument->IsDone(&isDone);
509     if (!isDone) {
510       blockIndex++;
511     }
512   } while (NS_SUCCEEDED(result) && !isDone);
513 
514   *aOutBlockIndex = blockIndex;
515 
516   return result;
517 }
518 
GetEngineList(nsCOMArray<mozISpellCheckingEngine> * aSpellCheckingEngines)519 nsresult mozSpellChecker::GetEngineList(
520     nsCOMArray<mozISpellCheckingEngine>* aSpellCheckingEngines) {
521   MOZ_ASSERT(!XRE_IsContentProcess());
522 
523   nsresult rv;
524   bool hasMoreEngines;
525 
526   nsCOMPtr<nsICategoryManager> catMgr =
527       do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
528   if (!catMgr) return NS_ERROR_NULL_POINTER;
529 
530   nsCOMPtr<nsISimpleEnumerator> catEntries;
531 
532   // Get contract IDs of registrated external spell-check engines and
533   // append one of HunSpell at the end.
534   rv = catMgr->EnumerateCategory("spell-check-engine",
535                                  getter_AddRefs(catEntries));
536   if (NS_FAILED(rv)) return rv;
537 
538   while (NS_SUCCEEDED(catEntries->HasMoreElements(&hasMoreEngines)) &&
539          hasMoreEngines) {
540     nsCOMPtr<nsISupports> elem;
541     rv = catEntries->GetNext(getter_AddRefs(elem));
542 
543     nsCOMPtr<nsISupportsCString> entry = do_QueryInterface(elem, &rv);
544     if (NS_FAILED(rv)) return rv;
545 
546     nsCString contractId;
547     rv = entry->GetData(contractId);
548     if (NS_FAILED(rv)) return rv;
549 
550     // Try to load spellchecker engine. Ignore errors silently
551     // except for the last one (HunSpell).
552     nsCOMPtr<mozISpellCheckingEngine> engine =
553         do_GetService(contractId.get(), &rv);
554     if (NS_SUCCEEDED(rv)) {
555       aSpellCheckingEngines->AppendObject(engine);
556     }
557   }
558 
559   // Try to load HunSpell spellchecker engine.
560   nsCOMPtr<mozISpellCheckingEngine> engine =
561       do_GetService(DEFAULT_SPELL_CHECKER, &rv);
562   if (NS_FAILED(rv)) {
563     // Fail if not succeeded to load HunSpell. Ignore errors
564     // for external spellcheck engines.
565     return rv;
566   }
567   aSpellCheckingEngines->AppendObject(engine);
568 
569   return NS_OK;
570 }
571