Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/editor/spellchecker/EditorSpellCheck.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* vim: set ts=2 sts=2 sw=2 tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
 * License, v. 2.0. If a copy of the MPL was not distributed with this
5
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
#include "EditorSpellCheck.h"
8
9
#include "mozilla/Attributes.h"         // for final
10
#include "mozilla/EditorBase.h"         // for EditorBase
11
#include "mozilla/HTMLEditor.h"         // for HTMLEditor
12
#include "mozilla/dom/Element.h"        // for Element
13
#include "mozilla/dom/Selection.h"
14
#include "mozilla/intl/LocaleService.h" // for retrieving app locale
15
#include "mozilla/mozalloc.h"           // for operator delete, etc
16
#include "mozilla/mozSpellChecker.h"    // for mozSpellChecker
17
#include "mozilla/Preferences.h"        // for Preferences
18
#include "mozilla/TextServicesDocument.h" // for TextServicesDocument
19
#include "nsAString.h"                  // for nsAString::IsEmpty, etc
20
#include "nsComponentManagerUtils.h"    // for do_CreateInstance
21
#include "nsDebug.h"                    // for NS_ENSURE_TRUE, etc
22
#include "nsDependentSubstring.h"       // for Substring
23
#include "nsError.h"                    // for NS_ERROR_NOT_INITIALIZED, etc
24
#include "nsIContent.h"                 // for nsIContent
25
#include "nsIContentPrefService2.h"     // for nsIContentPrefService2, etc
26
#include "nsIDocument.h"                // for nsIDocument
27
#include "nsIEditor.h"                  // for nsIEditor
28
#include "nsILoadContext.h"
29
#include "nsISupportsBase.h"            // for nsISupports
30
#include "nsISupportsUtils.h"           // for NS_ADDREF
31
#include "nsIURI.h"                     // for nsIURI
32
#include "nsThreadUtils.h"              // for GetMainThreadSerialEventTarget
33
#include "nsVariant.h"                  // for nsIWritableVariant, etc
34
#include "nsLiteralString.h"            // for NS_LITERAL_STRING, etc
35
#include "nsMemory.h"                   // for nsMemory
36
#include "nsRange.h"
37
#include "nsReadableUtils.h"            // for ToNewUnicode, EmptyString, etc
38
#include "nsServiceManagerUtils.h"      // for do_GetService
39
#include "nsString.h"                   // for nsAutoString, nsString, etc
40
#include "nsStringFwd.h"                // for nsAFlatString
41
#include "nsStyleUtil.h"                // for nsStyleUtil
42
#include "nsXULAppAPI.h"                // for XRE_GetProcessType
43
#include "nsIPlaintextEditor.h"         // for editor flags
44
45
namespace mozilla {
46
47
using namespace dom;
48
using intl::LocaleService;
49
50
class UpdateDictionaryHolder
51
{
52
private:
53
  EditorSpellCheck* mSpellCheck;
54
55
public:
56
  explicit UpdateDictionaryHolder(EditorSpellCheck* esc)
57
    : mSpellCheck(esc)
58
0
  {
59
0
    if (mSpellCheck) {
60
0
      mSpellCheck->BeginUpdateDictionary();
61
0
    }
62
0
  }
63
64
0
  ~UpdateDictionaryHolder() {
65
0
    if (mSpellCheck) {
66
0
      mSpellCheck->EndUpdateDictionary();
67
0
    }
68
0
  }
69
};
70
71
0
#define CPS_PREF_NAME NS_LITERAL_STRING("spellcheck.lang")
72
73
/**
74
 * Gets the URI of aEditor's document.
75
 */
76
static nsIURI*
77
GetDocumentURI(EditorBase* aEditor)
78
0
{
79
0
  MOZ_ASSERT(aEditor);
80
0
81
0
  nsIDocument* doc = aEditor->AsEditorBase()->GetDocument();
82
0
  if (NS_WARN_IF(!doc)) {
83
0
    return nullptr;
84
0
  }
85
0
86
0
  return doc->GetDocumentURI();
87
0
}
88
89
static nsILoadContext*
90
GetLoadContext(nsIEditor* aEditor)
91
0
{
92
0
  nsIDocument* doc = aEditor->AsEditorBase()->GetDocument();
93
0
  if (NS_WARN_IF(!doc)) {
94
0
    return nullptr;
95
0
  }
96
0
97
0
  return doc->GetLoadContext();
98
0
}
99
100
/**
101
 * Fetches the dictionary stored in content prefs and maintains state during the
102
 * fetch, which is asynchronous.
103
 */
104
class DictionaryFetcher final : public nsIContentPrefCallback2
105
{
106
public:
107
  NS_DECL_ISUPPORTS
108
109
  DictionaryFetcher(EditorSpellCheck* aSpellCheck,
110
                    nsIEditorSpellCheckCallback* aCallback,
111
                    uint32_t aGroup)
112
    : mCallback(aCallback)
113
    , mGroup(aGroup)
114
    , mSpellCheck(aSpellCheck)
115
0
  {
116
0
  }
117
118
  NS_IMETHOD Fetch(nsIEditor* aEditor);
119
120
  NS_IMETHOD HandleResult(nsIContentPref* aPref) override
121
0
  {
122
0
    nsCOMPtr<nsIVariant> value;
123
0
    nsresult rv = aPref->GetValue(getter_AddRefs(value));
124
0
    NS_ENSURE_SUCCESS(rv, rv);
125
0
    value->GetAsAString(mDictionary);
126
0
    return NS_OK;
127
0
  }
128
129
  NS_IMETHOD HandleCompletion(uint16_t reason) override
130
0
  {
131
0
    mSpellCheck->DictionaryFetched(this);
132
0
    return NS_OK;
133
0
  }
134
135
  NS_IMETHOD HandleError(nsresult error) override
136
0
  {
137
0
    return NS_OK;
138
0
  }
139
140
  nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
141
  uint32_t mGroup;
142
  nsString mRootContentLang;
143
  nsString mRootDocContentLang;
144
  nsString mDictionary;
145
146
private:
147
0
  ~DictionaryFetcher() {}
148
149
  RefPtr<EditorSpellCheck> mSpellCheck;
150
};
151
152
NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)
153
154
class ContentPrefInitializerRunnable final : public Runnable
155
{
156
public:
157
  ContentPrefInitializerRunnable(nsIEditor* aEditor,
158
                                 nsIContentPrefCallback2* aCallback)
159
    : Runnable("ContentPrefInitializerRunnable")
160
    , mEditorBase(aEditor->AsEditorBase())
161
    , mCallback(aCallback)
162
0
  {
163
0
  }
164
165
  NS_IMETHOD Run() override
166
0
  {
167
0
    if (mEditorBase->Destroyed()) {
168
0
      mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
169
0
      return NS_OK;
170
0
    }
171
0
172
0
    nsCOMPtr<nsIContentPrefService2> contentPrefService =
173
0
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
174
0
    if (NS_WARN_IF(!contentPrefService)) {
175
0
      mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
176
0
      return NS_OK;
177
0
    }
178
0
179
0
    nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
180
0
    if (NS_WARN_IF(!docUri)) {
181
0
      mCallback->HandleError(NS_ERROR_FAILURE);
182
0
      return NS_OK;
183
0
    }
184
0
185
0
    nsAutoCString docUriSpec;
186
0
    nsresult rv = docUri->GetSpec(docUriSpec);
187
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
188
0
      mCallback->HandleError(rv);
189
0
      return NS_OK;
190
0
    }
191
0
192
0
    rv = contentPrefService->GetByDomainAndName(
193
0
                               NS_ConvertUTF8toUTF16(docUriSpec),
194
0
                               CPS_PREF_NAME,
195
0
                               GetLoadContext(mEditorBase),
196
0
                               mCallback);
197
0
    if (NS_WARN_IF(NS_FAILED(rv))) {
198
0
      mCallback->HandleError(rv);
199
0
      return NS_OK;
200
0
    }
201
0
    return NS_OK;
202
0
  }
203
204
private:
205
  RefPtr<EditorBase> mEditorBase;
206
  nsCOMPtr<nsIContentPrefCallback2> mCallback;
207
};
208
209
NS_IMETHODIMP
210
DictionaryFetcher::Fetch(nsIEditor* aEditor)
211
0
{
212
0
  NS_ENSURE_ARG_POINTER(aEditor);
213
0
214
0
  nsCOMPtr<nsIRunnable> runnable =
215
0
     new ContentPrefInitializerRunnable(aEditor, this);
216
0
  NS_IdleDispatchToCurrentThread(runnable.forget(), 1000);
217
0
218
0
  return NS_OK;
219
0
}
220
221
/**
222
 * Stores the current dictionary for aEditor's document URL.
223
 */
224
static nsresult
225
StoreCurrentDictionary(EditorBase* aEditorBase, const nsAString& aDictionary)
226
0
{
227
0
  NS_ENSURE_ARG_POINTER(aEditorBase);
228
0
229
0
  nsresult rv;
230
0
231
0
  nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
232
0
  if (NS_WARN_IF(!docUri)) {
233
0
    return NS_ERROR_FAILURE;
234
0
  }
235
0
236
0
  nsAutoCString docUriSpec;
237
0
  rv = docUri->GetSpec(docUriSpec);
238
0
  NS_ENSURE_SUCCESS(rv, rv);
239
0
240
0
  RefPtr<nsVariant> prefValue = new nsVariant();
241
0
  prefValue->SetAsAString(aDictionary);
242
0
243
0
  nsCOMPtr<nsIContentPrefService2> contentPrefService =
244
0
    do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
245
0
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
246
0
247
0
  return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
248
0
                                 CPS_PREF_NAME, prefValue,
249
0
                                 GetLoadContext(aEditorBase),
250
0
                                 nullptr);
251
0
}
252
253
/**
254
 * Forgets the current dictionary stored for aEditor's document URL.
255
 */
256
static nsresult
257
ClearCurrentDictionary(EditorBase* aEditorBase)
258
0
{
259
0
  NS_ENSURE_ARG_POINTER(aEditorBase);
260
0
261
0
  nsresult rv;
262
0
263
0
  nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
264
0
  if (NS_WARN_IF(!docUri)) {
265
0
    return NS_ERROR_FAILURE;
266
0
  }
267
0
268
0
  nsAutoCString docUriSpec;
269
0
  rv = docUri->GetSpec(docUriSpec);
270
0
  NS_ENSURE_SUCCESS(rv, rv);
271
0
272
0
  nsCOMPtr<nsIContentPrefService2> contentPrefService =
273
0
    do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
274
0
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);
275
0
276
0
  return contentPrefService->RemoveByDomainAndName(
277
0
                               NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
278
0
                               GetLoadContext(aEditorBase), nullptr);
279
0
}
280
281
NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
282
NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)
283
284
0
NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
285
0
  NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
286
0
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
287
0
  NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
288
0
NS_INTERFACE_MAP_END
289
290
NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck,
291
                         mEditor,
292
                         mSpellChecker)
293
294
EditorSpellCheck::EditorSpellCheck()
295
  : mTxtSrvFilterType(0)
296
  , mSuggestedWordIndex(0)
297
  , mDictionaryIndex(0)
298
  , mDictionaryFetcherGroup(0)
299
  , mUpdateDictionaryRunning(false)
300
0
{
301
0
}
302
303
EditorSpellCheck::~EditorSpellCheck()
304
0
{
305
0
  // Make sure we blow the spellchecker away, just in
306
0
  // case it hasn't been destroyed already.
307
0
  mSpellChecker = nullptr;
308
0
}
309
310
mozSpellChecker*
311
EditorSpellCheck::GetSpellChecker()
312
0
{
313
0
  return mSpellChecker;
314
0
}
315
316
// The problem is that if the spell checker does not exist, we can not tell
317
// which dictionaries are installed. This function works around the problem,
318
// allowing callers to ask if we can spell check without actually doing so (and
319
// enabling or disabling UI as necessary). This just creates a spellcheck
320
// object if needed and asks it for the dictionary list.
321
NS_IMETHODIMP
322
EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck)
323
0
{
324
0
  RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
325
0
  if (!spellChecker) {
326
0
    spellChecker = mozSpellChecker::Create();
327
0
    MOZ_ASSERT(spellChecker);
328
0
  }
329
0
  nsTArray<nsString> dictList;
330
0
  nsresult rv = spellChecker->GetDictionaryList(&dictList);
331
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
332
0
    return rv;
333
0
  }
334
0
335
0
  *aCanSpellCheck = !dictList.IsEmpty();
336
0
  return NS_OK;
337
0
}
338
339
// Instances of this class can be used as either runnables or RAII helpers.
340
class CallbackCaller final : public Runnable
341
{
342
public:
343
  explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
344
    : mozilla::Runnable("CallbackCaller")
345
    , mCallback(aCallback)
346
0
  {
347
0
  }
348
349
  ~CallbackCaller()
350
0
  {
351
0
    Run();
352
0
  }
353
354
  NS_IMETHOD Run() override
355
0
  {
356
0
    if (mCallback) {
357
0
      mCallback->EditorSpellCheckDone();
358
0
      mCallback = nullptr;
359
0
    }
360
0
    return NS_OK;
361
0
  }
362
363
private:
364
  nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
365
};
366
367
NS_IMETHODIMP
368
EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
369
                                   bool aEnableSelectionChecking,
370
                                   nsIEditorSpellCheckCallback* aCallback)
371
0
{
372
0
  NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
373
0
  mEditor = aEditor->AsEditorBase();
374
0
375
0
  nsCOMPtr<nsIDocument> doc = mEditor->GetDocument();
376
0
  if (NS_WARN_IF(!doc)) {
377
0
    return NS_ERROR_FAILURE;
378
0
  }
379
0
380
0
  nsresult rv;
381
0
382
0
  // We can spell check with any editor type
383
0
  RefPtr<TextServicesDocument> textServicesDocument =
384
0
    new TextServicesDocument();
385
0
  textServicesDocument->SetFilterType(mTxtSrvFilterType);
386
0
387
0
  // EditorBase::AddEditActionListener() needs to access mSpellChecker and
388
0
  // mSpellChecker->GetTextServicesDocument().  Therefore, we need to
389
0
  // initialize them before calling TextServicesDocument::InitWithEditor()
390
0
  // since it calls EditorBase::AddEditActionListener().
391
0
  mSpellChecker = mozSpellChecker::Create();
392
0
  MOZ_ASSERT(mSpellChecker);
393
0
  rv = mSpellChecker->SetDocument(textServicesDocument, true);
394
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
395
0
    return rv;
396
0
  }
397
0
398
0
  // Pass the editor to the text services document
399
0
  rv = textServicesDocument->InitWithEditor(aEditor);
400
0
  NS_ENSURE_SUCCESS(rv, rv);
401
0
402
0
  if (aEnableSelectionChecking) {
403
0
    // Find out if the section is collapsed or not.
404
0
    // If it isn't, we want to spellcheck just the selection.
405
0
406
0
    RefPtr<Selection> selection;
407
0
    aEditor->GetSelection(getter_AddRefs(selection));
408
0
    if (NS_WARN_IF(!selection)) {
409
0
      return NS_ERROR_FAILURE;
410
0
    }
411
0
412
0
    if (selection->RangeCount()) {
413
0
      RefPtr<nsRange> range = selection->GetRangeAt(0);
414
0
      NS_ENSURE_STATE(range);
415
0
416
0
      if (!range->Collapsed()) {
417
0
        // We don't want to touch the range in the selection,
418
0
        // so create a new copy of it.
419
0
420
0
        RefPtr<nsRange> rangeBounds = range->CloneRange();
421
0
422
0
        // Make sure the new range spans complete words.
423
0
424
0
        rv = textServicesDocument->ExpandRangeToWordBoundaries(rangeBounds);
425
0
        NS_ENSURE_SUCCESS(rv, rv);
426
0
427
0
        // Now tell the text services that you only want
428
0
        // to iterate over the text in this range.
429
0
430
0
        rv = textServicesDocument->SetExtent(rangeBounds);
431
0
        NS_ENSURE_SUCCESS(rv, rv);
432
0
      }
433
0
    }
434
0
  }
435
0
  // do not fail if UpdateCurrentDictionary fails because this method may
436
0
  // succeed later.
437
0
  rv = UpdateCurrentDictionary(aCallback);
438
0
  if (NS_FAILED(rv) && aCallback) {
439
0
    // However, if it does fail, we still need to call the callback since we
440
0
    // discard the failure.  Do it asynchronously so that the caller is always
441
0
    // guaranteed async behavior.
442
0
    RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
443
0
    rv = doc->Dispatch(TaskCategory::Other, caller.forget());
444
0
    NS_ENSURE_SUCCESS(rv, rv);
445
0
  }
446
0
447
0
  return NS_OK;
448
0
}
449
450
NS_IMETHODIMP
451
EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord)
452
0
{
453
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
454
0
455
0
  DeleteSuggestedWordList();
456
0
  // Beware! This may flush notifications via synchronous
457
0
  // ScrollSelectionIntoView.
458
0
  return mSpellChecker->NextMisspelledWord(aNextMisspelledWord,
459
0
                                           &mSuggestedWordList);
460
0
}
461
462
NS_IMETHODIMP
463
EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord)
464
0
{
465
0
  // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
466
0
  if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
467
0
    aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
468
0
    mSuggestedWordIndex++;
469
0
  } else {
470
0
    // A blank string signals that there are no more strings
471
0
    aSuggestedWord.Truncate();
472
0
  }
473
0
  return NS_OK;
474
0
}
475
476
NS_IMETHODIMP
477
EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
478
                                   bool* aIsMisspelled)
479
0
{
480
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
481
0
482
0
  DeleteSuggestedWordList();
483
0
  return mSpellChecker->CheckWord(aSuggestedWord,
484
0
                                  aIsMisspelled, &mSuggestedWordList);
485
0
}
486
487
NS_IMETHODIMP
488
EditorSpellCheck::CheckCurrentWordNoSuggest(const nsAString& aSuggestedWord,
489
                                            bool* aIsMisspelled)
490
0
{
491
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
492
0
493
0
  return mSpellChecker->CheckWord(aSuggestedWord,
494
0
                                  aIsMisspelled, nullptr);
495
0
}
496
497
NS_IMETHODIMP
498
EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
499
                              const nsAString& aReplaceWord,
500
                              bool aAllOccurrences)
501
0
{
502
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
503
0
504
0
  return mSpellChecker->Replace(aMisspelledWord,
505
0
                                aReplaceWord, aAllOccurrences);
506
0
}
507
508
NS_IMETHODIMP
509
EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord)
510
0
{
511
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
512
0
513
0
  return mSpellChecker->IgnoreAll(aWord);
514
0
}
515
516
NS_IMETHODIMP
517
EditorSpellCheck::GetPersonalDictionary()
518
0
{
519
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
520
0
521
0
   // We can spell check with any editor type
522
0
  mDictionaryList.Clear();
523
0
  mDictionaryIndex = 0;
524
0
  return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
525
0
}
526
527
NS_IMETHODIMP
528
EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord)
529
0
{
530
0
  // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
531
0
  if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
532
0
    aDictionaryWord = mDictionaryList[mDictionaryIndex];
533
0
    mDictionaryIndex++;
534
0
  } else {
535
0
    // A blank string signals that there are no more strings
536
0
    aDictionaryWord.Truncate();
537
0
  }
538
0
539
0
  return NS_OK;
540
0
}
541
542
NS_IMETHODIMP
543
EditorSpellCheck::AddWordToDictionary(const nsAString& aWord)
544
0
{
545
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
546
0
547
0
  return mSpellChecker->AddWordToPersonalDictionary(aWord);
548
0
}
549
550
NS_IMETHODIMP
551
EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord)
552
0
{
553
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
554
0
555
0
  return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
556
0
}
557
558
NS_IMETHODIMP
559
EditorSpellCheck::GetDictionaryList(char16_t*** aDictionaryList,
560
                                    uint32_t* aCount)
561
0
{
562
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
563
0
564
0
  NS_ENSURE_TRUE(aDictionaryList && aCount, NS_ERROR_NULL_POINTER);
565
0
566
0
  *aDictionaryList = 0;
567
0
  *aCount          = 0;
568
0
569
0
  nsTArray<nsString> dictList;
570
0
571
0
  nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
572
0
573
0
  NS_ENSURE_SUCCESS(rv, rv);
574
0
575
0
  char16_t **tmpPtr = 0;
576
0
577
0
  if (dictList.IsEmpty()) {
578
0
    // If there are no dictionaries, return an array containing
579
0
    // one element and a count of one.
580
0
581
0
    tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *));
582
0
583
0
    *tmpPtr          = 0;
584
0
    *aDictionaryList = tmpPtr;
585
0
    *aCount          = 0;
586
0
587
0
    return NS_OK;
588
0
  }
589
0
590
0
  tmpPtr = (char16_t **)moz_xmalloc(sizeof(char16_t *) * dictList.Length());
591
0
592
0
  *aDictionaryList = tmpPtr;
593
0
  *aCount          = dictList.Length();
594
0
595
0
  for (uint32_t i = 0; i < *aCount; i++) {
596
0
    tmpPtr[i] = ToNewUnicode(dictList[i]);
597
0
  }
598
0
599
0
  return rv;
600
0
}
601
602
NS_IMETHODIMP
603
EditorSpellCheck::GetCurrentDictionary(nsAString& aDictionary)
604
0
{
605
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
606
0
607
0
  return mSpellChecker->GetCurrentDictionary(aDictionary);
608
0
}
609
610
NS_IMETHODIMP
611
EditorSpellCheck::SetCurrentDictionary(const nsAString& aDictionary)
612
0
{
613
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
614
0
615
0
  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
616
0
617
0
  // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
618
0
  // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
619
0
  // is on the stack. In other words: Only do this, if the user manually selected a
620
0
  // dictionary to use.
621
0
  if (!mUpdateDictionaryRunning) {
622
0
623
0
    // Ignore pending dictionary fetchers by increasing this number.
624
0
    mDictionaryFetcherGroup++;
625
0
626
0
    uint32_t flags = 0;
627
0
    mEditor->GetFlags(&flags);
628
0
    if (!(flags & nsIPlaintextEditor::eEditorMailMask)) {
629
0
      if (!aDictionary.IsEmpty() && (mPreferredLang.IsEmpty() ||
630
0
          !mPreferredLang.Equals(aDictionary,
631
0
                                 nsCaseInsensitiveStringComparator()))) {
632
0
        // When user sets dictionary manually, we store this value associated
633
0
        // with editor url, if it doesn't match the document language exactly.
634
0
        // For example on "en" sites, we need to store "en-GB", otherwise
635
0
        // the language might jump back to en-US although the user explicitly
636
0
        // chose otherwise.
637
0
        StoreCurrentDictionary(mEditor, aDictionary);
638
#ifdef DEBUG_DICT
639
        printf("***** Writing content preferences for |%s|\n",
640
               NS_ConvertUTF16toUTF8(aDictionary).get());
641
#endif
642
0
      } else {
643
0
        // If user sets a dictionary matching the language defined by
644
0
        // document, we consider content pref has been canceled, and we clear it.
645
0
        ClearCurrentDictionary(mEditor);
646
#ifdef DEBUG_DICT
647
        printf("***** Clearing content preferences for |%s|\n",
648
               NS_ConvertUTF16toUTF8(aDictionary).get());
649
#endif
650
      }
651
0
652
0
      // Also store it in as a preference, so we can use it as a fallback.
653
0
      // We don't want this for mail composer because it uses
654
0
      // "spellchecker.dictionary" as a preference.
655
0
      //
656
0
      // XXX: Prefs can only be set in the parent process, so this condition is
657
0
      // necessary to stop libpref from throwing errors. But this should
658
0
      // probably be handled in a better way.
659
0
      if (XRE_IsParentProcess()) {
660
0
        Preferences::SetString("spellchecker.dictionary", aDictionary);
661
#ifdef DEBUG_DICT
662
        printf("***** Possibly storing spellchecker.dictionary |%s|\n",
663
               NS_ConvertUTF16toUTF8(aDictionary).get());
664
#endif
665
      }
666
0
    }
667
0
  }
668
0
  return mSpellChecker->SetCurrentDictionary(aDictionary);
669
0
}
670
671
NS_IMETHODIMP
672
EditorSpellCheck::UninitSpellChecker()
673
0
{
674
0
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
675
0
676
0
  // Cleanup - kill the spell checker
677
0
  DeleteSuggestedWordList();
678
0
  mDictionaryList.Clear();
679
0
  mDictionaryIndex = 0;
680
0
  mDictionaryFetcherGroup++;
681
0
  mSpellChecker = nullptr;
682
0
  return NS_OK;
683
0
}
684
685
686
NS_IMETHODIMP
687
EditorSpellCheck::SetFilterType(uint32_t aFilterType)
688
0
{
689
0
  mTxtSrvFilterType = aFilterType;
690
0
  return NS_OK;
691
0
}
692
693
nsresult
694
EditorSpellCheck::DeleteSuggestedWordList()
695
0
{
696
0
  mSuggestedWordList.Clear();
697
0
  mSuggestedWordIndex = 0;
698
0
  return NS_OK;
699
0
}
700
701
NS_IMETHODIMP
702
EditorSpellCheck::UpdateCurrentDictionary(
703
                    nsIEditorSpellCheckCallback* aCallback)
704
0
{
705
0
  if (NS_WARN_IF(!mSpellChecker)) {
706
0
    return NS_ERROR_NOT_INITIALIZED;
707
0
  }
708
0
709
0
  nsresult rv;
710
0
711
0
  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
712
0
713
0
  // Get language with html5 algorithm
714
0
  nsCOMPtr<nsIContent> rootContent;
715
0
  HTMLEditor* htmlEditor = mEditor->AsHTMLEditor();
716
0
  if (htmlEditor) {
717
0
    rootContent = htmlEditor->GetActiveEditingHost();
718
0
  } else {
719
0
    rootContent = mEditor->GetRoot();
720
0
  }
721
0
722
0
  // Try to get topmost document's document element for embedded mail editor.
723
0
  uint32_t flags = 0;
724
0
  mEditor->GetFlags(&flags);
725
0
  if (flags & nsIPlaintextEditor::eEditorMailMask) {
726
0
    nsCOMPtr<nsIDocument> ownerDoc = rootContent->OwnerDoc();
727
0
    NS_ENSURE_TRUE(ownerDoc, NS_ERROR_FAILURE);
728
0
    nsIDocument* parentDoc = ownerDoc->GetParentDocument();
729
0
    if (parentDoc) {
730
0
      rootContent = do_QueryInterface(parentDoc->GetDocumentElement());
731
0
    }
732
0
  }
733
0
734
0
  if (!rootContent) {
735
0
    return NS_ERROR_FAILURE;
736
0
  }
737
0
738
0
  RefPtr<DictionaryFetcher> fetcher =
739
0
    new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
740
0
  rootContent->GetLang(fetcher->mRootContentLang);
741
0
  nsCOMPtr<nsIDocument> doc = rootContent->GetComposedDoc();
742
0
  NS_ENSURE_STATE(doc);
743
0
  doc->GetContentLanguage(fetcher->mRootDocContentLang);
744
0
745
0
  rv = fetcher->Fetch(mEditor);
746
0
  NS_ENSURE_SUCCESS(rv, rv);
747
0
748
0
  return NS_OK;
749
0
}
750
751
// Helper function that iterates over the list of dictionaries and sets the one
752
// that matches based on a given comparison type.
753
void
754
EditorSpellCheck::BuildDictionaryList(const nsAString& aDictName,
755
                                      const nsTArray<nsString>& aDictList,
756
                                      enum dictCompare aCompareType,
757
                                      nsTArray<nsString>& aOutList)
758
0
{
759
0
  for (uint32_t i = 0; i < aDictList.Length(); i++) {
760
0
    nsAutoString dictStr(aDictList.ElementAt(i));
761
0
    bool equals = false;
762
0
    switch (aCompareType) {
763
0
      case DICT_NORMAL_COMPARE:
764
0
        equals = aDictName.Equals(dictStr);
765
0
        break;
766
0
      case DICT_COMPARE_CASE_INSENSITIVE:
767
0
        equals = aDictName.Equals(dictStr, nsCaseInsensitiveStringComparator());
768
0
        break;
769
0
      case DICT_COMPARE_DASHMATCH:
770
0
        equals = nsStyleUtil::DashMatchCompare(dictStr, aDictName, nsCaseInsensitiveStringComparator());
771
0
        break;
772
0
    }
773
0
    if (equals) {
774
0
      aOutList.AppendElement(dictStr);
775
#ifdef DEBUG_DICT
776
      if (NS_SUCCEEDED(rv)) {
777
        printf("***** Trying |%s|.\n", NS_ConvertUTF16toUTF8(dictStr).get());
778
      }
779
#endif
780
      // We always break here. We tried to set the dictionary to an existing
781
0
      // dictionary from the list. This must work, if it doesn't, there is
782
0
      // no point trying another one.
783
0
      return;
784
0
    }
785
0
  }
786
0
}
787
788
nsresult
789
EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher)
790
0
{
791
0
  MOZ_ASSERT(aFetcher);
792
0
  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;
793
0
794
0
  BeginUpdateDictionary();
795
0
796
0
  if (aFetcher->mGroup < mDictionaryFetcherGroup) {
797
0
    // SetCurrentDictionary was called after the fetch started.  Don't overwrite
798
0
    // that dictionary with the fetched one.
799
0
    EndUpdateDictionary();
800
0
    if (aFetcher->mCallback) {
801
0
      aFetcher->mCallback->EditorSpellCheckDone();
802
0
    }
803
0
    return NS_OK;
804
0
  }
805
0
806
0
  /*
807
0
   * We try to derive the dictionary to use based on the following priorities:
808
0
   * 1) Content preference, so the language the user set for the site before.
809
0
   *    (Introduced in bug 678842 and corrected in bug 717433.)
810
0
   * 2) Language set by the website, or any other dictionary that partly
811
0
   *    matches that. (Introduced in bug 338427.)
812
0
   *    Eg. if the website is "en-GB", a user who only has "en-US" will get
813
0
   *    that. If the website is generic "en", the user will get one of the
814
0
   *    "en-*" installed, (almost) at random.
815
0
   *    However, we prefer what is stored in "spellchecker.dictionary",
816
0
   *    so if the user chose "en-AU" before, they will get "en-AU" on a plain
817
0
   *    "en" site. (Introduced in bug 682564.)
818
0
   * 3) The value of "spellchecker.dictionary" which reflects a previous
819
0
   *    language choice of the user (on another site).
820
0
   *    (This was the original behaviour before the aforementioned bugs
821
0
   *    landed).
822
0
   * 4) The user's locale.
823
0
   * 5) Use the current dictionary that is currently set.
824
0
   * 6) The content of the "LANG" environment variable (if set).
825
0
   * 7) The first spell check dictionary installed.
826
0
   */
827
0
828
0
  // Get the language from the element or its closest parent according to:
829
0
  // https://html.spec.whatwg.org/#attr-lang
830
0
  // This is used in SetCurrentDictionary.
831
0
  mPreferredLang.Assign(aFetcher->mRootContentLang);
832
#ifdef DEBUG_DICT
833
  printf("***** mPreferredLang (element) |%s|\n",
834
         NS_ConvertUTF16toUTF8(mPreferredLang).get());
835
#endif
836
837
0
  // If no luck, try the "Content-Language" header.
838
0
  if (mPreferredLang.IsEmpty()) {
839
0
    mPreferredLang.Assign(aFetcher->mRootDocContentLang);
840
#ifdef DEBUG_DICT
841
    printf("***** mPreferredLang (content-language) |%s|\n",
842
           NS_ConvertUTF16toUTF8(mPreferredLang).get());
843
#endif
844
  }
845
0
846
0
  // We obtain a list of available dictionaries.
847
0
  AutoTArray<nsString, 8> dictList;
848
0
  nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
849
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
850
0
    EndUpdateDictionary();
851
0
    if (aFetcher->mCallback) {
852
0
      aFetcher->mCallback->EditorSpellCheckDone();
853
0
    }
854
0
    return rv;
855
0
  }
856
0
857
0
  // Priority 1:
858
0
  // If we successfully fetched a dictionary from content prefs, do not go
859
0
  // further. Use this exact dictionary.
860
0
  // Don't use content preferences for editor with eEditorMailMask flag.
861
0
  nsAutoString dictName;
862
0
  uint32_t flags;
863
0
  mEditor->GetFlags(&flags);
864
0
  if (!(flags & nsIPlaintextEditor::eEditorMailMask)) {
865
0
    dictName.Assign(aFetcher->mDictionary);
866
0
    if (!dictName.IsEmpty()) {
867
0
      AutoTArray<nsString, 1> tryDictList;
868
0
      BuildDictionaryList(dictName, dictList, DICT_NORMAL_COMPARE, tryDictList);
869
0
870
0
      RefPtr<EditorSpellCheck> self = this;
871
0
      RefPtr<DictionaryFetcher> fetcher = aFetcher;
872
0
      mSpellChecker->SetCurrentDictionaryFromList(tryDictList)->Then(
873
0
        GetMainThreadSerialEventTarget(),
874
0
        __func__,
875
0
        [self, fetcher]() {
876
#ifdef DEBUG_DICT
877
          printf("***** Assigned from content preferences |%s|\n",
878
                 NS_ConvertUTF16toUTF8(dictName).get());
879
#endif
880
          // We take an early exit here, so let's not forget to clear the word
881
0
          // list.
882
0
          self->DeleteSuggestedWordList();
883
0
884
0
          self->EndUpdateDictionary();
885
0
          if (fetcher->mCallback) {
886
0
            fetcher->mCallback->EditorSpellCheckDone();
887
0
          }
888
0
        },
889
0
        [self, fetcher](nsresult aError) {
890
0
          if (aError == NS_ERROR_ABORT) {
891
0
            return;
892
0
          }
893
0
          // May be dictionary was uninstalled ?
894
0
          // Clear the content preference and continue.
895
0
          ClearCurrentDictionary(self->mEditor);
896
0
897
0
          // Priority 2 or later will handled by the following
898
0
          self->SetFallbackDictionary(fetcher);
899
0
        });
900
0
      return NS_OK;
901
0
    }
902
0
  }
903
0
  SetFallbackDictionary(aFetcher);
904
0
  return NS_OK;
905
0
}
906
907
void
908
EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher)
909
0
{
910
0
  MOZ_ASSERT(mUpdateDictionaryRunning);
911
0
912
0
  AutoTArray<nsString, 6> tryDictList;
913
0
914
0
  // We obtain a list of available dictionaries.
915
0
  AutoTArray<nsString, 8> dictList;
916
0
  nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
917
0
  if (NS_WARN_IF(NS_FAILED(rv))) {
918
0
    EndUpdateDictionary();
919
0
    if (aFetcher->mCallback) {
920
0
      aFetcher->mCallback->EditorSpellCheckDone();
921
0
    }
922
0
    return;
923
0
  }
924
0
925
0
  // Priority 2:
926
0
  // After checking the content preferences, we use the language of the element
927
0
  // or document.
928
0
  nsAutoString dictName(mPreferredLang);
929
#ifdef DEBUG_DICT
930
  printf("***** Assigned from element/doc |%s|\n",
931
         NS_ConvertUTF16toUTF8(dictName).get());
932
#endif
933
934
0
  // Get the preference value.
935
0
  nsAutoString preferredDict;
936
0
  Preferences::GetLocalizedString("spellchecker.dictionary", preferredDict);
937
0
938
0
  if (!dictName.IsEmpty()) {
939
0
    // RFC 5646 explicitly states that matches should be case-insensitive.
940
0
    BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
941
0
                        tryDictList);
942
0
943
#ifdef DEBUG_DICT
944
    printf("***** Trying from element/doc |%s| \n",
945
           NS_ConvertUTF16toUTF8(dictName).get());
946
#endif
947
948
0
    // Required dictionary was not available. Try to get a dictionary
949
0
    // matching at least language part of dictName.
950
0
    nsAutoString langCode;
951
0
    int32_t dashIdx = dictName.FindChar('-');
952
0
    if (dashIdx != -1) {
953
0
      langCode.Assign(Substring(dictName, 0, dashIdx));
954
0
    } else {
955
0
      langCode.Assign(dictName);
956
0
    }
957
0
958
0
    // Try dictionary.spellchecker preference, if it starts with langCode,
959
0
    // so we don't just get any random dictionary matching the language.
960
0
    if (!preferredDict.IsEmpty() &&
961
0
        nsStyleUtil::DashMatchCompare(preferredDict, langCode, nsDefaultStringComparator())) {
962
#ifdef DEBUG_DICT
963
      printf("***** Trying preference value |%s| since it matches language code\n",
964
             NS_ConvertUTF16toUTF8(preferredDict).get());
965
#endif
966
      BuildDictionaryList(preferredDict, dictList,
967
0
                          DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
968
0
    }
969
0
970
0
    // Use any dictionary with the required language.
971
#ifdef DEBUG_DICT
972
    printf("***** Trying to find match for language code |%s|\n",
973
           NS_ConvertUTF16toUTF8(langCode).get());
974
#endif
975
    BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
976
0
                        tryDictList);
977
0
  }
978
0
979
0
  // Priority 3:
980
0
  // If the document didn't supply a dictionary or the setting failed,
981
0
  // try the user preference next.
982
0
  if (!preferredDict.IsEmpty()) {
983
#ifdef DEBUG_DICT
984
    printf("***** Trying preference value |%s|\n",
985
           NS_ConvertUTF16toUTF8(preferredDict).get());
986
#endif
987
    BuildDictionaryList(preferredDict, dictList, DICT_NORMAL_COMPARE,
988
0
                             tryDictList);
989
0
  }
990
0
991
0
  // Priority 4:
992
0
  // As next fallback, try the current locale.
993
0
  nsAutoCString utf8DictName;
994
0
  LocaleService::GetInstance()->GetAppLocaleAsLangTag(utf8DictName);
995
0
996
0
  CopyUTF8toUTF16(utf8DictName, dictName);
997
#ifdef DEBUG_DICT
998
  printf("***** Trying locale |%s|\n",
999
         NS_ConvertUTF16toUTF8(dictName).get());
1000
#endif
1001
  BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
1002
0
                      tryDictList);
1003
0
1004
0
  // Priority 5:
1005
0
  // If we have a current dictionary and we don't have no item in try list,
1006
0
  // don't try anything else.
1007
0
  nsAutoString currentDictionary;
1008
0
  GetCurrentDictionary(currentDictionary);
1009
0
  if (!currentDictionary.IsEmpty() && tryDictList.IsEmpty()) {
1010
#ifdef DEBUG_DICT
1011
    printf("***** Retrieved current dict |%s|\n",
1012
           NS_ConvertUTF16toUTF8(currentDictionary).get());
1013
#endif
1014
    EndUpdateDictionary();
1015
0
    if (aFetcher->mCallback) {
1016
0
      aFetcher->mCallback->EditorSpellCheckDone();
1017
0
    }
1018
0
    return;
1019
0
  }
1020
0
1021
0
  // Priority 6:
1022
0
  // Try to get current dictionary from environment variable LANG.
1023
0
  // LANG = language[_territory][.charset]
1024
0
  char* env_lang = getenv("LANG");
1025
0
  if (env_lang) {
1026
0
    nsString lang = NS_ConvertUTF8toUTF16(env_lang);
1027
0
    // Strip trailing charset, if there is any.
1028
0
    int32_t dot_pos = lang.FindChar('.');
1029
0
    if (dot_pos != -1) {
1030
0
      lang = Substring(lang, 0, dot_pos);
1031
0
    }
1032
0
1033
0
    int32_t underScore = lang.FindChar('_');
1034
0
    if (underScore != -1) {
1035
0
      lang.Replace(underScore, 1, '-');
1036
#ifdef DEBUG_DICT
1037
      printf("***** Trying LANG from environment |%s|\n",
1038
             NS_ConvertUTF16toUTF8(lang).get());
1039
#endif
1040
      BuildDictionaryList(lang, dictList, DICT_COMPARE_CASE_INSENSITIVE,
1041
0
                          tryDictList);
1042
0
    }
1043
0
  }
1044
0
1045
0
  // Priority 7:
1046
0
  // If it does not work, pick the first one.
1047
0
  if (!dictList.IsEmpty()) {
1048
0
    BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
1049
0
                        tryDictList);
1050
#ifdef DEBUG_DICT
1051
    printf("***** Trying first of list |%s|\n",
1052
           NS_ConvertUTF16toUTF8(dictList[0]).get());
1053
#endif
1054
  }
1055
0
1056
0
  RefPtr<EditorSpellCheck> self = this;
1057
0
  RefPtr<DictionaryFetcher> fetcher = aFetcher;
1058
0
  mSpellChecker->SetCurrentDictionaryFromList(tryDictList)->Then(
1059
0
    GetMainThreadSerialEventTarget(),
1060
0
    __func__,
1061
0
    [self, fetcher]() {
1062
0
      // If an error was thrown while setting the dictionary, just
1063
0
      // fail silently so that the spellchecker dialog is allowed to come
1064
0
      // up. The user can manually reset the language to their choice on
1065
0
      // the dialog if it is wrong.
1066
0
      self->DeleteSuggestedWordList();
1067
0
      self->EndUpdateDictionary();
1068
0
      if (fetcher->mCallback) {
1069
0
        fetcher->mCallback->EditorSpellCheckDone();
1070
0
      }
1071
0
    });
1072
0
}
1073
1074
} // namespace mozilla