Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(791)

Side by Side Diff: components/autofill/content/renderer/password_autofill_agent.cc

Issue 1814193002: Better filling on suggestion of password fields in Password Manager. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Some fixes Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 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 "components/autofill/content/renderer/password_autofill_agent.h" 5 #include "components/autofill/content/renderer/password_autofill_agent.h"
6 6
7 #include <stddef.h> 7 #include <stddef.h>
8 #include <utility> 8 #include <utility>
9 9
10 #include "base/bind.h" 10 #include "base/bind.h"
(...skipping 752 matching lines...) Expand 10 before | Expand all | Expand 10 after
763 if (iter != password_to_username_.end()) { 763 if (iter != password_to_username_.end()) {
764 web_input_to_password_info_[iter->second].password_was_edited_last = true; 764 web_input_to_password_info_[iter->second].password_was_edited_last = true;
765 // Note that the suggested value of |mutable_element| was reset when its 765 // Note that the suggested value of |mutable_element| was reset when its
766 // value changed. 766 // value changed.
767 mutable_element.setAutofilled(false); 767 mutable_element.setAutofilled(false);
768 } 768 }
769 } 769 }
770 } 770 }
771 771
772 bool PasswordAutofillAgent::FillSuggestion( 772 bool PasswordAutofillAgent::FillSuggestion(
773 const blink::WebNode& node, 773 const blink::WebFormControlElement& control_element,
774 const blink::WebString& username, 774 const blink::WebString& username,
775 const blink::WebString& password) { 775 const blink::WebString& password) {
776 // The element in context of the suggestion popup. 776 // The element in context of the suggestion popup.
777 blink::WebInputElement filled_element; 777 const blink::WebInputElement* element = toWebInputElement(&control_element);
778 if (!element)
779 return false;
780
781 blink::WebInputElement username_element;
782 blink::WebInputElement password_element;
778 PasswordInfo* password_info; 783 PasswordInfo* password_info;
779 784
780 if (!FindLoginInfo(node, &filled_element, &password_info) || 785 if (!FindPasswordInfoForElement(*element, &username_element,
781 !IsElementAutocompletable(filled_element) || 786 &password_element, &password_info) ||
782 !IsElementAutocompletable(password_info->password_field)) { 787 (!username_element.isNull() &&
788 !IsElementAutocompletable(username_element)) ||
789 !IsElementAutocompletable(password_element)) {
783 return false; 790 return false;
784 } 791 }
785 792
786 password_info->password_was_edited_last = false; 793 password_info->password_was_edited_last = false;
787 // Note that in cases where filled_element is the password element, the value 794 if (element->isPasswordField()) {
788 // gets overwritten with the correct one below. 795 password_info->password_field_suggestion_was_accepted = true;
789 filled_element.setValue(username, true); 796 password_info->password_field = password_element;
790 filled_element.setAutofilled(true); 797 } else if (!username_element.isNull()) {
791 nonscript_modified_values_[filled_element] = username; 798 username_element.setValue(username, true);
799 username_element.setAutofilled(true);
800 nonscript_modified_values_[username_element] = username;
801 }
792 802
793 password_info->password_field.setValue(password, true); 803 password_element.setValue(password, true);
794 password_info->password_field.setAutofilled(true); 804 password_element.setAutofilled(true);
795 nonscript_modified_values_[password_info->password_field] = password; 805 nonscript_modified_values_[password_element] = password;
796 806
797 filled_element.setSelectionRange(filled_element.value().length(), 807 blink::WebInputElement mutable_filled_element = *element;
798 filled_element.value().length()); 808 mutable_filled_element.setSelectionRange(element->value().length(),
809 element->value().length());
799 810
800 return true; 811 return true;
801 } 812 }
802 813
803 bool PasswordAutofillAgent::PreviewSuggestion( 814 bool PasswordAutofillAgent::PreviewSuggestion(
804 const blink::WebNode& node, 815 const blink::WebFormControlElement& control_element,
805 const blink::WebString& username, 816 const blink::WebString& username,
806 const blink::WebString& password) { 817 const blink::WebString& password) {
818 // The element in context of the suggestion popup.
819 const blink::WebInputElement* element = toWebInputElement(&control_element);
820 if (!element)
821 return false;
822
807 blink::WebInputElement username_element; 823 blink::WebInputElement username_element;
824 blink::WebInputElement password_element;
808 PasswordInfo* password_info; 825 PasswordInfo* password_info;
809 826
810 if (!FindLoginInfo(node, &username_element, &password_info) || 827 if (!FindPasswordInfoForElement(*element, &username_element,
811 !IsElementAutocompletable(username_element) || 828 &password_element, &password_info) ||
812 !IsElementAutocompletable(password_info->password_field)) { 829 (!username_element.isNull() &&
830 !IsElementAutocompletable(username_element)) ||
831 !IsElementAutocompletable(password_element)) {
813 return false; 832 return false;
814 } 833 }
815 834
816 if (username_query_prefix_.empty()) 835 if (!element->isPasswordField() && !username_element.isNull()) {
817 username_query_prefix_ = username_element.value(); 836 if (username_query_prefix_.empty())
837 username_query_prefix_ = username_element.value();
818 838
819 was_username_autofilled_ = username_element.isAutofilled(); 839 was_username_autofilled_ = username_element.isAutofilled();
820 username_element.setSuggestedValue(username); 840 username_element.setSuggestedValue(username);
821 username_element.setAutofilled(true); 841 username_element.setAutofilled(true);
822 form_util::PreviewSuggestion(username_element.suggestedValue(), 842 form_util::PreviewSuggestion(username_element.suggestedValue(),
823 username_query_prefix_, &username_element); 843 username_query_prefix_, &username_element);
824 was_password_autofilled_ = password_info->password_field.isAutofilled(); 844 }
825 password_info->password_field.setSuggestedValue(password); 845 was_password_autofilled_ = password_element.isAutofilled();
826 password_info->password_field.setAutofilled(true); 846 password_element.setSuggestedValue(password);
847 password_element.setAutofilled(true);
827 848
828 return true; 849 return true;
829 } 850 }
830 851
831 bool PasswordAutofillAgent::DidClearAutofillSelection( 852 bool PasswordAutofillAgent::DidClearAutofillSelection(
832 const blink::WebNode& node) { 853 const blink::WebFormControlElement& control_element) {
833 blink::WebInputElement username_element; 854 const blink::WebInputElement* element = toWebInputElement(&control_element);
834 PasswordInfo* password_info; 855 if (!element)
835 if (!FindLoginInfo(node, &username_element, &password_info))
836 return false; 856 return false;
837 857
838 ClearPreview(&username_element, &password_info->password_field); 858 blink::WebInputElement username_element;
859 blink::WebInputElement password_element;
860 PasswordInfo* password_info;
861
862 if (!FindPasswordInfoForElement(*element, &username_element,
863 &password_element, &password_info))
864 return false;
865
866 ClearPreview(&username_element, &password_element);
839 return true; 867 return true;
840 } 868 }
841 869
842 bool PasswordAutofillAgent::FindPasswordInfoForElement( 870 bool PasswordAutofillAgent::FindPasswordInfoForElement(
843 const blink::WebInputElement& element, 871 const blink::WebInputElement& element,
844 const blink::WebInputElement** username_element, 872 blink::WebInputElement* username_element,
873 blink::WebInputElement* password_element,
845 PasswordInfo** password_info) { 874 PasswordInfo** password_info) {
846 DCHECK(username_element && password_info); 875 DCHECK(username_element && password_element && password_info);
876 username_element->reset();
877 password_element->reset();
847 if (!element.isPasswordField()) { 878 if (!element.isPasswordField()) {
848 *username_element = &element; 879 *username_element = element;
849 } else { 880 } else {
850 WebInputToPasswordInfoMap::iterator iter = 881 WebInputToPasswordInfoMap::iterator iter =
851 web_input_to_password_info_.find(element); 882 web_input_to_password_info_.find(element);
852 if (iter != web_input_to_password_info_.end()) { 883 if (iter != web_input_to_password_info_.end()) {
853 // It's a password field without corresponding username field. 884 // It's a password field without corresponding username field.
854 *username_element = nullptr; 885 *password_element = element;
855 *password_info = &iter->second; 886 *password_info = &iter->second;
856 return true; 887 return true;
857 } 888 }
858 PasswordToLoginMap::const_iterator password_iter = 889 PasswordToLoginMap::const_iterator password_iter =
859 password_to_username_.find(element); 890 password_to_username_.find(element);
860 if (password_iter == password_to_username_.end()) 891 if (password_iter == password_to_username_.end()) {
861 return false; 892 if (web_input_to_password_info_.empty())
862 *username_element = &password_iter->second; 893 return false;
894
895 *password_element = element;
896 *password_info = &web_input_to_password_info_.begin()->second;
vabr (Chromium) 2016/04/01 14:07:37 nit: Please comment on the choice of the value for
dvadym 2016/04/07 17:30:20 Done.
897 return true;
898 }
899 *username_element = password_iter->second;
900 *password_element = element;
863 } 901 }
864 902
865 WebInputToPasswordInfoMap::iterator iter = 903 WebInputToPasswordInfoMap::iterator iter =
866 web_input_to_password_info_.find(**username_element); 904 web_input_to_password_info_.find(*username_element);
867 905
868 if (iter == web_input_to_password_info_.end()) 906 if (iter == web_input_to_password_info_.end())
869 return false; 907 return false;
870 908
871 *password_info = &iter->second; 909 *password_info = &iter->second;
910 if (password_element->isNull())
911 *password_element = (*password_info)->password_field;
912
872 return true; 913 return true;
873 } 914 }
874 915
875 bool PasswordAutofillAgent::ShowSuggestions( 916 bool PasswordAutofillAgent::ShowSuggestions(
876 const blink::WebInputElement& element, 917 const blink::WebInputElement& element,
877 bool show_all, 918 bool show_all,
878 bool generation_popup_showing) { 919 bool generation_popup_showing) {
879 const blink::WebInputElement* username_element; 920 blink::WebInputElement username_element;
921 blink::WebInputElement password_element;
880 PasswordInfo* password_info; 922 PasswordInfo* password_info;
881 if (!FindPasswordInfoForElement(element, &username_element, &password_info)) 923 if (!FindPasswordInfoForElement(element, &username_element, &password_element,
924 &password_info))
882 return false; 925 return false;
883 926
884 // If autocomplete='off' is set on the form elements, no suggestion dialog 927 // If autocomplete='off' is set on the form elements, no suggestion dialog
885 // should be shown. However, return |true| to indicate that this is a known 928 // should be shown. However, return |true| to indicate that this is a known
886 // password form and that the request to show suggestions has been handled (as 929 // password form and that the request to show suggestions has been handled (as
887 // a no-op). 930 // a no-op).
888 if (!element.isTextField() || !IsElementAutocompletable(element) || 931 if (!element.isTextField() || !IsElementAutocompletable(element) ||
889 !IsElementAutocompletable(password_info->password_field)) 932 !IsElementAutocompletable(password_info->password_field))
890 return true; 933 return true;
891 934
892 if (element.nameForAutofill().isEmpty() && 935 if (element.nameForAutofill().isEmpty() &&
893 !DoesFormContainAmbiguousOrEmptyNames(password_info->fill_data)) { 936 !DoesFormContainAmbiguousOrEmptyNames(password_info->fill_data)) {
894 return false; // If the field has no name, then we won't have values. 937 return false; // If the field has no name, then we won't have values.
895 } 938 }
896 939
897 // Don't attempt to autofill with values that are too large. 940 // Don't attempt to autofill with values that are too large.
898 if (element.value().length() > kMaximumTextSizeForAutocomplete) 941 if (element.value().length() > kMaximumTextSizeForAutocomplete)
899 return false; 942 return false;
900 943
901 bool username_is_available = username_element && 944 bool username_is_available =
902 !username_element->isNull() && 945 !username_element.isNull() && IsElementEditable(username_element);
903 IsElementEditable(*username_element);
904 // If the element is a password field, a popup should only be shown if there 946 // If the element is a password field, a popup should only be shown if there
905 // is no username or the corresponding username element is not editable since 947 // is no username or the corresponding username element is not editable since
906 // it is only in that case that the username element does not have a 948 // it is only in that case that the username element does not have a
907 // suggestions popup. 949 // suggestions popup.
vabr (Chromium) 2016/04/01 14:07:37 Please update the comment to also speak about not
dvadym 2016/04/07 17:30:20 I've decided to simplify this condition, the only
vabr (Chromium) 2016/04/08 14:47:31 Acknowledged.
908 if (element.isPasswordField() && username_is_available && 950 if (element.isPasswordField() &&
909 (!password_info->fill_data.is_possible_change_password_form || 951 ((username_is_available &&
910 password_info->username_was_edited)) 952 (!password_info->fill_data.is_possible_change_password_form ||
953 password_info->username_was_edited)) ||
954 (password_info->password_field_suggestion_was_accepted &&
955 element != password_info->password_field)))
911 return true; 956 return true;
912 957
913 UMA_HISTOGRAM_BOOLEAN( 958 UMA_HISTOGRAM_BOOLEAN(
914 "PasswordManager.AutocompletePopupSuppressedByGeneration", 959 "PasswordManager.AutocompletePopupSuppressedByGeneration",
915 generation_popup_showing); 960 generation_popup_showing);
916 961
917 if (generation_popup_showing) 962 if (generation_popup_showing)
918 return false; 963 return false;
919 964
920 // Chrome should never show more than one account for a password element since 965 // Chrome should never show more than one account for a password element since
921 // this implies that the username element cannot be modified. Thus even if 966 // this implies that the username element cannot be modified. Thus even if
922 // |show_all| is true, check if the element in question is a password element 967 // |show_all| is true, check if the element in question is a password element
923 // for the call to ShowSuggestionPopup. 968 // for the call to ShowSuggestionPopup.
924 return ShowSuggestionPopup( 969 return ShowSuggestionPopup(
925 password_info->fill_data, 970 *password_info, username_element.isNull() ? element : username_element,
926 (!username_element || username_element->isNull()) ? element
927 : *username_element,
928 show_all && !element.isPasswordField(), element.isPasswordField()); 971 show_all && !element.isPasswordField(), element.isPasswordField());
929 } 972 }
930 973
931 bool PasswordAutofillAgent::OriginCanAccessPasswordManager( 974 bool PasswordAutofillAgent::OriginCanAccessPasswordManager(
932 const blink::WebSecurityOrigin& origin) { 975 const blink::WebSecurityOrigin& origin) {
933 return origin.canAccessPasswordManager(); 976 return origin.canAccessPasswordManager();
934 } 977 }
935 978
936 void PasswordAutofillAgent::OnDynamicFormsSeen() { 979 void PasswordAutofillAgent::OnDynamicFormsSeen() {
937 SendPasswordForms(false /* only_visible */); 980 SendPasswordForms(false /* only_visible */);
(...skipping 394 matching lines...) Expand 10 before | Expand all | Expand 10 after
1332 form_data, 1375 form_data,
1333 username_element, 1376 username_element,
1334 password_element, 1377 password_element,
1335 &nonscript_modified_values_, 1378 &nonscript_modified_values_,
1336 base::Bind(&PasswordValueGatekeeper::RegisterElement, 1379 base::Bind(&PasswordValueGatekeeper::RegisterElement,
1337 base::Unretained(&gatekeeper_))); 1380 base::Unretained(&gatekeeper_)));
1338 } 1381 }
1339 1382
1340 PasswordInfo password_info; 1383 PasswordInfo password_info;
1341 password_info.fill_data = form_data; 1384 password_info.fill_data = form_data;
1385 password_info.key = key;
1342 password_info.password_field = password_element; 1386 password_info.password_field = password_element;
1343 web_input_to_password_info_[main_element] = password_info; 1387 web_input_to_password_info_[main_element] = password_info;
1344 password_to_username_[password_element] = username_element; 1388 password_to_username_[password_element] = username_element;
1345 web_element_to_password_info_key_[main_element] = key;
1346 } 1389 }
1347 } 1390 }
1348 1391
1349 void PasswordAutofillAgent::OnSetLoggingState(bool active) { 1392 void PasswordAutofillAgent::OnSetLoggingState(bool active) {
1350 logging_state_active_ = active; 1393 logging_state_active_ = active;
1351 } 1394 }
1352 1395
1353 void PasswordAutofillAgent::OnAutofillUsernameAndPasswordDataReceived( 1396 void PasswordAutofillAgent::OnAutofillUsernameAndPasswordDataReceived(
1354 const FormsPredictionsMap& predictions) { 1397 const FormsPredictionsMap& predictions) {
1355 form_predictions_.insert(predictions.begin(), predictions.end()); 1398 form_predictions_.insert(predictions.begin(), predictions.end());
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
1387 1430
1388 //////////////////////////////////////////////////////////////////////////////// 1431 ////////////////////////////////////////////////////////////////////////////////
1389 // PasswordAutofillAgent, private: 1432 // PasswordAutofillAgent, private:
1390 1433
1391 PasswordAutofillAgent::PasswordInfo::PasswordInfo() 1434 PasswordAutofillAgent::PasswordInfo::PasswordInfo()
1392 : password_was_edited_last(false), 1435 : password_was_edited_last(false),
1393 username_was_edited(false) { 1436 username_was_edited(false) {
1394 } 1437 }
1395 1438
1396 bool PasswordAutofillAgent::ShowSuggestionPopup( 1439 bool PasswordAutofillAgent::ShowSuggestionPopup(
1397 const PasswordFormFillData& fill_data, 1440 const PasswordInfo& password_info,
1398 const blink::WebInputElement& user_input, 1441 const blink::WebInputElement& user_input,
1399 bool show_all, 1442 bool show_all,
1400 bool show_on_password_field) { 1443 bool show_on_password_field) {
1401 DCHECK(!user_input.isNull()); 1444 DCHECK(!user_input.isNull());
1402 blink::WebFrame* frame = user_input.document().frame(); 1445 blink::WebFrame* frame = user_input.document().frame();
1403 if (!frame) 1446 if (!frame)
1404 return false; 1447 return false;
1405 1448
1406 blink::WebView* webview = frame->view(); 1449 blink::WebView* webview = frame->view();
1407 if (!webview) 1450 if (!webview)
(...skipping 14 matching lines...) Expand all
1422 WebInputToPasswordInfoMap::const_iterator iter = 1465 WebInputToPasswordInfoMap::const_iterator iter =
1423 web_input_to_password_info_.find(user_input); 1466 web_input_to_password_info_.find(user_input);
1424 DCHECK(iter != web_input_to_password_info_.end()); 1467 DCHECK(iter != web_input_to_password_info_.end());
1425 selected_element = iter->second.password_field; 1468 selected_element = iter->second.password_field;
1426 } 1469 }
1427 1470
1428 blink::WebInputElement username; 1471 blink::WebInputElement username;
1429 if (!show_on_password_field || !user_input.isPasswordField()) { 1472 if (!show_on_password_field || !user_input.isPasswordField()) {
1430 username = user_input; 1473 username = user_input;
1431 } 1474 }
1432 WebElementToPasswordInfoKeyMap::const_iterator key_it =
1433 web_element_to_password_info_key_.find(user_input);
1434 DCHECK(key_it != web_element_to_password_info_key_.end());
1435 1475
1436 int options = 0; 1476 int options = 0;
1437 if (show_all) 1477 if (show_all)
1438 options |= SHOW_ALL; 1478 options |= SHOW_ALL;
1439 if (show_on_password_field) 1479 if (show_on_password_field)
1440 options |= IS_PASSWORD_FIELD; 1480 options |= IS_PASSWORD_FIELD;
1441 base::string16 username_string( 1481 base::string16 username_string(
1442 username.isNull() ? base::string16() 1482 username.isNull() ? base::string16()
1443 : static_cast<base::string16>(user_input.value())); 1483 : static_cast<base::string16>(user_input.value()));
1444 1484
1445 Send(new AutofillHostMsg_ShowPasswordSuggestions( 1485 Send(new AutofillHostMsg_ShowPasswordSuggestions(
1446 routing_id(), 1486 routing_id(), password_info.key, field.text_direction, username_string,
1447 key_it->second, 1487 options, render_frame()->GetRenderView()->ElementBoundsInWindow(
1448 field.text_direction, 1488 selected_element)));
1449 username_string,
1450 options,
1451 render_frame()->GetRenderView()->ElementBoundsInWindow(
1452 selected_element)));
1453 username_query_prefix_ = username_string; 1489 username_query_prefix_ = username_string;
1454 return CanShowSuggestion(fill_data, username_string, show_all); 1490 return CanShowSuggestion(password_info.fill_data, username_string, show_all);
1455 } 1491 }
1456 1492
1457 void PasswordAutofillAgent::FrameClosing() { 1493 void PasswordAutofillAgent::FrameClosing() {
1458 for (auto const& iter : web_input_to_password_info_) { 1494 for (auto const& iter : web_input_to_password_info_) {
1459 web_element_to_password_info_key_.erase(iter.first);
1460 password_to_username_.erase(iter.second.password_field); 1495 password_to_username_.erase(iter.second.password_field);
1461 } 1496 }
1462 web_input_to_password_info_.clear(); 1497 web_input_to_password_info_.clear();
1463 provisionally_saved_form_.reset(); 1498 provisionally_saved_form_.reset();
1464 nonscript_modified_values_.clear(); 1499 nonscript_modified_values_.clear();
1465 } 1500 }
1466 1501
1467 bool PasswordAutofillAgent::FindLoginInfo(const blink::WebNode& node,
1468 blink::WebInputElement* found_input,
1469 PasswordInfo** found_password) {
1470 if (!node.isElementNode())
1471 return false;
1472
1473 blink::WebElement element = node.toConst<blink::WebElement>();
1474 if (!element.hasHTMLTagName("input"))
1475 return false;
1476
1477 *found_input = element.to<blink::WebInputElement>();
1478 const blink::WebInputElement* username_element; // ignored
1479 return FindPasswordInfoForElement(*found_input, &username_element,
1480 found_password);
1481 }
1482
1483 void PasswordAutofillAgent::ClearPreview( 1502 void PasswordAutofillAgent::ClearPreview(
1484 blink::WebInputElement* username, 1503 blink::WebInputElement* username,
1485 blink::WebInputElement* password) { 1504 blink::WebInputElement* password) {
1486 if (!username->suggestedValue().isEmpty()) { 1505 if (!username->isNull() && !username->suggestedValue().isEmpty()) {
1487 username->setSuggestedValue(blink::WebString()); 1506 username->setSuggestedValue(blink::WebString());
1488 username->setAutofilled(was_username_autofilled_); 1507 username->setAutofilled(was_username_autofilled_);
1489 username->setSelectionRange(username_query_prefix_.length(), 1508 username->setSelectionRange(username_query_prefix_.length(),
1490 username->value().length()); 1509 username->value().length());
1491 } 1510 }
1492 if (!password->suggestedValue().isEmpty()) { 1511 if (!password->suggestedValue().isEmpty()) {
1493 password->setSuggestedValue(blink::WebString()); 1512 password->setSuggestedValue(blink::WebString());
1494 password->setAutofilled(was_password_autofilled_); 1513 password->setAutofilled(was_password_autofilled_);
1495 } 1514 }
1496 } 1515 }
(...skipping 10 matching lines...) Expand all
1507 } 1526 }
1508 1527
1509 bool PasswordAutofillAgent::ProvisionallySavedPasswordIsValid() { 1528 bool PasswordAutofillAgent::ProvisionallySavedPasswordIsValid() {
1510 return provisionally_saved_form_ && 1529 return provisionally_saved_form_ &&
1511 !provisionally_saved_form_->username_value.empty() && 1530 !provisionally_saved_form_->username_value.empty() &&
1512 !(provisionally_saved_form_->password_value.empty() && 1531 !(provisionally_saved_form_->password_value.empty() &&
1513 provisionally_saved_form_->new_password_value.empty()); 1532 provisionally_saved_form_->new_password_value.empty());
1514 } 1533 }
1515 1534
1516 } // namespace autofill 1535 } // namespace autofill
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698