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