1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/spellcheck_host_impl.h"
6
7#include <set>
8
9#include "base/file_util.h"
10#include "base/logging.h"
11#include "base/path_service.h"
12#include "base/string_split.h"
13#include "base/threading/thread_restrictions.h"
14#include "base/utf_string_conversions.h"
15#include "chrome/browser/spellcheck_host_observer.h"
16#include "chrome/browser/spellchecker_platform_engine.h"
17#include "chrome/common/chrome_constants.h"
18#include "chrome/common/chrome_paths.h"
19#include "chrome/common/spellcheck_common.h"
20#include "content/common/notification_service.h"
21#include "googleurl/src/gurl.h"
22#include "net/url_request/url_request_context_getter.h"
23#include "third_party/hunspell/google/bdict.h"
24#include "ui/base/l10n/l10n_util.h"
25#if defined(OS_MACOSX)
26#include "base/metrics/histogram.h"
27#endif
28
29namespace {
30
31FilePath GetFirstChoiceFilePath(const std::string& language) {
32  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
33
34  FilePath dict_dir;
35  PathService::Get(chrome::DIR_APP_DICTIONARIES, &dict_dir);
36  return SpellCheckCommon::GetVersionedFileName(language, dict_dir);
37}
38
39#if defined(OS_MACOSX)
40// Collect metrics on how often Hunspell is used on OS X vs the native
41// spellchecker.
42void RecordSpellCheckStats(bool native_spellchecker_used,
43                           const std::string& language) {
44  static std::set<std::string> languages_seen;
45
46  // Only count a language code once for each session..
47  if (languages_seen.find(language) != languages_seen.end()) {
48    return;
49  }
50  languages_seen.insert(language);
51
52  enum {
53    SPELLCHECK_OSX_NATIVE_SPELLCHECKER_USED = 0,
54    SPELLCHECK_HUNSPELL_USED = 1
55  };
56
57  bool engine_used = native_spellchecker_used ?
58                         SPELLCHECK_OSX_NATIVE_SPELLCHECKER_USED :
59                         SPELLCHECK_HUNSPELL_USED;
60
61  UMA_HISTOGRAM_COUNTS("SpellCheck.OSXEngineUsed", engine_used);
62}
63#endif
64
65#if defined(OS_WIN)
66FilePath GetFallbackFilePath(const FilePath& first_choice) {
67  FilePath dict_dir;
68  PathService::Get(chrome::DIR_USER_DATA, &dict_dir);
69  return dict_dir.Append(first_choice.BaseName());
70}
71#endif
72
73}  // namespace
74
75// Constructed on UI thread.
76SpellCheckHostImpl::SpellCheckHostImpl(
77    SpellCheckHostObserver* observer,
78    const std::string& language,
79    net::URLRequestContextGetter* request_context_getter)
80    : observer_(observer),
81      language_(language),
82      file_(base::kInvalidPlatformFileValue),
83      tried_to_download_(false),
84      use_platform_spellchecker_(false),
85      request_context_getter_(request_context_getter) {
86  DCHECK(observer_);
87  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
88
89  FilePath personal_file_directory;
90  PathService::Get(chrome::DIR_USER_DATA, &personal_file_directory);
91  custom_dictionary_file_ =
92      personal_file_directory.Append(chrome::kCustomDictionaryFileName);
93}
94
95SpellCheckHostImpl::~SpellCheckHostImpl() {
96  if (file_ != base::kInvalidPlatformFileValue)
97    base::ClosePlatformFile(file_);
98}
99
100void SpellCheckHostImpl::Initialize() {
101  if (SpellCheckerPlatform::SpellCheckerAvailable() &&
102      SpellCheckerPlatform::PlatformSupportsLanguage(language_)) {
103#if defined(OS_MACOSX)
104    RecordSpellCheckStats(true, language_);
105#endif
106    use_platform_spellchecker_ = true;
107    SpellCheckerPlatform::SetLanguage(language_);
108    MessageLoop::current()->PostTask(FROM_HERE,
109        NewRunnableMethod(this,
110            &SpellCheckHostImpl::InformObserverOfInitialization));
111    return;
112  }
113
114#if defined(OS_MACOSX)
115  RecordSpellCheckStats(false, language_);
116#endif
117
118  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
119      NewRunnableMethod(this,
120                        &SpellCheckHostImpl::InitializeDictionaryLocation));
121}
122
123void SpellCheckHostImpl::UnsetObserver() {
124  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
125
126  observer_ = NULL;
127  request_context_getter_ = NULL;
128  fetcher_.reset();
129}
130
131void SpellCheckHostImpl::AddWord(const std::string& word) {
132  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
133
134  custom_words_.push_back(word);
135  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
136      NewRunnableMethod(this,
137          &SpellCheckHostImpl::WriteWordToCustomDictionary, word));
138  NotificationService::current()->Notify(
139      NotificationType::SPELLCHECK_WORD_ADDED,
140      Source<SpellCheckHost>(this), NotificationService::NoDetails());
141}
142
143void SpellCheckHostImpl::InitializeDictionaryLocation() {
144  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
145
146  // Initialize the BDICT path. This initialization should be in the FILE thread
147  // because it checks if there is a "Dictionaries" directory and create it.
148  if (bdict_file_path_.empty())
149    bdict_file_path_ = GetFirstChoiceFilePath(language_);
150
151#if defined(OS_WIN)
152  // Check if the dictionary exists in the fallback location. If so, use it
153  // rather than downloading anew.
154  FilePath fallback = GetFallbackFilePath(bdict_file_path_);
155  if (!file_util::PathExists(bdict_file_path_) &&
156      file_util::PathExists(fallback)) {
157    bdict_file_path_ = fallback;
158  }
159#endif
160
161  InitializeInternal();
162}
163
164void SpellCheckHostImpl::InitializeInternal() {
165  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
166
167  if (!observer_)
168    return;
169
170  file_ = base::CreatePlatformFile(
171      bdict_file_path_,
172      base::PLATFORM_FILE_READ | base::PLATFORM_FILE_OPEN,
173      NULL, NULL);
174
175  // File didn't exist. Download it.
176  if (file_ == base::kInvalidPlatformFileValue && !tried_to_download_ &&
177      request_context_getter_) {
178    // We download from the ui thread because we need to know that
179    // |request_context_getter_| is still valid before initiating the download.
180    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
181        NewRunnableMethod(this, &SpellCheckHostImpl::DownloadDictionary));
182    return;
183  }
184
185  request_context_getter_ = NULL;
186
187  if (file_ != base::kInvalidPlatformFileValue) {
188    // Load custom dictionary.
189    std::string contents;
190    file_util::ReadFileToString(custom_dictionary_file_, &contents);
191    std::vector<std::string> list_of_words;
192    base::SplitString(contents, '\n', &list_of_words);
193    for (size_t i = 0; i < list_of_words.size(); ++i)
194      custom_words_.push_back(list_of_words[i]);
195  }
196
197  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
198      NewRunnableMethod(this,
199          &SpellCheckHostImpl::InformObserverOfInitialization));
200}
201
202void SpellCheckHostImpl::InitializeOnFileThread() {
203  DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::FILE));
204
205  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
206      NewRunnableMethod(this, &SpellCheckHostImpl::Initialize));
207}
208
209void SpellCheckHostImpl::InformObserverOfInitialization() {
210  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
211
212  if (observer_)
213    observer_->SpellCheckHostInitialized();
214}
215
216void SpellCheckHostImpl::DownloadDictionary() {
217  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
218
219  if (!request_context_getter_) {
220    InitializeOnFileThread();
221    return;
222  }
223
224  // Determine URL of file to download.
225  static const char kDownloadServerUrl[] =
226      "http://cache.pack.google.com/edgedl/chrome/dict/";
227  std::string bdict_file = bdict_file_path_.BaseName().MaybeAsASCII();
228  if (bdict_file.empty()) {
229    NOTREACHED();
230    return;
231  }
232  GURL url = GURL(std::string(kDownloadServerUrl) +
233                  StringToLowerASCII(bdict_file));
234  fetcher_.reset(new URLFetcher(url, URLFetcher::GET, this));
235  fetcher_->set_request_context(request_context_getter_);
236  tried_to_download_ = true;
237  fetcher_->Start();
238  request_context_getter_ = NULL;
239}
240
241void SpellCheckHostImpl::WriteWordToCustomDictionary(const std::string& word) {
242  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
243
244  // Stored in UTF-8.
245  std::string word_to_add(word + "\n");
246  FILE* f = file_util::OpenFile(custom_dictionary_file_, "a+");
247  if (f)
248    fputs(word_to_add.c_str(), f);
249  file_util::CloseFile(f);
250}
251
252void SpellCheckHostImpl::OnURLFetchComplete(const URLFetcher* source,
253                                            const GURL& url,
254                                            const net::URLRequestStatus& status,
255                                            int response_code,
256                                            const ResponseCookies& cookies,
257                                            const std::string& data) {
258  DCHECK(source);
259  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
260  fetcher_.reset();
261
262  if ((response_code / 100) != 2) {
263    // Initialize will not try to download the file a second time.
264    LOG(ERROR) << "Failure to download dictionary.";
265    InitializeOnFileThread();
266    return;
267  }
268
269  // Basic sanity check on the dictionary.
270  // There's the small chance that we might see a 200 status code for a body
271  // that represents some form of failure.
272  if (data.size() < 4 || data[0] != 'B' || data[1] != 'D' || data[2] != 'i' ||
273      data[3] != 'c') {
274    LOG(ERROR) << "Failure to download dictionary.";
275    InitializeOnFileThread();
276    return;
277  }
278
279  data_ = data;
280  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
281      NewRunnableMethod(this, &SpellCheckHostImpl::SaveDictionaryData));
282}
283
284void SpellCheckHostImpl::SaveDictionaryData() {
285  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
286
287  // To prevent corrupted dictionary data from causing a renderer crash, scan
288  // the dictionary data and verify it is sane before save it to a file.
289  if (!hunspell::BDict::Verify(data_.data(), data_.size())) {
290    LOG(ERROR) << "Failure to verify the downloaded dictionary.";
291    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
292        NewRunnableMethod(this,
293                          &SpellCheckHostImpl::InformObserverOfInitialization));
294    return;
295  }
296
297  size_t bytes_written =
298      file_util::WriteFile(bdict_file_path_, data_.data(), data_.length());
299  if (bytes_written != data_.length()) {
300    bool success = false;
301#if defined(OS_WIN)
302    bdict_file_path_ = GetFallbackFilePath(bdict_file_path_);
303    bytes_written =
304        file_util::WriteFile(GetFallbackFilePath(bdict_file_path_),
305                                                 data_.data(), data_.length());
306    if (bytes_written == data_.length())
307      success = true;
308#endif
309    data_.clear();
310
311    if (!success) {
312      LOG(ERROR) << "Failure to save dictionary.";
313      file_util::Delete(bdict_file_path_, false);
314      // To avoid trying to load a partially saved dictionary, shortcut the
315      // Initialize() call.
316      BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
317          NewRunnableMethod(this,
318              &SpellCheckHostImpl::InformObserverOfInitialization));
319      return;
320    }
321  }
322
323  data_.clear();
324  Initialize();
325}
326
327const base::PlatformFile& SpellCheckHostImpl::GetDictionaryFile() const {
328  return file_;
329}
330
331const std::vector<std::string>& SpellCheckHostImpl::GetCustomWords() const {
332  return custom_words_;
333}
334
335const std::string& SpellCheckHostImpl::GetLastAddedFile() const {
336  return custom_words_.back();
337}
338
339const std::string& SpellCheckHostImpl::GetLanguage() const {
340  return language_;
341}
342
343bool SpellCheckHostImpl::IsUsingPlatformChecker() const {
344  return use_platform_spellchecker_;
345}
346