| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 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/password_manager/native_backend_gnome_x.h" | 5 #include "chrome/browser/password_manager/native_backend_gnome_x.h" |
| 6 | 6 |
| 7 #include <dlfcn.h> | 7 #include <dlfcn.h> |
| 8 #include <gnome-keyring.h> | 8 #include <gnome-keyring.h> |
| 9 | 9 |
| 10 #include <map> | 10 #include <map> |
| (...skipping 477 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 488 method->forms_.clear(); | 488 method->forms_.clear(); |
| 489 // |list| will be freed after this callback returns, so convert it now. | 489 // |list| will be freed after this callback returns, so convert it now. |
| 490 ConvertFormList( | 490 ConvertFormList( |
| 491 list, method->original_signon_realm_, method->helper_, &method->forms_); | 491 list, method->original_signon_realm_, method->helper_, &method->forms_); |
| 492 method->original_signon_realm_.clear(); | 492 method->original_signon_realm_.clear(); |
| 493 method->event_.Signal(); | 493 method->event_.Signal(); |
| 494 } | 494 } |
| 495 | 495 |
| 496 } // namespace | 496 } // namespace |
| 497 | 497 |
| 498 NativeBackendGnome::NativeBackendGnome(LocalProfileId id, PrefService* prefs) | 498 NativeBackendGnome::NativeBackendGnome(LocalProfileId id) |
| 499 : profile_id_(id), prefs_(prefs) { | 499 : profile_id_(id) { |
| 500 // TODO(mdm): after a few more releases, remove the code which is now dead due | 500 app_string_ = GetProfileSpecificAppString(); |
| 501 // to the true || here, and simplify this code. We don't do it yet to make it | |
| 502 // easier to revert if necessary. | |
| 503 if (true || PasswordStoreX::PasswordsUseLocalProfileId(prefs)) { | |
| 504 app_string_ = GetProfileSpecificAppString(); | |
| 505 // We already did the migration previously. Don't try again. | |
| 506 migrate_tried_ = true; | |
| 507 } else { | |
| 508 app_string_ = kGnomeKeyringAppString; | |
| 509 migrate_tried_ = false; | |
| 510 } | |
| 511 } | 501 } |
| 512 | 502 |
| 513 NativeBackendGnome::~NativeBackendGnome() { | 503 NativeBackendGnome::~NativeBackendGnome() { |
| 514 } | 504 } |
| 515 | 505 |
| 516 bool NativeBackendGnome::Init() { | 506 bool NativeBackendGnome::Init() { |
| 517 return LoadGnomeKeyring() && gnome_keyring_is_available(); | 507 return LoadGnomeKeyring() && gnome_keyring_is_available(); |
| 518 } | 508 } |
| 519 | 509 |
| 520 bool NativeBackendGnome::RawAddLogin(const PasswordForm& form) { | 510 bool NativeBackendGnome::RawAddLogin(const PasswordForm& form) { |
| 521 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 511 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 522 GKRMethod method; | 512 GKRMethod method; |
| 523 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 513 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 524 base::Bind(&GKRMethod::AddLogin, | 514 base::Bind(&GKRMethod::AddLogin, |
| 525 base::Unretained(&method), | 515 base::Unretained(&method), |
| 526 form, app_string_.c_str())); | 516 form, app_string_.c_str())); |
| 527 GnomeKeyringResult result = method.WaitResult(); | 517 GnomeKeyringResult result = method.WaitResult(); |
| 528 if (result != GNOME_KEYRING_RESULT_OK) { | 518 if (result != GNOME_KEYRING_RESULT_OK) { |
| 529 LOG(ERROR) << "Keyring save failed: " | 519 LOG(ERROR) << "Keyring save failed: " |
| 530 << gnome_keyring_result_to_message(result); | 520 << gnome_keyring_result_to_message(result); |
| 531 return false; | 521 return false; |
| 532 } | 522 } |
| 533 // Successful write. Try migration if necessary. | |
| 534 if (!migrate_tried_) | |
| 535 MigrateToProfileSpecificLogins(); | |
| 536 return true; | 523 return true; |
| 537 } | 524 } |
| 538 | 525 |
| 539 bool NativeBackendGnome::AddLogin(const PasswordForm& form) { | 526 bool NativeBackendGnome::AddLogin(const PasswordForm& form) { |
| 540 // Based on LoginDatabase::AddLogin(), we search for an existing match based | 527 // Based on LoginDatabase::AddLogin(), we search for an existing match based |
| 541 // on origin_url, username_element, username_value, password_element, submit | 528 // on origin_url, username_element, username_value, password_element, submit |
| 542 // element, and signon_realm first, remove that, and then add the new entry. | 529 // element, and signon_realm first, remove that, and then add the new entry. |
| 543 // We'd add the new one first, and then delete the original, but then the | 530 // We'd add the new one first, and then delete the original, but then the |
| 544 // delete might actually delete the newly-added entry! | 531 // delete might actually delete the newly-added entry! |
| 545 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 532 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 546 GKRMethod method; | 533 GKRMethod method; |
| 547 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 534 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 548 base::Bind(&GKRMethod::AddLoginSearch, | 535 base::Bind(&GKRMethod::AddLoginSearch, |
| 549 base::Unretained(&method), | 536 base::Unretained(&method), |
| 550 form, app_string_.c_str())); | 537 form, app_string_.c_str())); |
| 551 PasswordFormList forms; | 538 PasswordFormList forms; |
| 552 GnomeKeyringResult result = method.WaitResult(&forms); | 539 GnomeKeyringResult result = method.WaitResult(&forms); |
| 553 if (result != GNOME_KEYRING_RESULT_OK && | 540 if (result != GNOME_KEYRING_RESULT_OK && |
| 554 result != GNOME_KEYRING_RESULT_NO_MATCH) { | 541 result != GNOME_KEYRING_RESULT_NO_MATCH) { |
| 555 LOG(ERROR) << "Keyring find failed: " | 542 LOG(ERROR) << "Keyring find failed: " |
| 556 << gnome_keyring_result_to_message(result); | 543 << gnome_keyring_result_to_message(result); |
| 557 return false; | 544 return false; |
| 558 } | 545 } |
| 559 if (forms.size() > 0) { | 546 if (forms.size() > 0) { |
| 560 if (forms.size() > 1) { | 547 if (forms.size() > 1) { |
| 561 LOG(WARNING) << "Adding login when there are " << forms.size() | 548 LOG(WARNING) << "Adding login when there are " << forms.size() |
| 562 << " matching logins already! Will replace only the first."; | 549 << " matching logins already! Will replace only the first."; |
| 563 } | 550 } |
| 564 | 551 |
| 565 // We try migration before updating the existing logins, since otherwise | |
| 566 // we'd do it after making some but not all of the changes below. | |
| 567 if (forms.size() > 0 && !migrate_tried_) | |
| 568 MigrateToProfileSpecificLogins(); | |
| 569 | |
| 570 RemoveLogin(*forms[0]); | 552 RemoveLogin(*forms[0]); |
| 571 for (size_t i = 0; i < forms.size(); ++i) | 553 for (size_t i = 0; i < forms.size(); ++i) |
| 572 delete forms[i]; | 554 delete forms[i]; |
| 573 } | 555 } |
| 574 return RawAddLogin(form); | 556 return RawAddLogin(form); |
| 575 } | 557 } |
| 576 | 558 |
| 577 bool NativeBackendGnome::UpdateLogin(const PasswordForm& form) { | 559 bool NativeBackendGnome::UpdateLogin(const PasswordForm& form) { |
| 578 // Based on LoginDatabase::UpdateLogin(), we search for forms to update by | 560 // Based on LoginDatabase::UpdateLogin(), we search for forms to update by |
| 579 // origin_url, username_element, username_value, password_element, and | 561 // origin_url, username_element, username_value, password_element, and |
| 580 // signon_realm. We then compare the result to the updated form. If they | 562 // signon_realm. We then compare the result to the updated form. If they |
| 581 // differ in any of the mutable fields, then we remove the original, and | 563 // differ in any of the mutable fields, then we remove the original, and |
| 582 // then add the new entry. We'd add the new one first, and then delete the | 564 // then add the new entry. We'd add the new one first, and then delete the |
| 583 // original, but then the delete might actually delete the newly-added entry! | 565 // original, but then the delete might actually delete the newly-added entry! |
| 584 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 566 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 585 GKRMethod method; | 567 GKRMethod method; |
| 586 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 568 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 587 base::Bind(&GKRMethod::UpdateLoginSearch, | 569 base::Bind(&GKRMethod::UpdateLoginSearch, |
| 588 base::Unretained(&method), | 570 base::Unretained(&method), |
| 589 form, app_string_.c_str())); | 571 form, app_string_.c_str())); |
| 590 PasswordFormList forms; | 572 PasswordFormList forms; |
| 591 GnomeKeyringResult result = method.WaitResult(&forms); | 573 GnomeKeyringResult result = method.WaitResult(&forms); |
| 592 if (result != GNOME_KEYRING_RESULT_OK) { | 574 if (result != GNOME_KEYRING_RESULT_OK) { |
| 593 LOG(ERROR) << "Keyring find failed: " | 575 LOG(ERROR) << "Keyring find failed: " |
| 594 << gnome_keyring_result_to_message(result); | 576 << gnome_keyring_result_to_message(result); |
| 595 return false; | 577 return false; |
| 596 } | 578 } |
| 597 | 579 |
| 598 // We try migration before updating the existing logins, since otherwise | |
| 599 // we'd do it after making some but not all of the changes below. | |
| 600 if (forms.size() > 0 && !migrate_tried_) | |
| 601 MigrateToProfileSpecificLogins(); | |
| 602 | |
| 603 bool ok = true; | 580 bool ok = true; |
| 604 for (size_t i = 0; i < forms.size(); ++i) { | 581 for (size_t i = 0; i < forms.size(); ++i) { |
| 605 if (forms[i]->action != form.action || | 582 if (forms[i]->action != form.action || |
| 606 forms[i]->password_value != form.password_value || | 583 forms[i]->password_value != form.password_value || |
| 607 forms[i]->ssl_valid != form.ssl_valid || | 584 forms[i]->ssl_valid != form.ssl_valid || |
| 608 forms[i]->preferred != form.preferred || | 585 forms[i]->preferred != form.preferred || |
| 609 forms[i]->times_used != form.times_used) { | 586 forms[i]->times_used != form.times_used) { |
| 610 RemoveLogin(*forms[i]); | 587 RemoveLogin(*forms[i]); |
| 611 | 588 |
| 612 forms[i]->action = form.action; | 589 forms[i]->action = form.action; |
| (...skipping 17 matching lines...) Expand all Loading... |
| 630 base::Unretained(&method), | 607 base::Unretained(&method), |
| 631 form, app_string_.c_str())); | 608 form, app_string_.c_str())); |
| 632 GnomeKeyringResult result = method.WaitResult(); | 609 GnomeKeyringResult result = method.WaitResult(); |
| 633 if (result != GNOME_KEYRING_RESULT_OK) { | 610 if (result != GNOME_KEYRING_RESULT_OK) { |
| 634 // Warning, not error, because this can sometimes happen due to the user | 611 // Warning, not error, because this can sometimes happen due to the user |
| 635 // racing with the daemon to delete the password a second time. | 612 // racing with the daemon to delete the password a second time. |
| 636 LOG(WARNING) << "Keyring delete failed: " | 613 LOG(WARNING) << "Keyring delete failed: " |
| 637 << gnome_keyring_result_to_message(result); | 614 << gnome_keyring_result_to_message(result); |
| 638 return false; | 615 return false; |
| 639 } | 616 } |
| 640 // Successful write. Try migration if necessary. Note that presumably if we've | |
| 641 // been asked to delete a login, it's because we returned it previously; thus, | |
| 642 // this will probably never happen since we'd have already tried migration. | |
| 643 if (!migrate_tried_) | |
| 644 MigrateToProfileSpecificLogins(); | |
| 645 return true; | 617 return true; |
| 646 } | 618 } |
| 647 | 619 |
| 648 bool NativeBackendGnome::RemoveLoginsCreatedBetween( | 620 bool NativeBackendGnome::RemoveLoginsCreatedBetween( |
| 649 const base::Time& delete_begin, | 621 const base::Time& delete_begin, |
| 650 const base::Time& delete_end) { | 622 const base::Time& delete_end) { |
| 651 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 623 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 652 bool ok = true; | 624 bool ok = true; |
| 653 // We could walk the list and delete items as we find them, but it is much | 625 // We could walk the list and delete items as we find them, but it is much |
| 654 // easier to build the list and use RemoveLogin() to delete them. | 626 // easier to build the list and use RemoveLogin() to delete them. |
| 655 PasswordFormList forms; | 627 PasswordFormList forms; |
| 656 if (!GetAllLogins(&forms)) | 628 if (!GetAllLogins(&forms)) |
| 657 return false; | 629 return false; |
| 658 // No need to try migration here: GetAllLogins() does it. | |
| 659 | 630 |
| 660 for (size_t i = 0; i < forms.size(); ++i) { | 631 for (size_t i = 0; i < forms.size(); ++i) { |
| 661 if (delete_begin <= forms[i]->date_created && | 632 if (delete_begin <= forms[i]->date_created && |
| 662 (delete_end.is_null() || forms[i]->date_created < delete_end)) { | 633 (delete_end.is_null() || forms[i]->date_created < delete_end)) { |
| 663 if (!RemoveLogin(*forms[i])) | 634 if (!RemoveLogin(*forms[i])) |
| 664 ok = false; | 635 ok = false; |
| 665 } | 636 } |
| 666 delete forms[i]; | 637 delete forms[i]; |
| 667 } | 638 } |
| 668 return ok; | 639 return ok; |
| 669 } | 640 } |
| 670 | 641 |
| 671 bool NativeBackendGnome::GetLogins(const PasswordForm& form, | 642 bool NativeBackendGnome::GetLogins(const PasswordForm& form, |
| 672 PasswordFormList* forms) { | 643 PasswordFormList* forms) { |
| 673 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 644 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 674 GKRMethod method; | 645 GKRMethod method; |
| 675 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 646 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 676 base::Bind(&GKRMethod::GetLogins, | 647 base::Bind(&GKRMethod::GetLogins, |
| 677 base::Unretained(&method), | 648 base::Unretained(&method), |
| 678 form, app_string_.c_str())); | 649 form, app_string_.c_str())); |
| 679 GnomeKeyringResult result = method.WaitResult(forms); | 650 GnomeKeyringResult result = method.WaitResult(forms); |
| 680 if (result == GNOME_KEYRING_RESULT_NO_MATCH) | 651 if (result == GNOME_KEYRING_RESULT_NO_MATCH) |
| 681 return true; | 652 return true; |
| 682 if (result != GNOME_KEYRING_RESULT_OK) { | 653 if (result != GNOME_KEYRING_RESULT_OK) { |
| 683 LOG(ERROR) << "Keyring find failed: " | 654 LOG(ERROR) << "Keyring find failed: " |
| 684 << gnome_keyring_result_to_message(result); | 655 << gnome_keyring_result_to_message(result); |
| 685 return false; | 656 return false; |
| 686 } | 657 } |
| 687 // Successful read of actual data. Try migration if necessary. | |
| 688 if (!migrate_tried_) | |
| 689 MigrateToProfileSpecificLogins(); | |
| 690 return true; | 658 return true; |
| 691 } | 659 } |
| 692 | 660 |
| 693 bool NativeBackendGnome::GetLoginsCreatedBetween(const base::Time& get_begin, | 661 bool NativeBackendGnome::GetLoginsCreatedBetween(const base::Time& get_begin, |
| 694 const base::Time& get_end, | 662 const base::Time& get_end, |
| 695 PasswordFormList* forms) { | 663 PasswordFormList* forms) { |
| 696 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | 664 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| 697 // We could walk the list and add items as we find them, but it is much | 665 // We could walk the list and add items as we find them, but it is much |
| 698 // easier to build the list and then filter the results. | 666 // easier to build the list and then filter the results. |
| 699 PasswordFormList all_forms; | 667 PasswordFormList all_forms; |
| 700 if (!GetAllLogins(&all_forms)) | 668 if (!GetAllLogins(&all_forms)) |
| 701 return false; | 669 return false; |
| 702 // No need to try migration here: GetAllLogins() does it. | |
| 703 | 670 |
| 704 forms->reserve(forms->size() + all_forms.size()); | 671 forms->reserve(forms->size() + all_forms.size()); |
| 705 for (size_t i = 0; i < all_forms.size(); ++i) { | 672 for (size_t i = 0; i < all_forms.size(); ++i) { |
| 706 if (get_begin <= all_forms[i]->date_created && | 673 if (get_begin <= all_forms[i]->date_created && |
| 707 (get_end.is_null() || all_forms[i]->date_created < get_end)) { | 674 (get_end.is_null() || all_forms[i]->date_created < get_end)) { |
| 708 forms->push_back(all_forms[i]); | 675 forms->push_back(all_forms[i]); |
| 709 } else { | 676 } else { |
| 710 delete all_forms[i]; | 677 delete all_forms[i]; |
| 711 } | 678 } |
| 712 } | 679 } |
| (...skipping 21 matching lines...) Expand all Loading... |
| 734 base::Unretained(&method), | 701 base::Unretained(&method), |
| 735 blacklisted_by_user, app_string_.c_str())); | 702 blacklisted_by_user, app_string_.c_str())); |
| 736 GnomeKeyringResult result = method.WaitResult(forms); | 703 GnomeKeyringResult result = method.WaitResult(forms); |
| 737 if (result == GNOME_KEYRING_RESULT_NO_MATCH) | 704 if (result == GNOME_KEYRING_RESULT_NO_MATCH) |
| 738 return true; | 705 return true; |
| 739 if (result != GNOME_KEYRING_RESULT_OK) { | 706 if (result != GNOME_KEYRING_RESULT_OK) { |
| 740 LOG(ERROR) << "Keyring find failed: " | 707 LOG(ERROR) << "Keyring find failed: " |
| 741 << gnome_keyring_result_to_message(result); | 708 << gnome_keyring_result_to_message(result); |
| 742 return false; | 709 return false; |
| 743 } | 710 } |
| 744 // Successful read of actual data. Try migration if necessary. | |
| 745 if (!migrate_tried_) | |
| 746 MigrateToProfileSpecificLogins(); | |
| 747 return true; | 711 return true; |
| 748 } | 712 } |
| 749 | 713 |
| 750 bool NativeBackendGnome::GetAllLogins(PasswordFormList* forms) { | 714 bool NativeBackendGnome::GetAllLogins(PasswordFormList* forms) { |
| 751 GKRMethod method; | 715 GKRMethod method; |
| 752 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | 716 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| 753 base::Bind(&GKRMethod::GetAllLogins, | 717 base::Bind(&GKRMethod::GetAllLogins, |
| 754 base::Unretained(&method), | 718 base::Unretained(&method), |
| 755 app_string_.c_str())); | 719 app_string_.c_str())); |
| 756 GnomeKeyringResult result = method.WaitResult(forms); | 720 GnomeKeyringResult result = method.WaitResult(forms); |
| 757 if (result == GNOME_KEYRING_RESULT_NO_MATCH) | 721 if (result == GNOME_KEYRING_RESULT_NO_MATCH) |
| 758 return true; | 722 return true; |
| 759 if (result != GNOME_KEYRING_RESULT_OK) { | 723 if (result != GNOME_KEYRING_RESULT_OK) { |
| 760 LOG(ERROR) << "Keyring find failed: " | 724 LOG(ERROR) << "Keyring find failed: " |
| 761 << gnome_keyring_result_to_message(result); | 725 << gnome_keyring_result_to_message(result); |
| 762 return false; | 726 return false; |
| 763 } | 727 } |
| 764 // Successful read of actual data. Try migration if necessary. | |
| 765 if (!migrate_tried_) | |
| 766 MigrateToProfileSpecificLogins(); | |
| 767 return true; | 728 return true; |
| 768 } | 729 } |
| 769 | 730 |
| 770 std::string NativeBackendGnome::GetProfileSpecificAppString() const { | 731 std::string NativeBackendGnome::GetProfileSpecificAppString() const { |
| 771 // Originally, the application string was always just "chrome" and used only | 732 // Originally, the application string was always just "chrome" and used only |
| 772 // so that we had *something* to search for since GNOME Keyring won't search | 733 // so that we had *something* to search for since GNOME Keyring won't search |
| 773 // for nothing. Now we use it to distinguish passwords for different profiles. | 734 // for nothing. Now we use it to distinguish passwords for different profiles. |
| 774 return base::StringPrintf("%s-%d", kGnomeKeyringAppString, profile_id_); | 735 return base::StringPrintf("%s-%d", kGnomeKeyringAppString, profile_id_); |
| 775 } | 736 } |
| 776 | |
| 777 void NativeBackendGnome::MigrateToProfileSpecificLogins() { | |
| 778 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); | |
| 779 | |
| 780 DCHECK(!migrate_tried_); | |
| 781 DCHECK_EQ(app_string_, kGnomeKeyringAppString); | |
| 782 | |
| 783 // Record the fact that we've attempted migration already right away, so that | |
| 784 // we don't get recursive calls back to MigrateToProfileSpecificLogins(). | |
| 785 migrate_tried_ = true; | |
| 786 | |
| 787 // First get all the logins, using the old app string. | |
| 788 PasswordFormList forms; | |
| 789 if (!GetAllLogins(&forms)) | |
| 790 return; | |
| 791 | |
| 792 // Now switch to a profile-specific app string. | |
| 793 app_string_ = GetProfileSpecificAppString(); | |
| 794 | |
| 795 // Try to add all the logins with the new app string. | |
| 796 bool ok = true; | |
| 797 for (size_t i = 0; i < forms.size(); ++i) { | |
| 798 if (!RawAddLogin(*forms[i])) | |
| 799 ok = false; | |
| 800 delete forms[i]; | |
| 801 } | |
| 802 | |
| 803 if (ok) { | |
| 804 // All good! Keep the new app string and set a persistent pref. | |
| 805 // NOTE: We explicitly don't delete the old passwords yet. They are | |
| 806 // potentially shared with other profiles and other user data dirs! | |
| 807 // Each other profile must be able to migrate the shared data as well, | |
| 808 // so we must leave it alone. After a few releases, we'll add code to | |
| 809 // delete them, and eventually remove this migration code. | |
| 810 // TODO(mdm): follow through with the plan above. | |
| 811 PasswordStoreX::SetPasswordsUseLocalProfileId(prefs_); | |
| 812 } else { | |
| 813 // We failed to migrate for some reason. Use the old app string. | |
| 814 app_string_ = kGnomeKeyringAppString; | |
| 815 } | |
| 816 } | |
| OLD | NEW |