Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/sync/glue/autofill_model_associator.h" | 5 #include "chrome/browser/sync/glue/autofill_model_associator.h" |
| 6 | 6 |
| 7 #include <vector> | 7 #include <vector> |
| 8 | 8 |
| 9 #include "base/task.h" | 9 #include "base/task.h" |
| 10 #include "base/time.h" | 10 #include "base/time.h" |
| 11 #include "base/string_number_conversions.h" | 11 #include "base/string_number_conversions.h" |
| 12 #include "base/utf_string_conversions.h" | 12 #include "base/utf_string_conversions.h" |
| 13 #include "chrome/browser/autofill/autofill_profile.h" | 13 #include "chrome/browser/autofill/autofill_profile.h" |
| 14 #include "chrome/browser/browser_thread.h" | 14 #include "chrome/browser/browser_thread.h" |
| 15 #include "chrome/browser/guid.h" | 15 #include "chrome/browser/guid.h" |
| 16 #include "chrome/browser/prefs/pref_service.h" | |
| 16 #include "chrome/browser/profiles/profile.h" | 17 #include "chrome/browser/profiles/profile.h" |
| 17 #include "chrome/browser/sync/engine/syncapi.h" | 18 #include "chrome/browser/sync/engine/syncapi.h" |
| 18 #include "chrome/browser/sync/glue/autofill_change_processor.h" | 19 #include "chrome/browser/sync/glue/autofill_change_processor.h" |
| 20 #include "chrome/browser/sync/glue/autofill_profile_model_associator.h" | |
| 21 #include "chrome/browser/sync/glue/do_optimistic_refresh_task.h" | |
| 19 #include "chrome/browser/sync/profile_sync_service.h" | 22 #include "chrome/browser/sync/profile_sync_service.h" |
| 20 #include "chrome/browser/sync/protocol/autofill_specifics.pb.h" | 23 #include "chrome/browser/sync/protocol/autofill_specifics.pb.h" |
| 21 #include "chrome/browser/webdata/web_database.h" | 24 #include "chrome/browser/webdata/web_database.h" |
| 25 #include "chrome/common/pref_names.h" | |
| 22 #include "net/base/escape.h" | 26 #include "net/base/escape.h" |
| 23 | 27 |
| 24 using base::TimeTicks; | 28 using base::TimeTicks; |
| 25 | 29 |
| 26 namespace browser_sync { | 30 namespace browser_sync { |
| 27 | 31 |
| 28 const char kAutofillTag[] = "google_chrome_autofill"; | 32 const char kAutofillTag[] = "google_chrome_autofill"; |
| 29 const char kAutofillEntryNamespaceTag[] = "autofill_entry|"; | 33 const char kAutofillEntryNamespaceTag[] = "autofill_entry|"; |
| 30 | 34 |
| 31 struct AutofillModelAssociator::DataBundle { | 35 struct AutofillModelAssociator::DataBundle { |
| 32 std::set<AutofillKey> current_entries; | 36 std::set<AutofillKey> current_entries; |
| 33 std::vector<AutofillEntry> new_entries; | 37 std::vector<AutofillEntry> new_entries; |
| 34 std::set<string16> current_profiles; | 38 std::set<string16> current_profiles; |
| 35 std::vector<AutoFillProfile*> updated_profiles; | 39 std::vector<AutoFillProfile*> updated_profiles; |
| 36 std::vector<AutoFillProfile*> new_profiles; // We own these pointers. | 40 std::vector<AutoFillProfile*> new_profiles; // We own these pointers. |
| 37 ~DataBundle() { STLDeleteElements(&new_profiles); } | 41 ~DataBundle() { STLDeleteElements(&new_profiles); } |
| 38 }; | 42 }; |
| 39 | 43 |
| 40 AutofillModelAssociator::DoOptimisticRefreshTask::DoOptimisticRefreshTask( | |
| 41 PersonalDataManager* pdm) : pdm_(pdm) {} | |
| 42 | |
| 43 AutofillModelAssociator::DoOptimisticRefreshTask::~DoOptimisticRefreshTask() {} | |
| 44 | |
| 45 void AutofillModelAssociator::DoOptimisticRefreshTask::Run() { | |
| 46 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 47 pdm_->Refresh(); | |
| 48 } | |
| 49 | |
| 50 AutofillModelAssociator::AutofillModelAssociator( | 44 AutofillModelAssociator::AutofillModelAssociator( |
| 51 ProfileSyncService* sync_service, | 45 ProfileSyncService* sync_service, |
| 52 WebDatabase* web_database, | 46 WebDatabase* web_database, |
| 53 PersonalDataManager* personal_data) | 47 PersonalDataManager* personal_data) |
| 54 : sync_service_(sync_service), | 48 : sync_service_(sync_service), |
| 55 web_database_(web_database), | 49 web_database_(web_database), |
| 56 personal_data_(personal_data), | 50 personal_data_(personal_data), |
| 57 autofill_node_id_(sync_api::kInvalidId), | 51 autofill_node_id_(sync_api::kInvalidId), |
| 58 abort_association_pending_(false) { | 52 abort_association_pending_(false), |
| 53 number_of_entries_created_(0) { | |
| 59 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 54 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 60 DCHECK(sync_service_); | 55 DCHECK(sync_service_); |
| 61 DCHECK(web_database_); | 56 DCHECK(web_database_); |
| 62 DCHECK(personal_data_); | 57 DCHECK(personal_data_); |
| 63 } | 58 } |
| 64 | 59 |
| 65 AutofillModelAssociator::~AutofillModelAssociator() { | 60 AutofillModelAssociator::~AutofillModelAssociator() { |
| 66 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 61 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 67 } | 62 } |
| 68 | 63 |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 107 } else { | 102 } else { |
| 108 sync_api::WriteNode node(write_trans); | 103 sync_api::WriteNode node(write_trans); |
| 109 if (!node.InitUniqueByCreation(syncable::AUTOFILL, | 104 if (!node.InitUniqueByCreation(syncable::AUTOFILL, |
| 110 autofill_root, tag)) { | 105 autofill_root, tag)) { |
| 111 LOG(ERROR) << "Failed to create autofill sync node."; | 106 LOG(ERROR) << "Failed to create autofill sync node."; |
| 112 return false; | 107 return false; |
| 113 } | 108 } |
| 114 node.SetTitle(UTF8ToWide(tag)); | 109 node.SetTitle(UTF8ToWide(tag)); |
| 115 AutofillChangeProcessor::WriteAutofillEntry(*ix, &node); | 110 AutofillChangeProcessor::WriteAutofillEntry(*ix, &node); |
| 116 Associate(&tag, node.GetId()); | 111 Associate(&tag, node.GetId()); |
| 112 number_of_entries_created_++; | |
| 117 } | 113 } |
| 118 | 114 |
| 119 current_entries->insert(ix->key()); | 115 current_entries->insert(ix->key()); |
| 120 } | 116 } |
| 121 return true; | 117 return true; |
| 122 } | 118 } |
| 123 | 119 |
| 124 bool AutofillModelAssociator::MakeNewAutofillProfileSyncNode( | |
| 125 sync_api::WriteTransaction* trans, const sync_api::BaseNode& autofill_root, | |
| 126 const std::string& tag, const AutoFillProfile& profile, int64* sync_id) { | |
| 127 sync_api::WriteNode node(trans); | |
| 128 if (!node.InitUniqueByCreation(syncable::AUTOFILL, autofill_root, tag)) { | |
| 129 LOG(ERROR) << "Failed to create autofill sync node."; | |
| 130 return false; | |
| 131 } | |
| 132 node.SetTitle(UTF8ToWide(tag)); | |
| 133 AutofillChangeProcessor::WriteAutofillProfile(profile, &node); | |
| 134 *sync_id = node.GetId(); | |
| 135 return true; | |
| 136 } | |
| 137 | |
| 138 | |
| 139 bool AutofillModelAssociator::LoadAutofillData( | 120 bool AutofillModelAssociator::LoadAutofillData( |
| 140 std::vector<AutofillEntry>* entries, | 121 std::vector<AutofillEntry>* entries, |
| 141 std::vector<AutoFillProfile*>* profiles) { | 122 std::vector<AutoFillProfile*>* profiles) { |
| 142 if (IsAbortPending()) | 123 if (IsAbortPending()) |
| 143 return false; | 124 return false; |
| 144 if (!web_database_->GetAllAutofillEntries(entries)) | 125 if (!web_database_->GetAllAutofillEntries(entries)) |
| 145 return false; | 126 return false; |
| 146 | 127 |
| 147 if (IsAbortPending()) | 128 if (IsAbortPending()) |
| 148 return false; | 129 return false; |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 198 // Since we're on the DB thread, we don't have to worry about updating | 179 // Since we're on the DB thread, we don't have to worry about updating |
| 199 // the autofill database after closing the write transaction, since | 180 // the autofill database after closing the write transaction, since |
| 200 // this is the only thread that writes to the database. We also don't have | 181 // this is the only thread that writes to the database. We also don't have |
| 201 // to worry about the sync model getting out of sync, because changes are | 182 // to worry about the sync model getting out of sync, because changes are |
| 202 // propogated to the ChangeProcessor on this thread. | 183 // propogated to the ChangeProcessor on this thread. |
| 203 if (!SaveChangesToWebData(bundle)) { | 184 if (!SaveChangesToWebData(bundle)) { |
| 204 LOG(ERROR) << "Failed to update autofill entries."; | 185 LOG(ERROR) << "Failed to update autofill entries."; |
| 205 return false; | 186 return false; |
| 206 } | 187 } |
| 207 | 188 |
| 189 if (sync_service_->backend()->GetAutofillMigrationState() != | |
| 190 syncable::MIGRATED) { | |
| 191 syncable::AutofillMigrationDebugInfo debug_info; | |
| 192 debug_info.autofill_entries_added_during_migration = | |
| 193 number_of_entries_created_; | |
| 194 sync_service_->backend()->SetAutofillMigrationDebugInfo( | |
| 195 syncable::AutofillMigrationDebugInfo::ENTRIES_ADDED, | |
| 196 debug_info); | |
| 197 } | |
| 198 | |
| 208 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 199 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 209 new DoOptimisticRefreshTask(personal_data_)); | 200 new DoOptimisticRefreshForAutofill(personal_data_)); |
| 210 return true; | 201 return true; |
| 211 } | 202 } |
| 212 | 203 |
| 213 bool AutofillModelAssociator::SaveChangesToWebData(const DataBundle& bundle) { | 204 bool AutofillModelAssociator::SaveChangesToWebData(const DataBundle& bundle) { |
| 214 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 205 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 215 | 206 |
| 216 if (IsAbortPending()) | 207 if (IsAbortPending()) |
| 217 return false; | 208 return false; |
| 218 | 209 |
| 219 if (bundle.new_entries.size() && | 210 if (bundle.new_entries.size() && |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 237 return true; | 228 return true; |
| 238 } | 229 } |
| 239 | 230 |
| 240 bool AutofillModelAssociator::TraverseAndAssociateAllSyncNodes( | 231 bool AutofillModelAssociator::TraverseAndAssociateAllSyncNodes( |
| 241 sync_api::WriteTransaction* write_trans, | 232 sync_api::WriteTransaction* write_trans, |
| 242 const sync_api::ReadNode& autofill_root, | 233 const sync_api::ReadNode& autofill_root, |
| 243 DataBundle* bundle, | 234 DataBundle* bundle, |
| 244 const std::vector<AutoFillProfile*>& all_profiles_from_db) { | 235 const std::vector<AutoFillProfile*>& all_profiles_from_db) { |
| 245 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 236 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 246 | 237 |
| 238 bool autofill_profile_not_migrated = HasNotMigratedYet(write_trans); | |
| 239 | |
| 240 if (MigrationLoggingEnabled() && | |
| 241 autofill_profile_not_migrated) { | |
| 242 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 243 << "Printing profiles from web db"; | |
| 244 | |
| 245 for (std::vector<AutoFillProfile*>::const_iterator ix = | |
| 246 all_profiles_from_db.begin(); ix != all_profiles_from_db.end(); ++ix) { | |
| 247 AutoFillProfile* p = *ix; | |
| 248 VLOG(1) << "[AUTOFILL MIGRATION] " | |
| 249 << UTF16ToUTF8(p->GetFieldText(AutoFillType(NAME_FIRST))) | |
|
tim (not reviewing)
2010/12/15 20:11:42
conversion shouldn't be necessary
lipalani
2010/12/15 21:28:15
Done.
| |
| 250 << UTF16ToUTF8(p->GetFieldText(AutoFillType(NAME_LAST))); | |
| 251 } | |
| 252 } | |
| 253 | |
| 254 if (MigrationLoggingEnabled() && autofill_profile_not_migrated) { | |
| 255 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 256 << "Iterating over sync db"; | |
| 257 } | |
| 258 | |
| 247 int64 sync_child_id = autofill_root.GetFirstChildId(); | 259 int64 sync_child_id = autofill_root.GetFirstChildId(); |
| 248 while (sync_child_id != sync_api::kInvalidId) { | 260 while (sync_child_id != sync_api::kInvalidId) { |
| 249 sync_api::ReadNode sync_child(write_trans); | 261 sync_api::ReadNode sync_child(write_trans); |
| 250 if (!sync_child.InitByIdLookup(sync_child_id)) { | 262 if (!sync_child.InitByIdLookup(sync_child_id)) { |
| 251 LOG(ERROR) << "Failed to fetch child node."; | 263 LOG(ERROR) << "Failed to fetch child node."; |
| 252 return false; | 264 return false; |
| 253 } | 265 } |
| 254 const sync_pb::AutofillSpecifics& autofill( | 266 const sync_pb::AutofillSpecifics& autofill( |
| 255 sync_child.GetAutofillSpecifics()); | 267 sync_child.GetAutofillSpecifics()); |
| 256 | 268 |
| 257 if (autofill.has_value()) { | 269 if (autofill.has_value()) { |
| 258 AddNativeEntryIfNeeded(autofill, bundle, sync_child); | 270 AddNativeEntryIfNeeded(autofill, bundle, sync_child); |
| 259 } else if (autofill.has_profile() && HasNotMigratedYet()) { | 271 } else if (autofill.has_profile()) { |
| 260 // Ignore autofill profiles if we are not upgrading. | 272 // Ignore autofill profiles if we are not upgrading. |
| 261 AddNativeProfileIfNeeded( | 273 if (autofill_profile_not_migrated) { |
| 262 autofill.profile(), | 274 if (MigrationLoggingEnabled()) { |
| 263 bundle, | 275 VLOG(1) << "[AUTOFILL MIGRATION] Looking for " |
| 264 sync_child, | 276 << autofill.profile().name_first() |
| 265 all_profiles_from_db); | 277 << autofill.profile().name_last(); |
| 278 } | |
| 279 AddNativeProfileIfNeeded( | |
| 280 autofill.profile(), | |
| 281 bundle, | |
| 282 sync_child, | |
| 283 all_profiles_from_db); | |
| 284 } | |
| 266 } else { | 285 } else { |
| 267 NOTREACHED() << "AutofillSpecifics has no autofill data!"; | 286 NOTREACHED() << "AutofillSpecifics has no autofill data!"; |
| 268 } | 287 } |
| 269 | 288 |
| 270 sync_child_id = sync_child.GetSuccessorId(); | 289 sync_child_id = sync_child.GetSuccessorId(); |
| 271 } | 290 } |
| 272 return true; | 291 return true; |
| 273 } | 292 } |
| 274 | 293 |
| 275 // Define the functor to be used as the predicate in find_if call. | 294 // Define the functor to be used as the predicate in find_if call. |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 325 } | 344 } |
| 326 | 345 |
| 327 void AutofillModelAssociator::AddNativeProfileIfNeeded( | 346 void AutofillModelAssociator::AddNativeProfileIfNeeded( |
| 328 const sync_pb::AutofillProfileSpecifics& profile, | 347 const sync_pb::AutofillProfileSpecifics& profile, |
| 329 DataBundle* bundle, | 348 DataBundle* bundle, |
| 330 const sync_api::ReadNode& node, | 349 const sync_api::ReadNode& node, |
| 331 const std::vector<AutoFillProfile*>& all_profiles_from_db) { | 350 const std::vector<AutoFillProfile*>& all_profiles_from_db) { |
| 332 | 351 |
| 333 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 352 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 334 | 353 |
| 335 scoped_ptr<AutoFillProfile> profile_in_web_db(FindCorrespondingNodeFromWebDB( | 354 AutoFillProfile* profile_in_web_db = FindCorrespondingNodeFromWebDB( |
| 336 profile, all_profiles_from_db)); | 355 profile, all_profiles_from_db); |
| 337 | 356 |
| 338 if (profile_in_web_db.get() != NULL) { | 357 if (profile_in_web_db != NULL) { |
| 358 if (MigrationLoggingEnabled()) { | |
| 359 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 360 << "Node found in web db. So associating"; | |
| 361 } | |
| 339 int64 sync_id = node.GetId(); | 362 int64 sync_id = node.GetId(); |
| 340 std::string guid = profile_in_web_db->guid(); | 363 std::string guid = profile_in_web_db->guid(); |
| 341 Associate(&guid, sync_id); | 364 Associate(&guid, sync_id); |
| 342 return; | 365 return; |
| 343 } else { // Create a new node. | 366 } else { // Create a new node. |
| 367 if (MigrationLoggingEnabled()) { | |
| 368 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 369 << "Node not found in web db so creating and associating"; | |
| 370 } | |
| 344 std::string guid = guid::GenerateGUID(); | 371 std::string guid = guid::GenerateGUID(); |
| 345 Associate(&guid, node.GetId()); | 372 Associate(&guid, node.GetId()); |
| 346 AutoFillProfile* p = new AutoFillProfile(guid); | 373 AutoFillProfile* p = new AutoFillProfile(guid); |
| 347 FillProfileWithServerData(p, profile); | 374 FillProfileWithServerData(p, profile); |
| 348 bundle->new_profiles.push_back(p); | 375 bundle->new_profiles.push_back(p); |
| 349 } | 376 } |
| 350 } | 377 } |
| 351 | 378 |
| 352 bool AutofillModelAssociator::DisassociateModels() { | 379 bool AutofillModelAssociator::DisassociateModels() { |
| 353 id_map_.clear(); | 380 id_map_.clear(); |
| (...skipping 148 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 502 diff = MergeField(p, ADDRESS_HOME_ZIP, s.address_home_zip()) || diff; | 529 diff = MergeField(p, ADDRESS_HOME_ZIP, s.address_home_zip()) || diff; |
| 503 diff = MergeField(p, EMAIL_ADDRESS, s.email_address()) || diff; | 530 diff = MergeField(p, EMAIL_ADDRESS, s.email_address()) || diff; |
| 504 diff = MergeField(p, COMPANY_NAME, s.company_name()) || diff; | 531 diff = MergeField(p, COMPANY_NAME, s.company_name()) || diff; |
| 505 diff = MergeField(p, PHONE_FAX_WHOLE_NUMBER, s.phone_fax_whole_number()) | 532 diff = MergeField(p, PHONE_FAX_WHOLE_NUMBER, s.phone_fax_whole_number()) |
| 506 || diff; | 533 || diff; |
| 507 diff = MergeField(p, PHONE_HOME_WHOLE_NUMBER, s.phone_home_whole_number()) | 534 diff = MergeField(p, PHONE_HOME_WHOLE_NUMBER, s.phone_home_whole_number()) |
| 508 || diff; | 535 || diff; |
| 509 return diff; | 536 return diff; |
| 510 } | 537 } |
| 511 | 538 |
| 512 bool AutofillModelAssociator::HasNotMigratedYet() { | 539 bool AutofillModelAssociator::HasNotMigratedYet( |
| 513 return true; | 540 const sync_api::BaseTransaction* trans) { |
| 541 | |
| 542 // Now read the current value from the directory. | |
| 543 syncable::AutofillMigrationState autofill_migration_state = | |
|
tim (not reviewing)
2010/12/15 20:11:42
nit - indent is off.
lipalani
2010/12/15 21:28:15
Done.
| |
| 544 sync_service()->backend()->GetAutofillMigrationState(); | |
| 545 | |
| 546 DCHECK_NE(autofill_migration_state, syncable::NOT_DETERMINED); | |
| 547 | |
| 548 if (autofill_migration_state== syncable::NOT_DETERMINED) { | |
| 549 VLOG(1) << "Autofill migration state is not determined inside " | |
| 550 << " model associator"; | |
| 551 } | |
| 552 | |
| 553 if (autofill_migration_state == syncable::NOT_MIGRATED) { | |
| 554 return true; | |
| 555 } | |
| 556 | |
| 557 if (autofill_migration_state == syncable::INSUFFICIENT_INFO_TO_DETERMINE) { | |
| 558 if (MigrationLoggingEnabled()) { | |
| 559 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 560 << "current autofill migration state is insufficient info to" | |
| 561 << "Determine"; | |
| 562 } | |
| 563 sync_api::ReadNode autofill_profile_root_node(trans); | |
| 564 if (!autofill_profile_root_node.InitByTagLookup( | |
| 565 browser_sync::kAutofillProfileTag) || | |
| 566 autofill_profile_root_node.GetFirstChildId()== | |
| 567 static_cast<int64>(0)) { | |
| 568 sync_service()->backend()->SetAutofillMigrationState( | |
| 569 syncable::NOT_MIGRATED); | |
| 570 | |
| 571 if (MigrationLoggingEnabled()) { | |
| 572 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 573 << "Current autofill migration state is NOT Migrated becausse" | |
|
tim (not reviewing)
2010/12/15 20:11:42
couple typos: 'because' 'autofill' and 'present'
lipalani
2010/12/15 21:28:15
Done.
| |
| 574 << "legacy autofil root node is preseent whereas new " | |
| 575 << "Autofill profile root node is not present"; | |
| 576 } | |
| 577 return true; | |
| 578 } | |
| 579 | |
| 580 sync_service()->backend()->SetAutofillMigrationState(syncable::MIGRATED); | |
| 581 | |
| 582 if (MigrationLoggingEnabled()) { | |
| 583 VLOG(1) << "[AUTOFILL MIGRATION]" | |
| 584 << "Current autofill migration state is migrated"; | |
| 585 } | |
| 586 } | |
| 587 | |
| 588 return false; | |
| 514 } | 589 } |
| 515 | 590 |
| 591 bool AutofillModelAssociator::MigrationLoggingEnabled() { | |
| 592 // [TODO] enable logging via a command line flag. | |
| 593 return false; | |
| 594 } | |
| 516 } // namespace browser_sync | 595 } // namespace browser_sync |
| 596 | |
| OLD | NEW |