/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozPersonalDictionary.h" #include "nsIUnicharInputStream.h" #include "nsReadableUtils.h" #include "nsIFile.h" #include "nsAppDirectoryServiceDefs.h" #include "nsIObserverService.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "nsIWeakReference.h" #include "nsCRT.h" #include "nsNetUtil.h" #include "nsNetCID.h" #include "nsIInputStream.h" #include "nsIOutputStream.h" #include "nsISafeOutputStream.h" #include "nsTArray.h" #include "nsStringEnumerator.h" #include "nsUnicharInputStream.h" #include "nsIRunnable.h" #include "nsThreadUtils.h" #include "nsProxyRelease.h" #include "prio.h" #include "mozilla/Move.h" #define MOZ_PERSONAL_DICT_NAME "persdict.dat" /** * This is the most braindead implementation of a personal dictionary possible. * There is not much complexity needed, though. It could be made much faster, * and probably should, but I don't see much need for more in terms of interface. * * Allowing personal words to be associated with only certain dictionaries maybe. * * TODO: * Implement the suggestion record. */ NS_IMPL_CYCLE_COLLECTING_ADDREF(mozPersonalDictionary) NS_IMPL_CYCLE_COLLECTING_RELEASE(mozPersonalDictionary) NS_INTERFACE_MAP_BEGIN(mozPersonalDictionary) NS_INTERFACE_MAP_ENTRY(mozIPersonalDictionary) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIPersonalDictionary) NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(mozPersonalDictionary) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION(mozPersonalDictionary, mEncoder) class mozPersonalDictionaryLoader final : public mozilla::Runnable { public: explicit mozPersonalDictionaryLoader(mozPersonalDictionary *dict) : mDict(dict) { } NS_IMETHOD Run() override { mDict->SyncLoad(); // Release the dictionary on the main thread NS_ReleaseOnMainThread(mDict.forget()); return NS_OK; } private: RefPtr<mozPersonalDictionary> mDict; }; class mozPersonalDictionarySave final : public mozilla::Runnable { public: explicit mozPersonalDictionarySave(mozPersonalDictionary *aDict, nsCOMPtr<nsIFile> aFile, nsTArray<nsString> &&aDictWords) : mDictWords(aDictWords), mFile(aFile), mDict(aDict) { } NS_IMETHOD Run() override { nsresult res; MOZ_ASSERT(!NS_IsMainThread()); { mozilla::MonitorAutoLock mon(mDict->mMonitorSave); nsCOMPtr<nsIOutputStream> outStream; NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStream), mFile, PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE, 0664); // Get a buffered output stream 4096 bytes big, to optimize writes. nsCOMPtr<nsIOutputStream> bufferedOutputStream; res = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream), outStream, 4096); if (NS_FAILED(res)) { return res; } uint32_t bytesWritten; nsAutoCString utf8Key; for (uint32_t i = 0; i < mDictWords.Length(); ++i) { CopyUTF16toUTF8(mDictWords[i], utf8Key); bufferedOutputStream->Write(utf8Key.get(), utf8Key.Length(), &bytesWritten); bufferedOutputStream->Write("\n", 1, &bytesWritten); } nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(bufferedOutputStream); NS_ASSERTION(safeStream, "expected a safe output stream!"); if (safeStream) { res = safeStream->Finish(); if (NS_FAILED(res)) { NS_WARNING("failed to save personal dictionary file! possible data loss"); } } // Save is done, reset the state variable and notify those who are waiting. mDict->mSavePending = false; mon.Notify(); // Leaving the block where 'mon' was declared will call the destructor // and unlock. } // Release the dictionary on the main thread. NS_ReleaseOnMainThread(mDict.forget()); return NS_OK; } private: nsTArray<nsString> mDictWords; nsCOMPtr<nsIFile> mFile; RefPtr<mozPersonalDictionary> mDict; }; mozPersonalDictionary::mozPersonalDictionary() : mIsLoaded(false), mSavePending(false), mMonitor("mozPersonalDictionary::mMonitor"), mMonitorSave("mozPersonalDictionary::mMonitorSave") { } mozPersonalDictionary::~mozPersonalDictionary() { } nsresult mozPersonalDictionary::Init() { nsCOMPtr<nsIObserverService> svc = do_GetService("@mozilla.org/observer-service;1"); NS_ENSURE_STATE(svc); // we want to reload the dictionary if the profile switches nsresult rv = svc->AddObserver(this, "profile-do-change", true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = svc->AddObserver(this, "profile-before-change", true); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Load(); return NS_OK; } void mozPersonalDictionary::WaitForLoad() { // If the dictionary is already loaded, we return straight away. if (mIsLoaded) { return; } // If the dictionary hasn't been loaded, we try to lock the same monitor // that the thread uses that does the load. This way the main thread will // be suspended until the monitor becomes available. mozilla::MonitorAutoLock mon(mMonitor); // The monitor has become available. This can have two reasons: // 1: The thread that does the load has finished. // 2: The thread that does the load hasn't even started. // In this case we need to wait. if (!mIsLoaded) { mon.Wait(); } } nsresult mozPersonalDictionary::LoadInternal() { nsresult rv; mozilla::MonitorAutoLock mon(mMonitor); if (mIsLoaded) { return NS_OK; } rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!mFile) { return NS_ERROR_FAILURE; } rv = mFile->Append(NS_LITERAL_STRING(MOZ_PERSONAL_DICT_NAME)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIRunnable> runnable = new mozPersonalDictionaryLoader(this); rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } NS_IMETHODIMP mozPersonalDictionary::Load() { nsresult rv = LoadInternal(); if (NS_FAILED(rv)) { mIsLoaded = true; } return rv; } void mozPersonalDictionary::SyncLoad() { MOZ_ASSERT(!NS_IsMainThread()); mozilla::MonitorAutoLock mon(mMonitor); if (mIsLoaded) { return; } SyncLoadInternal(); mIsLoaded = true; mon.Notify(); } void mozPersonalDictionary::SyncLoadInternal() { MOZ_ASSERT(!NS_IsMainThread()); //FIXME Deinst -- get dictionary name from prefs; nsresult rv; bool dictExists; rv = mFile->Exists(&dictExists); if (NS_FAILED(rv)) { return; } if (!dictExists) { // Nothing is really wrong... return; } nsCOMPtr<nsIInputStream> inStream; NS_NewLocalFileInputStream(getter_AddRefs(inStream), mFile); nsCOMPtr<nsIUnicharInputStream> convStream; rv = NS_NewUnicharInputStream(inStream, getter_AddRefs(convStream)); if (NS_FAILED(rv)) { return; } // we're rereading to get rid of the old data -- we shouldn't have any, but... mDictionaryTable.Clear(); char16_t c; uint32_t nRead; bool done = false; do{ // read each line of text into the string array. if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) break; while(!done && ((c == '\n') || (c == '\r'))){ if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) done = true; } if (!done){ nsAutoString word; while((c != '\n') && (c != '\r') && !done){ word.Append(c); if( (NS_OK != convStream->Read(&c, 1, &nRead)) || (nRead != 1)) done = true; } mDictionaryTable.PutEntry(word.get()); } } while(!done); } void mozPersonalDictionary::WaitForSave() { // If no save is pending, we return straight away. if (!mSavePending) { return; } // If a save is pending, we try to lock the same monitor that the thread uses // that does the save. This way the main thread will be suspended until the // monitor becomes available. mozilla::MonitorAutoLock mon(mMonitorSave); // The monitor has become available. This can have two reasons: // 1: The thread that does the save has finished. // 2: The thread that does the save hasn't even started. // In this case we need to wait. if (mSavePending) { mon.Wait(); } } NS_IMETHODIMP mozPersonalDictionary::Save() { nsCOMPtr<nsIFile> theFile; nsresult res; WaitForSave(); mSavePending = true; //FIXME Deinst -- get dictionary name from prefs; res = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(theFile)); if(NS_FAILED(res)) return res; if(!theFile)return NS_ERROR_FAILURE; res = theFile->Append(NS_LITERAL_STRING(MOZ_PERSONAL_DICT_NAME)); if(NS_FAILED(res)) return res; nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &res); if (NS_WARN_IF(NS_FAILED(res))) { return res; } nsTArray<nsString> array; nsString* elems = array.AppendElements(mDictionaryTable.Count()); for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) { elems->Assign(iter.Get()->GetKey()); elems++; } nsCOMPtr<nsIRunnable> runnable = new mozPersonalDictionarySave(this, theFile, mozilla::Move(array)); res = target->Dispatch(runnable, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(res))) { return res; } return res; } NS_IMETHODIMP mozPersonalDictionary::GetWordList(nsIStringEnumerator **aWords) { NS_ENSURE_ARG_POINTER(aWords); *aWords = nullptr; WaitForLoad(); nsTArray<nsString> *array = new nsTArray<nsString>(); nsString* elems = array->AppendElements(mDictionaryTable.Count()); for (auto iter = mDictionaryTable.Iter(); !iter.Done(); iter.Next()) { elems->Assign(iter.Get()->GetKey()); elems++; } array->Sort(); return NS_NewAdoptingStringEnumerator(aWords, array); } NS_IMETHODIMP mozPersonalDictionary::Check(const char16_t *aWord, const char16_t *aLanguage, bool *aResult) { NS_ENSURE_ARG_POINTER(aWord); NS_ENSURE_ARG_POINTER(aResult); WaitForLoad(); *aResult = (mDictionaryTable.GetEntry(aWord) || mIgnoreTable.GetEntry(aWord)); return NS_OK; } NS_IMETHODIMP mozPersonalDictionary::AddWord(const char16_t *aWord, const char16_t *aLang) { nsresult res; WaitForLoad(); mDictionaryTable.PutEntry(aWord); res = Save(); return res; } NS_IMETHODIMP mozPersonalDictionary::RemoveWord(const char16_t *aWord, const char16_t *aLang) { nsresult res; WaitForLoad(); mDictionaryTable.RemoveEntry(aWord); res = Save(); return res; } NS_IMETHODIMP mozPersonalDictionary::IgnoreWord(const char16_t *aWord) { // avoid adding duplicate words to the ignore list if (aWord && !mIgnoreTable.GetEntry(aWord)) mIgnoreTable.PutEntry(aWord); return NS_OK; } NS_IMETHODIMP mozPersonalDictionary::EndSession() { WaitForLoad(); WaitForSave(); mIgnoreTable.Clear(); return NS_OK; } NS_IMETHODIMP mozPersonalDictionary::AddCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP mozPersonalDictionary::RemoveCorrection(const char16_t *word, const char16_t *correction, const char16_t *lang) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP mozPersonalDictionary::GetCorrection(const char16_t *word, char16_t ***words, uint32_t *count) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP mozPersonalDictionary::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { if (!nsCRT::strcmp(aTopic, "profile-do-change")) { // The observer is registered in Init() which calls Load and in turn // LoadInternal(); i.e. Observe() can't be called before Load(). WaitForLoad(); mIsLoaded = false; Load(); // load automatically clears out the existing dictionary table } else if (!nsCRT::strcmp(aTopic, "profile-before-change")) { WaitForSave(); } return NS_OK; }