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