1 // Copyright (c) 2012 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/spellchecker/spellcheck_custom_dictionary.h"
6
7 #include <functional>
8
9 #include "base/file_util.h"
10 #include "base/files/important_file_writer.h"
11 #include "base/md5.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_split.h"
14 #include "chrome/browser/spellchecker/spellcheck_host_metrics.h"
15 #include "chrome/common/chrome_constants.h"
16 #include "chrome/common/spellcheck_messages.h"
17 #include "content/public/browser/browser_thread.h"
18 #include "sync/api/sync_change.h"
19 #include "sync/api/sync_data.h"
20 #include "sync/api/sync_error_factory.h"
21 #include "sync/protocol/sync.pb.h"
22
23 using content::BrowserThread;
24 using chrome::spellcheck_common::WordList;
25 using chrome::spellcheck_common::WordSet;
26
27 namespace {
28
29 // Filename extension for backup dictionary file.
30 const base::FilePath::CharType BACKUP_EXTENSION[] = FILE_PATH_LITERAL("backup");
31
32 // Prefix for the checksum in the dictionary file.
33 const char CHECKSUM_PREFIX[] = "checksum_v1 = ";
34
35 // The status of the checksum in a custom spellcheck dictionary.
36 enum ChecksumStatus {
37 VALID_CHECKSUM,
38 INVALID_CHECKSUM,
39 };
40
41 // The result of a dictionary sanitation. Can be used as a bitmap.
42 enum ChangeSanitationResult {
43 // The change is valid and can be applied as-is.
44 VALID_CHANGE = 0,
45
46 // The change contained words to be added that are not valid.
47 DETECTED_INVALID_WORDS = 1,
48
49 // The change contained words to be added that are already in the dictionary.
50 DETECTED_DUPLICATE_WORDS = 2,
51
52 // The change contained words to be removed that are not in the dictionary.
53 DETECTED_MISSING_WORDS = 4,
54 };
55
56 // Loads the file at |file_path| into the |words| container. If the file has a
57 // valid checksum, then returns ChecksumStatus::VALID. If the file has an
58 // invalid checksum, then returns ChecksumStatus::INVALID and clears |words|.
LoadFile(const base::FilePath & file_path,WordList & words)59 ChecksumStatus LoadFile(const base::FilePath& file_path, WordList& words) {
60 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
61 words.clear();
62 std::string contents;
63 base::ReadFileToString(file_path, &contents);
64 size_t pos = contents.rfind(CHECKSUM_PREFIX);
65 if (pos != std::string::npos) {
66 std::string checksum = contents.substr(pos + strlen(CHECKSUM_PREFIX));
67 contents = contents.substr(0, pos);
68 if (checksum != base::MD5String(contents))
69 return INVALID_CHECKSUM;
70 }
71 base::TrimWhitespaceASCII(contents, base::TRIM_ALL, &contents);
72 base::SplitString(contents, '\n', &words);
73 return VALID_CHECKSUM;
74 }
75
76 // Returns true for invalid words and false for valid words.
IsInvalidWord(const std::string & word)77 bool IsInvalidWord(const std::string& word) {
78 std::string tmp;
79 return !base::IsStringUTF8(word) ||
80 word.length() >
81 chrome::spellcheck_common::MAX_CUSTOM_DICTIONARY_WORD_BYTES ||
82 word.empty() ||
83 base::TRIM_NONE != base::TrimWhitespaceASCII(word, base::TRIM_ALL, &tmp);
84 }
85
86 // Loads the custom spellcheck dictionary from |path| into |custom_words|. If
87 // the dictionary checksum is not valid, but backup checksum is valid, then
88 // restores the backup and loads that into |custom_words| instead. If the backup
89 // is invalid too, then clears |custom_words|. Must be called on the file
90 // thread.
LoadDictionaryFileReliably(WordList & custom_words,const base::FilePath & path)91 void LoadDictionaryFileReliably(WordList& custom_words,
92 const base::FilePath& path) {
93 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
94 // Load the contents and verify the checksum.
95 if (LoadFile(path, custom_words) == VALID_CHECKSUM)
96 return;
97 // Checksum is not valid. See if there's a backup.
98 base::FilePath backup = path.AddExtension(BACKUP_EXTENSION);
99 if (!base::PathExists(backup))
100 return;
101 // Load the backup and verify its checksum.
102 if (LoadFile(backup, custom_words) != VALID_CHECKSUM)
103 return;
104 // Backup checksum is valid. Restore the backup.
105 base::CopyFile(backup, path);
106 }
107
108 // Backs up the original dictionary, saves |custom_words| and its checksum into
109 // the custom spellcheck dictionary at |path|.
SaveDictionaryFileReliably(const WordList & custom_words,const base::FilePath & path)110 void SaveDictionaryFileReliably(
111 const WordList& custom_words,
112 const base::FilePath& path) {
113 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
114 std::stringstream content;
115 for (WordList::const_iterator it = custom_words.begin();
116 it != custom_words.end();
117 ++it) {
118 content << *it << '\n';
119 }
120 std::string checksum = base::MD5String(content.str());
121 content << CHECKSUM_PREFIX << checksum;
122 base::CopyFile(path, path.AddExtension(BACKUP_EXTENSION));
123 base::ImportantFileWriter::WriteFileAtomically(path, content.str());
124 }
125
126 // Removes duplicate and invalid words from |to_add| word list and sorts it.
127 // Looks for duplicates in both |to_add| and |existing| word lists. Returns a
128 // bitmap of |ChangeSanitationResult| values.
SanitizeWordsToAdd(const WordSet & existing,WordList & to_add)129 int SanitizeWordsToAdd(const WordSet& existing, WordList& to_add) {
130 // Do not add duplicate words.
131 std::sort(to_add.begin(), to_add.end());
132 WordList new_words = base::STLSetDifference<WordList>(to_add, existing);
133 new_words.erase(std::unique(new_words.begin(), new_words.end()),
134 new_words.end());
135 int result = VALID_CHANGE;
136 if (to_add.size() != new_words.size())
137 result |= DETECTED_DUPLICATE_WORDS;
138 // Do not add invalid words.
139 size_t size = new_words.size();
140 new_words.erase(std::remove_if(new_words.begin(),
141 new_words.end(),
142 IsInvalidWord),
143 new_words.end());
144 if (size != new_words.size())
145 result |= DETECTED_INVALID_WORDS;
146 // Save the sanitized words to be added.
147 std::swap(to_add, new_words);
148 return result;
149 }
150
151 // Removes word from |to_remove| that are missing from |existing| word list and
152 // sorts |to_remove|. Returns a bitmap of |ChangeSanitationResult| values.
SanitizeWordsToRemove(const WordSet & existing,WordList & to_remove)153 int SanitizeWordsToRemove(const WordSet& existing, WordList& to_remove) {
154 // Do not remove words that are missing from the dictionary.
155 std::sort(to_remove.begin(), to_remove.end());
156 WordList found_words;
157 std::set_intersection(existing.begin(),
158 existing.end(),
159 to_remove.begin(),
160 to_remove.end(),
161 std::back_inserter(found_words));
162 int result = VALID_CHANGE;
163 if (to_remove.size() > found_words.size())
164 result |= DETECTED_MISSING_WORDS;
165 // Save the sanitized words to be removed.
166 std::swap(to_remove, found_words);
167 return result;
168 }
169
170 } // namespace
171
172
Change()173 SpellcheckCustomDictionary::Change::Change() {
174 }
175
Change(const SpellcheckCustomDictionary::Change & other)176 SpellcheckCustomDictionary::Change::Change(
177 const SpellcheckCustomDictionary::Change& other)
178 : to_add_(other.to_add()),
179 to_remove_(other.to_remove()) {
180 }
181
Change(const WordList & to_add)182 SpellcheckCustomDictionary::Change::Change(const WordList& to_add)
183 : to_add_(to_add) {
184 }
185
~Change()186 SpellcheckCustomDictionary::Change::~Change() {
187 }
188
AddWord(const std::string & word)189 void SpellcheckCustomDictionary::Change::AddWord(const std::string& word) {
190 to_add_.push_back(word);
191 }
192
RemoveWord(const std::string & word)193 void SpellcheckCustomDictionary::Change::RemoveWord(const std::string& word) {
194 to_remove_.push_back(word);
195 }
196
Sanitize(const WordSet & words)197 int SpellcheckCustomDictionary::Change::Sanitize(const WordSet& words) {
198 int result = VALID_CHANGE;
199 if (!to_add_.empty())
200 result |= SanitizeWordsToAdd(words, to_add_);
201 if (!to_remove_.empty())
202 result |= SanitizeWordsToRemove(words, to_remove_);
203 return result;
204 }
205
to_add() const206 const WordList& SpellcheckCustomDictionary::Change::to_add() const {
207 return to_add_;
208 }
209
to_remove() const210 const WordList& SpellcheckCustomDictionary::Change::to_remove() const {
211 return to_remove_;
212 }
213
empty() const214 bool SpellcheckCustomDictionary::Change::empty() const {
215 return to_add_.empty() && to_remove_.empty();
216 }
217
SpellcheckCustomDictionary(const base::FilePath & path)218 SpellcheckCustomDictionary::SpellcheckCustomDictionary(
219 const base::FilePath& path)
220 : custom_dictionary_path_(),
221 is_loaded_(false),
222 weak_ptr_factory_(this) {
223 custom_dictionary_path_ =
224 path.Append(chrome::kCustomDictionaryFileName);
225 }
226
~SpellcheckCustomDictionary()227 SpellcheckCustomDictionary::~SpellcheckCustomDictionary() {
228 }
229
GetWords() const230 const WordSet& SpellcheckCustomDictionary::GetWords() const {
231 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
232 return words_;
233 }
234
AddWord(const std::string & word)235 bool SpellcheckCustomDictionary::AddWord(const std::string& word) {
236 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
237 Change dictionary_change;
238 dictionary_change.AddWord(word);
239 int result = dictionary_change.Sanitize(GetWords());
240 Apply(dictionary_change);
241 Notify(dictionary_change);
242 Sync(dictionary_change);
243 Save(dictionary_change);
244 return result == VALID_CHANGE;
245 }
246
RemoveWord(const std::string & word)247 bool SpellcheckCustomDictionary::RemoveWord(const std::string& word) {
248 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
249 Change dictionary_change;
250 dictionary_change.RemoveWord(word);
251 int result = dictionary_change.Sanitize(GetWords());
252 Apply(dictionary_change);
253 Notify(dictionary_change);
254 Sync(dictionary_change);
255 Save(dictionary_change);
256 return result == VALID_CHANGE;
257 }
258
HasWord(const std::string & word) const259 bool SpellcheckCustomDictionary::HasWord(const std::string& word) const {
260 return !!words_.count(word);
261 }
262
AddObserver(Observer * observer)263 void SpellcheckCustomDictionary::AddObserver(Observer* observer) {
264 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
265 observers_.AddObserver(observer);
266 }
267
RemoveObserver(Observer * observer)268 void SpellcheckCustomDictionary::RemoveObserver(Observer* observer) {
269 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
270 observers_.RemoveObserver(observer);
271 }
272
IsLoaded()273 bool SpellcheckCustomDictionary::IsLoaded() {
274 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
275 return is_loaded_;
276 }
277
IsSyncing()278 bool SpellcheckCustomDictionary::IsSyncing() {
279 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
280 return !!sync_processor_.get();
281 }
282
Load()283 void SpellcheckCustomDictionary::Load() {
284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
285 BrowserThread::PostTaskAndReplyWithResult(
286 BrowserThread::FILE,
287 FROM_HERE,
288 base::Bind(&SpellcheckCustomDictionary::LoadDictionaryFile,
289 custom_dictionary_path_),
290 base::Bind(&SpellcheckCustomDictionary::OnLoaded,
291 weak_ptr_factory_.GetWeakPtr()));
292 }
293
MergeDataAndStartSyncing(syncer::ModelType type,const syncer::SyncDataList & initial_sync_data,scoped_ptr<syncer::SyncChangeProcessor> sync_processor,scoped_ptr<syncer::SyncErrorFactory> sync_error_handler)294 syncer::SyncMergeResult SpellcheckCustomDictionary::MergeDataAndStartSyncing(
295 syncer::ModelType type,
296 const syncer::SyncDataList& initial_sync_data,
297 scoped_ptr<syncer::SyncChangeProcessor> sync_processor,
298 scoped_ptr<syncer::SyncErrorFactory> sync_error_handler) {
299 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
300 DCHECK(!sync_processor_.get());
301 DCHECK(!sync_error_handler_.get());
302 DCHECK(sync_processor.get());
303 DCHECK(sync_error_handler.get());
304 DCHECK_EQ(syncer::DICTIONARY, type);
305 sync_processor_ = sync_processor.Pass();
306 sync_error_handler_ = sync_error_handler.Pass();
307
308 // Build a list of words to add locally.
309 WordList to_add_locally;
310 for (syncer::SyncDataList::const_iterator it = initial_sync_data.begin();
311 it != initial_sync_data.end();
312 ++it) {
313 DCHECK_EQ(syncer::DICTIONARY, it->GetDataType());
314 to_add_locally.push_back(it->GetSpecifics().dictionary().word());
315 }
316
317 // Add remote words locally.
318 Change to_change_locally(to_add_locally);
319 to_change_locally.Sanitize(GetWords());
320 Apply(to_change_locally);
321 Notify(to_change_locally);
322 Save(to_change_locally);
323
324 // Add as many as possible local words remotely.
325 std::sort(to_add_locally.begin(), to_add_locally.end());
326 WordList to_add_remotely = base::STLSetDifference<WordList>(words_,
327 to_add_locally);
328
329 // Send local changes to the sync server.
330 Change to_change_remotely(to_add_remotely);
331 syncer::SyncMergeResult result(type);
332 result.set_error(Sync(to_change_remotely));
333 return result;
334 }
335
StopSyncing(syncer::ModelType type)336 void SpellcheckCustomDictionary::StopSyncing(syncer::ModelType type) {
337 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
338 DCHECK_EQ(syncer::DICTIONARY, type);
339 sync_processor_.reset();
340 sync_error_handler_.reset();
341 }
342
GetAllSyncData(syncer::ModelType type) const343 syncer::SyncDataList SpellcheckCustomDictionary::GetAllSyncData(
344 syncer::ModelType type) const {
345 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
346 DCHECK_EQ(syncer::DICTIONARY, type);
347 syncer::SyncDataList data;
348 std::string word;
349 size_t i = 0;
350 for (WordSet::const_iterator it = words_.begin();
351 it != words_.end() &&
352 i < chrome::spellcheck_common::MAX_SYNCABLE_DICTIONARY_WORDS;
353 ++it, ++i) {
354 word = *it;
355 sync_pb::EntitySpecifics specifics;
356 specifics.mutable_dictionary()->set_word(word);
357 data.push_back(syncer::SyncData::CreateLocalData(word, word, specifics));
358 }
359 return data;
360 }
361
ProcessSyncChanges(const tracked_objects::Location & from_here,const syncer::SyncChangeList & change_list)362 syncer::SyncError SpellcheckCustomDictionary::ProcessSyncChanges(
363 const tracked_objects::Location& from_here,
364 const syncer::SyncChangeList& change_list) {
365 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
366 Change dictionary_change;
367 for (syncer::SyncChangeList::const_iterator it = change_list.begin();
368 it != change_list.end();
369 ++it) {
370 DCHECK(it->IsValid());
371 std::string word = it->sync_data().GetSpecifics().dictionary().word();
372 switch (it->change_type()) {
373 case syncer::SyncChange::ACTION_ADD:
374 dictionary_change.AddWord(word);
375 break;
376 case syncer::SyncChange::ACTION_DELETE:
377 dictionary_change.RemoveWord(word);
378 break;
379 default:
380 return sync_error_handler_->CreateAndUploadError(
381 FROM_HERE,
382 "Processing sync changes failed on change type " +
383 syncer::SyncChange::ChangeTypeToString(it->change_type()));
384 }
385 }
386
387 dictionary_change.Sanitize(GetWords());
388 Apply(dictionary_change);
389 Notify(dictionary_change);
390 Save(dictionary_change);
391
392 return syncer::SyncError();
393 }
394
395 // static
LoadDictionaryFile(const base::FilePath & path)396 WordList SpellcheckCustomDictionary::LoadDictionaryFile(
397 const base::FilePath& path) {
398 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
399 WordList words;
400 LoadDictionaryFileReliably(words, path);
401 if (!words.empty() && VALID_CHANGE != SanitizeWordsToAdd(WordSet(), words))
402 SaveDictionaryFileReliably(words, path);
403 SpellCheckHostMetrics::RecordCustomWordCountStats(words.size());
404 return words;
405 }
406
407 // static
UpdateDictionaryFile(const SpellcheckCustomDictionary::Change & dictionary_change,const base::FilePath & path)408 void SpellcheckCustomDictionary::UpdateDictionaryFile(
409 const SpellcheckCustomDictionary::Change& dictionary_change,
410 const base::FilePath& path) {
411 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
412 if (dictionary_change.empty())
413 return;
414
415 WordList custom_words;
416 LoadDictionaryFileReliably(custom_words, path);
417
418 // Add words.
419 custom_words.insert(custom_words.end(),
420 dictionary_change.to_add().begin(),
421 dictionary_change.to_add().end());
422
423 // Remove words.
424 std::sort(custom_words.begin(), custom_words.end());
425 WordList remaining =
426 base::STLSetDifference<WordList>(custom_words,
427 dictionary_change.to_remove());
428 std::swap(custom_words, remaining);
429
430 SaveDictionaryFileReliably(custom_words, path);
431 }
432
OnLoaded(WordList custom_words)433 void SpellcheckCustomDictionary::OnLoaded(WordList custom_words) {
434 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
435 Change dictionary_change(custom_words);
436 dictionary_change.Sanitize(GetWords());
437 Apply(dictionary_change);
438 Sync(dictionary_change);
439 is_loaded_ = true;
440 FOR_EACH_OBSERVER(Observer, observers_, OnCustomDictionaryLoaded());
441 }
442
Apply(const SpellcheckCustomDictionary::Change & dictionary_change)443 void SpellcheckCustomDictionary::Apply(
444 const SpellcheckCustomDictionary::Change& dictionary_change) {
445 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
446 if (!dictionary_change.to_add().empty()) {
447 words_.insert(dictionary_change.to_add().begin(),
448 dictionary_change.to_add().end());
449 }
450 if (!dictionary_change.to_remove().empty()) {
451 WordSet updated_words =
452 base::STLSetDifference<WordSet>(words_,
453 dictionary_change.to_remove());
454 std::swap(words_, updated_words);
455 }
456 }
457
Save(const SpellcheckCustomDictionary::Change & dictionary_change)458 void SpellcheckCustomDictionary::Save(
459 const SpellcheckCustomDictionary::Change& dictionary_change) {
460 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
461 BrowserThread::PostTask(
462 BrowserThread::FILE,
463 FROM_HERE,
464 base::Bind(&SpellcheckCustomDictionary::UpdateDictionaryFile,
465 dictionary_change,
466 custom_dictionary_path_));
467 }
468
Sync(const SpellcheckCustomDictionary::Change & dictionary_change)469 syncer::SyncError SpellcheckCustomDictionary::Sync(
470 const SpellcheckCustomDictionary::Change& dictionary_change) {
471 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
472 syncer::SyncError error;
473 if (!IsSyncing() || dictionary_change.empty())
474 return error;
475
476 // The number of words on the sync server should not exceed the limits.
477 int server_size = static_cast<int>(words_.size()) -
478 static_cast<int>(dictionary_change.to_add().size());
479 int max_upload_size = std::max(
480 0,
481 static_cast<int>(
482 chrome::spellcheck_common::MAX_SYNCABLE_DICTIONARY_WORDS) -
483 server_size);
484 int upload_size = std::min(
485 static_cast<int>(dictionary_change.to_add().size()),
486 max_upload_size);
487
488 syncer::SyncChangeList sync_change_list;
489 int i = 0;
490
491 for (WordList::const_iterator it = dictionary_change.to_add().begin();
492 it != dictionary_change.to_add().end() && i < upload_size;
493 ++it, ++i) {
494 std::string word = *it;
495 sync_pb::EntitySpecifics specifics;
496 specifics.mutable_dictionary()->set_word(word);
497 sync_change_list.push_back(syncer::SyncChange(
498 FROM_HERE,
499 syncer::SyncChange::ACTION_ADD,
500 syncer::SyncData::CreateLocalData(word, word, specifics)));
501 }
502
503 for (WordList::const_iterator it = dictionary_change.to_remove().begin();
504 it != dictionary_change.to_remove().end();
505 ++it) {
506 std::string word = *it;
507 sync_pb::EntitySpecifics specifics;
508 specifics.mutable_dictionary()->set_word(word);
509 sync_change_list.push_back(syncer::SyncChange(
510 FROM_HERE,
511 syncer::SyncChange::ACTION_DELETE,
512 syncer::SyncData::CreateLocalData(word, word, specifics)));
513 }
514
515 // Send the changes to the sync processor.
516 error = sync_processor_->ProcessSyncChanges(FROM_HERE, sync_change_list);
517 if (error.IsSet())
518 return error;
519
520 // Turn off syncing of this dictionary if the server already has the maximum
521 // number of words.
522 if (words_.size() > chrome::spellcheck_common::MAX_SYNCABLE_DICTIONARY_WORDS)
523 StopSyncing(syncer::DICTIONARY);
524
525 return error;
526 }
527
Notify(const SpellcheckCustomDictionary::Change & dictionary_change)528 void SpellcheckCustomDictionary::Notify(
529 const SpellcheckCustomDictionary::Change& dictionary_change) {
530 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
531 if (!IsLoaded() || dictionary_change.empty())
532 return;
533 FOR_EACH_OBSERVER(Observer,
534 observers_,
535 OnCustomDictionaryChanged(dictionary_change));
536 }
537