diff options
author | jcivelli@google.com <jcivelli@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-07-14 17:36:08 +0000 |
---|---|---|
committer | jcivelli@google.com <jcivelli@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-07-14 17:36:08 +0000 |
commit | 03f57072cd19125c59fbc1d7c76f2856ca200850 (patch) | |
tree | 80eadc11ae10e48c8c21fe2bc6dfe50dd4a32ec6 | |
parent | a2a7fcfe13e9d42c35cb79a0c57d02c3ce7778ea (diff) | |
download | chromium_src-03f57072cd19125c59fbc1d7c76f2856ca200850.zip chromium_src-03f57072cd19125c59fbc1d7c76f2856ca200850.tar.gz chromium_src-03f57072cd19125c59fbc1d7c76f2856ca200850.tar.bz2 |
First step in moving the password autocomplete code from the WebKit API
to the Chromium code.
This is currently behind a #define that will be defined in the WebKit API
when the change is finished on that side.
PasswordAutocompleteManager replaces webpasswordautocompletelistener_impl.cc, it
is responsible for trigering password autocomplete
The unit-tests in webpasswordautocompletelistener_unittest.cc have been ported
to password_autocomplete_manager_unittest.cc. They are now test shell unit-tests
so we don't have to use a wrapper around WebElements.
BUG=None
TEST=The new code is not yet enabled. Test that the password autocomplete still
works as expected.
Review URL: http://codereview.chromium.org/2827030
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@52342 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/chrome_renderer.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/renderer/password_autocomplete_manager.cc | 413 | ||||
-rw-r--r-- | chrome/renderer/password_autocomplete_manager.h | 90 | ||||
-rw-r--r-- | chrome/renderer/password_autocomplete_manager_unittest.cc | 329 | ||||
-rw-r--r-- | chrome/renderer/render_view.cc | 44 | ||||
-rw-r--r-- | chrome/renderer/render_view.h | 14 |
7 files changed, 892 insertions, 1 deletions
diff --git a/chrome/chrome_renderer.gypi b/chrome/chrome_renderer.gypi index 1b27e29..b0b2532 100644 --- a/chrome/chrome_renderer.gypi +++ b/chrome/chrome_renderer.gypi @@ -107,6 +107,8 @@ 'renderer/notification_provider.cc', 'renderer/notification_provider.h', 'renderer/paint_aggregator.cc', + 'renderer/password_autocomplete_manager.cc', + 'renderer/password_autocomplete_manager.h', 'renderer/pepper_devices.cc', 'renderer/pepper_devices.h', 'renderer/pepper_plugin_delegate_impl.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index bfa03e2..08265f1 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1052,6 +1052,7 @@ 'renderer/net/predictor_queue_unittest.cc', 'renderer/net/renderer_predictor_unittest.cc', 'renderer/paint_aggregator_unittest.cc', + 'renderer/password_autocomplete_manager_unittest.cc', 'renderer/pepper_devices_unittest.cc', 'renderer/render_process_unittest.cc', 'renderer/render_thread_unittest.cc', diff --git a/chrome/renderer/password_autocomplete_manager.cc b/chrome/renderer/password_autocomplete_manager.cc new file mode 100644 index 0000000..a715a58 --- /dev/null +++ b/chrome/renderer/password_autocomplete_manager.cc @@ -0,0 +1,413 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/renderer/password_autocomplete_manager.h" + +#include "base/keyboard_codes.h" +#include "base/message_loop.h" +#include "base/scoped_ptr.h" +#include "third_party/WebKit/WebKit/chromium/public/WebDocument.h" +#include "third_party/WebKit/WebKit/chromium/public/WebElement.h" +#include "third_party/WebKit/WebKit/chromium/public/WebFormElement.h" +#include "third_party/WebKit/WebKit/chromium/public/WebFrame.h" +#include "third_party/WebKit/WebKit/chromium/public/WebInputEvent.h" +#include "third_party/WebKit/WebKit/chromium/public/WebVector.h" +#include "third_party/WebKit/WebKit/chromium/public/WebView.h" +#include "webkit/glue/form_field.h" +#include "webkit/glue/password_form_dom_manager.h" + +namespace { + +// The size above which we stop triggering autocomplete. +static const size_t kMaximumTextSizeForAutocomplete = 1000; + +// Maps element names to the actual elements to simplify form filling. +typedef std::map<string16, WebKit::WebInputElement> + FormInputElementMap; + +// Utility struct for form lookup and autocomplete. When we parse the DOM to +// lookup a form, in addition to action and origin URL's we have to compare all +// necessary form elements. To avoid having to look these up again when we want +// to fill the form, the FindFormElements function stores the pointers +// in a FormElements* result, referenced to ensure they are safe to use. +struct FormElements { + WebKit::WebFormElement form_element; + FormInputElementMap input_elements; +}; + +typedef std::vector<FormElements*> FormElementsList; + +// Helper to search the given form element for the specified input elements +// in |data|, and add results to |result|. +static bool FindFormInputElements(WebKit::WebFormElement* fe, + const webkit_glue::FormData& data, + FormElements* result) { + // Loop through the list of elements we need to find on the form in order to + // autocomplete it. If we don't find any one of them, abort processing this + // form; it can't be the right one. + for (size_t j = 0; j < data.fields.size(); j++) { + WebKit::WebVector<WebKit::WebNode> temp_elements; + fe->getNamedElements(data.fields[j].name(), temp_elements); + if (temp_elements.isEmpty()) { + // We didn't find a required element. This is not the right form. + // Make sure no input elements from a partially matched form in this + // iteration remain in the result set. + // Note: clear will remove a reference from each InputElement. + result->input_elements.clear(); + return false; + } + // This element matched, add it to our temporary result. It's possible there + // are multiple matches, but for purposes of identifying the form one + // suffices and if some function needs to deal with multiple matching + // elements it can get at them through the FormElement*. + // Note: This assignment adds a reference to the InputElement. + result->input_elements[data.fields[j].name()] = + temp_elements[0].to<WebKit::WebInputElement>(); + } + return true; +} + +// Helper to locate form elements identified by |data|. +void FindFormElements(WebKit::WebView* view, + const webkit_glue::FormData& data, + FormElementsList* results) { + DCHECK(view); + DCHECK(results); + WebKit::WebFrame* main_frame = view->mainFrame(); + if (!main_frame) + return; + + GURL::Replacements rep; + rep.ClearQuery(); + rep.ClearRef(); + + // Loop through each frame. + for (WebKit::WebFrame* f = main_frame; f; f = f->traverseNext(false)) { + WebKit::WebDocument doc = f->document(); + if (!doc.isHTMLDocument()) + continue; + + GURL full_origin(f->url()); + if (data.origin != full_origin.ReplaceComponents(rep)) + continue; + + WebKit::WebVector<WebKit::WebFormElement> forms; + f->forms(forms); + + for (size_t i = 0; i < forms.size(); ++i) { + WebKit::WebFormElement fe = forms[i]; + // Action URL must match. + GURL full_action(f->document().completeURL(fe.action())); + if (data.action != full_action.ReplaceComponents(rep)) + continue; + + scoped_ptr<FormElements> curr_elements(new FormElements); + if (!FindFormInputElements(&fe, data, curr_elements.get())) + continue; + + // We found the right element. + // Note: this assignment adds a reference to |fe|. + curr_elements->form_element = fe; + results->push_back(curr_elements.release()); + } + } +} + +bool FillForm(FormElements* fe, const webkit_glue::FormData& data) { + if (!fe->form_element.autoComplete()) + return false; + + std::map<string16, string16> data_map; + for (size_t i = 0; i < data.fields.size(); i++) + data_map[data.fields[i].name()] = data.fields[i].value(); + + for (FormInputElementMap::iterator it = fe->input_elements.begin(); + it != fe->input_elements.end(); ++it) { + WebKit::WebInputElement& element = it->second; + if (!element.value().isEmpty()) // Don't overwrite pre-filled values. + continue; + if (element.inputType() == WebKit::WebInputElement::Password && + (!element.isEnabledFormControl() || element.hasAttribute("readonly"))) { + continue; // Don't fill uneditable password fields. + } + element.setValue(data_map[it->first]); + element.setAutofilled(true); + element.dispatchFormControlChangeEvent(); + } + + return false; +} + +bool IsElementEditable(const WebKit::WebInputElement& element) { + return element.isEnabledFormControl() && !element.hasAttribute("readonly"); +} + +void SetElementAutofilled(WebKit::WebInputElement* element, bool autofilled) { + if (element->isAutofilled() == autofilled) + return; + element->setAutofilled(autofilled); + // Notify any changeEvent listeners. + element->dispatchFormControlChangeEvent(); +} + +bool DoUsernamesMatch(const string16& username1, + const string16& username2, + bool exact_match) { + if (exact_match) + return username1 == username2; + return StartsWith(username1, username2, false); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// PasswordAutocompleteManager, public: + +PasswordAutocompleteManager::PasswordAutocompleteManager( + RenderView* render_view) + : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { +} + +void PasswordAutocompleteManager::ReceivedPasswordFormFillData( + WebKit::WebView* view, + const webkit_glue::PasswordFormDomManager::FillData& form_data) { + FormElementsList forms; + // We own the FormElements* in forms. + FindFormElements(view, form_data.basic_data, &forms); + FormElementsList::iterator iter; + for (iter = forms.begin(); iter != forms.end(); ++iter) { + scoped_ptr<FormElements> form_elements(*iter); + + // If wait_for_username is true, we don't want to initially fill the form + // until the user types in a valid username. + if (!form_data.wait_for_username) + FillForm(form_elements.get(), form_data.basic_data); + + // Attach autocomplete listener to enable selecting alternate logins. + // First, get pointers to username element. + WebKit::WebInputElement username_element = + form_elements->input_elements[form_data.basic_data.fields[0].name()]; + + // Get pointer to password element. (We currently only support single + // password forms). + WebKit::WebInputElement password_element = + form_elements->input_elements[form_data.basic_data.fields[1].name()]; + + DCHECK(login_to_password_info_.find(username_element) == + login_to_password_info_.end()); + PasswordInfo password_info; + password_info.fill_data = form_data; + password_info.password_field = password_element; + login_to_password_info_[username_element] = password_info; + } +} + +void PasswordAutocompleteManager::FrameClosing(const WebKit::WebFrame* frame) { + for (LoginToPasswordInfoMap::iterator iter = login_to_password_info_.begin(); + iter != login_to_password_info_.end(); ++iter) { + if (iter->first.document().frame() == frame) + login_to_password_info_.erase(iter); + } +} + +void PasswordAutocompleteManager::TextFieldDidBeginEditing( + const WebKit::WebInputElement& element) { +} + +void PasswordAutocompleteManager::TextFieldDidEndEditing( + const WebKit::WebInputElement& element) { + LoginToPasswordInfoMap::const_iterator iter = + login_to_password_info_.find(element); + if (iter == login_to_password_info_.end()) + return; + + const webkit_glue::PasswordFormDomManager::FillData& fill_data = + iter->second.fill_data; + + // If wait_for_username is false, we should have filled when the text changed. + if (!fill_data.wait_for_username) + return; + + WebKit::WebInputElement password = iter->second.password_field; + if (!IsElementEditable(password)) + return; + + WebKit::WebInputElement username = element; // We need a non-const. + FillUserNameAndPassword(&username, &password, fill_data, true); +} + +void PasswordAutocompleteManager::TextDidChangeInTextField( + const WebKit::WebInputElement& element) { + LoginToPasswordInfoMap::const_iterator iter = + login_to_password_info_.find(element); + if (iter == login_to_password_info_.end()) + return; + + // The input text is being changed, so any autocompleted password is now + // outdated. + WebKit::WebInputElement username = element; // We need a non-const. + WebKit::WebInputElement password = iter->second.password_field; + SetElementAutofilled(&username, false); + if (password.isAutofilled()) { + password.setValue(string16()); + SetElementAutofilled(&password, false); + } + + // If wait_for_username is true we will fill when the username loses focus. + if (iter->second.fill_data.wait_for_username) + return; + + if (!element.isEnabledFormControl() || + element.inputType() != WebKit::WebInputElement::Text || + !element.autoComplete() || element.isReadOnly()) { + return; + } + + // Don't inline autocomplete if the user is deleting, that would be confusing. + if (iter->second.backspace_pressed_last) + return; + + WebKit::WebString name = element.nameForAutofill(); + if (name.isEmpty()) + return; // If the field has no name, then we won't have values. + + // Don't attempt to autocomplete with values that are too large. + if (element.value().length() > kMaximumTextSizeForAutocomplete) + return; + + // We post a task for doing the autocomplete as the caret position is not set + // properly at this point (http://bugs.webkit.org/show_bug.cgi?id=16976) and + // we need it to determine whether or not to trigger autocomplete. + MessageLoop::current()->PostTask(FROM_HERE, method_factory_.NewRunnableMethod( + &PasswordAutocompleteManager::PerformInlineAutocomplete, + element, password, iter->second.fill_data)); +} + +void PasswordAutocompleteManager::TextFieldHandlingKeyDown( + const WebKit::WebInputElement& element, + const WebKit::WebKeyboardEvent& event) { + + LoginToPasswordInfoMap::iterator iter = login_to_password_info_.find(element); + if (iter == login_to_password_info_.end()) + return; + + int win_key_code = event.windowsKeyCode; + iter->second.backspace_pressed_last = + (win_key_code == base::VKEY_BACK || win_key_code == base::VKEY_DELETE); +} + +bool PasswordAutocompleteManager::FillPassword( + const WebKit::WebInputElement& user_input) { + LoginToPasswordInfoMap::iterator iter = + login_to_password_info_.find(user_input); + if (iter == login_to_password_info_.end()) + return false; + const webkit_glue::PasswordFormDomManager::FillData& fill_data = + iter->second.fill_data; + WebKit::WebInputElement password = iter->second.password_field; + WebKit::WebInputElement non_const_user_input(user_input); + return FillUserNameAndPassword(&non_const_user_input, &password, + fill_data, true); +} + +void PasswordAutocompleteManager::PerformInlineAutocomplete( + const WebKit::WebInputElement& username_input, + const WebKit::WebInputElement& password_input, + const webkit_glue::PasswordFormDomManager::FillData& fill_data) { + DCHECK(!fill_data.wait_for_username); + + // We need non-const versions of the username and password inputs. + WebKit::WebInputElement username = username_input; + WebKit::WebInputElement password = password_input; + + // Don't inline autocomplete if the caret is not at the end. + // TODO(jcivelli): is there a better way to test the caret location? + if (username.selectionStart() != username.selectionEnd() || + username.selectionEnd() != static_cast<int>(username.value().length())) { + return; + } + + // Show the popup with the list of available usernames. + ShowSuggestionPopup(fill_data, username); + + // Fill the user and password field with the most relevant match. + FillUserNameAndPassword(&username, &password, fill_data, false); +} + +//////////////////////////////////////////////////////////////////////////////// +// PasswordAutocompleteManager, private: + +void PasswordAutocompleteManager::GetSuggestions( + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + const string16& input, + std::vector<string16>* suggestions) { + if (StartsWith(fill_data.basic_data.fields[0].value(), input, false)) + suggestions->push_back(fill_data.basic_data.fields[0].value()); + + webkit_glue::PasswordFormDomManager::LoginCollection::const_iterator iter; + for (iter = fill_data.additional_logins.begin(); + iter != fill_data.additional_logins.end(); ++iter) { + if (StartsWith(iter->first, input, false)) + suggestions->push_back(iter->first); + } +} + +bool PasswordAutocompleteManager::ShowSuggestionPopup( + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + const WebKit::WebInputElement& user_input) { + std::vector<string16> suggestions; + GetSuggestions(fill_data, user_input.value(), &suggestions); + if (suggestions.empty()) + return false; + + WebKit::WebView* webview = user_input.document().frame()->view(); + if (!webview) + return false; + + webview->applyAutocompleteSuggestions(user_input, suggestions, -1); + return true; +} + +bool PasswordAutocompleteManager::FillUserNameAndPassword( + WebKit::WebInputElement* username_element, + WebKit::WebInputElement* password_element, + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + bool exact_username_match) { + string16 current_username = username_element->value(); + // username and password will contain the match found if any. + string16 username; + string16 password; + + // Look for any suitable matches to current field text. + if (DoUsernamesMatch(fill_data.basic_data.fields[0].value(), current_username, + exact_username_match)) { + username = fill_data.basic_data.fields[0].value(); + password = fill_data.basic_data.fields[1].value(); + } else { + // Scan additional logins for a match. + webkit_glue::PasswordFormDomManager::LoginCollection::const_iterator iter; + for (iter = fill_data.additional_logins.begin(); + iter != fill_data.additional_logins.end(); ++iter) { + if (DoUsernamesMatch(iter->first, current_username, + exact_username_match)) { + username = iter->first; + password = iter->second; + break; + } + } + } + if (password.empty()) + return false; // No match was found. + + // Input matches the username, fill in required values. + username_element->setValue(username); + username_element->setSelectionRange(current_username.length(), + username.length()); + SetElementAutofilled(username_element, true); + if (IsElementEditable(*password_element)) + password_element->setValue(password); + SetElementAutofilled(password_element, true); + return true; +} + diff --git a/chrome/renderer/password_autocomplete_manager.h b/chrome/renderer/password_autocomplete_manager.h new file mode 100644 index 0000000..a931026 --- /dev/null +++ b/chrome/renderer/password_autocomplete_manager.h @@ -0,0 +1,90 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_RENDERER_PASSWORD_AUTOCOMPLETE_MANAGER_H_ +#define CHROME_RENDERER_PASSWORD_AUTOCOMPLETE_MANAGER_H_ + +#include <map> +#include <vector> + +#include "base/task.h" +#include "webkit/glue/password_form_dom_manager.h" +#include "third_party/WebKit/WebKit/chromium/public/WebInputElement.h" + +class RenderView; +namespace WebKit { +class WebKeyboardEvent; +class WebView; +} + +// This class is responsible for filling password forms. +// There is one PasswordAutocompleteManager per RenderView. +class PasswordAutocompleteManager { + public: + explicit PasswordAutocompleteManager(RenderView* render_view); + virtual ~PasswordAutocompleteManager() {} + + // Invoked by the renderer when it receives the password info from the + // browser. This triggers a password autocomplete (if wait_for_username is + // false on |form_data|). + void ReceivedPasswordFormFillData(WebKit::WebView* view, + const webkit_glue::PasswordFormDomManager::FillData& form_data); + + // Invoked when the passed frame is closing. Gives us a chance to clear any + // reference we may have to elements in that frame. + void FrameClosing(const WebKit::WebFrame* frame); + + // Fills the password associated with |user_input|, using its current value + // as the actual user name. Returns true if the password field was filled, + // false otherwise, typically if there was no matching suggestions for the + // currently typed username. + bool FillPassword(const WebKit::WebInputElement& user_input); + + // Fills |login_input| and |password| with the most relevant suggestion from + // |fill_data| and shows a popup with other suggestions. + void PerformInlineAutocomplete( + const WebKit::WebInputElement& username, + const WebKit::WebInputElement& password, + const webkit_glue::PasswordFormDomManager::FillData& fill_data); + + // WebViewClient editor related calls forwarded by the RenderView. + void TextFieldDidBeginEditing(const WebKit::WebInputElement& element); + void TextFieldDidEndEditing(const WebKit::WebInputElement& element); + void TextDidChangeInTextField(const WebKit::WebInputElement& element); + void TextFieldHandlingKeyDown(const WebKit::WebInputElement& element, + const WebKit::WebKeyboardEvent& event); + + private: + struct PasswordInfo { + WebKit::WebInputElement password_field; + webkit_glue::PasswordFormDomManager::FillData fill_data; + bool backspace_pressed_last; + PasswordInfo() : backspace_pressed_last(false) {} + }; + typedef std::map<WebKit::WebElement, PasswordInfo> LoginToPasswordInfoMap; + + void GetSuggestions( + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + const string16& input, + std::vector<string16>* suggestions); + + bool ShowSuggestionPopup( + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + const WebKit::WebInputElement& user_input); + + bool FillUserNameAndPassword( + WebKit::WebInputElement* username_element, + WebKit::WebInputElement* password_element, + const webkit_glue::PasswordFormDomManager::FillData& fill_data, + bool exact_username_match); + + // The logins we have filled so far with their associated info. + LoginToPasswordInfoMap login_to_password_info_; + + ScopedRunnableMethodFactory<PasswordAutocompleteManager> method_factory_; + + DISALLOW_COPY_AND_ASSIGN(PasswordAutocompleteManager); +}; + +#endif // CHROME_RENDERER_PASSWORD_AUTOCOMPLETE_MANAGER_H_ diff --git a/chrome/renderer/password_autocomplete_manager_unittest.cc b/chrome/renderer/password_autocomplete_manager_unittest.cc new file mode 100644 index 0000000..fc32332 --- /dev/null +++ b/chrome/renderer/password_autocomplete_manager_unittest.cc @@ -0,0 +1,329 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/keyboard_codes.h" +#include "base/string_util.h" +#include "chrome/renderer/password_autocomplete_manager.h" +#include "chrome/test/render_view_test.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/WebKit/chromium/public/WebDocument.h" +#include "third_party/WebKit/WebKit/chromium/public/WebElement.h" +#include "third_party/WebKit/WebKit/chromium/public/WebFormElement.h" +#include "third_party/WebKit/WebKit/chromium/public/WebInputElement.h" +#include "third_party/WebKit/WebKit/chromium/public/WebNode.h" +#include "third_party/WebKit/WebKit/chromium/public/WebString.h" +#include "third_party/WebKit/WebKit/chromium/public/WebVector.h" +#include "webkit/glue/form_data.h" +#include "webkit/glue/form_field.h" + +using webkit_glue::FormField; +using webkit_glue::PasswordForm; +using webkit_glue::PasswordFormDomManager; +using WebKit::WebDocument; +using WebKit::WebElement; +using WebKit::WebFrame; +using WebKit::WebInputElement; +using WebKit::WebString; + +namespace { + +// The name of the username/password element in the form. +const char* const kUsernameName = "username"; +const char* const kPasswordName = "password"; + +const char* const kAliceUsername = "alice"; +const char* const kAlicePassword = "password"; +const char* const kBobUsername = "bob"; +const char* const kBobPassword = "secret"; + +const char* const kFormHTML = + "<FORM name='LoginTestForm' action='http://www.bidule.com'>" + " <INPUT type='text' id='username'/>" + " <INPUT type='password' id='password'/>" + " <INPUT type='submit' value='Login'/>" + "</FORM>"; + +class PasswordAutocompleteManagerTest : public RenderViewTest { + public: + PasswordAutocompleteManagerTest() { + } + + // Simulates the fill password form message being sent to the renderer. + // We use that so we don't have to make RenderView::OnFillPasswordForm() + // protected. + void SimulateOnFillPasswordForm( + const PasswordFormDomManager::FillData& fill_data) { + ViewMsg_FillPasswordForm msg(0, fill_data); + view_->OnMessageReceived(msg); + } + + virtual void SetUp() { + RenderViewTest::SetUp(); + + // Add a preferred login and an additional login to the FillData. + username1_ = ASCIIToUTF16(kAliceUsername); + password1_ = ASCIIToUTF16(kAlicePassword); + username2_ = ASCIIToUTF16(kBobUsername); + password2_ = ASCIIToUTF16(kBobPassword); + + fill_data_.basic_data.fields.push_back( + FormField(string16(), ASCIIToUTF16(kUsernameName), + username1_, string16(), 0)); + fill_data_.basic_data.fields.push_back( + FormField(string16(), ASCIIToUTF16(kPasswordName), + password1_, string16(), 0)); + fill_data_.additional_logins[username2_] = password2_; + + // We need to set the origin so it matches the frame URL and the action so + // it matches the form action, otherwise we won't autocomplete. + std::string origin("data:text/html;charset=utf-8,"); + origin += kFormHTML; + fill_data_.basic_data.origin = GURL(origin); + fill_data_.basic_data.action = GURL("http://www.bidule.com"); + + LoadHTML(kFormHTML); + + // Now retrieves the input elements so the test can access them. + WebDocument document = GetMainFrame()->document(); + WebElement element = + document.getElementById(WebString::fromUTF8(kUsernameName)); + ASSERT_FALSE(element.isNull()); + username_element_ = element.to<WebKit::WebInputElement>(); + element = document.getElementById(WebString::fromUTF8(kPasswordName)); + ASSERT_FALSE(element.isNull()); + password_element_ = element.to<WebKit::WebInputElement>(); + } + + void ClearUsernameAndPasswordFields() { + username_element_.setValue(""); + username_element_.setAutofilled(false); + password_element_.setValue(""); + password_element_.setAutofilled(false); + } + + void SimulateUsernameChange(const std::string& username, + bool move_caret_to_end) { + username_element_.setValue(WebString::fromUTF8(username)); + if (move_caret_to_end) + username_element_.setSelectionRange(username.length(), username.length()); + view_->textFieldDidChange(username_element_); + // Processing is delayed because of a WebKit bug, see + // PasswordAutocompleteManager::TextDidChangeInTextField() for details. + MessageLoop::current()->RunAllPending(); + } + + void SimulateKeyDownEvent(const WebInputElement& element, + base::KeyboardCode key_code) { + WebKit::WebKeyboardEvent key_event; + key_event.windowsKeyCode = key_code; + view_->textFieldDidReceiveKeyDown(element, key_event); + } + + void CheckTextFieldsState(const std::string& username, + bool username_autofilled, + const std::string& password, + bool password_autofilled) { + EXPECT_EQ(username, + static_cast<std::string>(username_element_.value().utf8())); + EXPECT_EQ(username_autofilled, username_element_.isAutofilled()); + EXPECT_EQ(password, + static_cast<std::string>(password_element_.value().utf8())); + EXPECT_EQ(password_autofilled, password_element_.isAutofilled()); + } + + void CheckUsernameSelection(int start, int end) { + EXPECT_EQ(start, username_element_.selectionStart()); + EXPECT_EQ(end, username_element_.selectionEnd()); + } + + string16 username1_; + string16 username2_; + string16 password1_; + string16 password2_; + PasswordFormDomManager::FillData fill_data_; + + WebInputElement username_element_; + WebInputElement password_element_; + + private: + DISALLOW_COPY_AND_ASSIGN(PasswordAutocompleteManagerTest); +}; + +#if defined(WEBKIT_BUG_41283_IS_FIXED) +#define MAYBE_InitialAutocomplete InitialAutocomplete +#define MAYBE_NoInitialAutocompleteForReadOnly NoInitialAutocompleteForReadOnly +#define MAYBE_PasswordClearOnEdit PasswordClearOnEdit +#define MAYBE_WaitUsername WaitUsername +#define MAYBE_InlineAutocomplete InlineAutocomplete +#define MAYBE_SuggestionSelect SuggestionSelect +#else +#define MAYBE_InitialAutocomplete DISABLED_InitialAutocomplete +#define MAYBE_NoInitialAutocompleteForReadOnly \ + DISABLED_NoInitialAutocompleteForReadOnly +#define MAYBE_PasswordClearOnEdit DISABLED_PasswordClearOnEdit +#define MAYBE_WaitUsername DISABLED_WaitUsername +#define MAYBE_InlineAutocomplete DISABLED_InlineAutocomplete +#define MAYBE_SuggestionSelect DISABLED_SuggestionSelect +#endif + +// Tests that the password login is autocompleted as expected when the browser +// sends back the password info. +TEST_F(PasswordAutocompleteManagerTest, MAYBE_InitialAutocomplete) { + /* + * Right now we are not sending the message to the browser because we are + * loading a data URL and the security origin canAccessPasswordManager() + * returns false. May be we should mock URL loading to cirmcuvent this? + TODO(jcivelli): find a way to make the security origin not deny access to the + password manager and then reenable this code. + + // The form has been loaded, we should have sent the browser a message about + // the form. + const IPC::Message* msg = render_thread_.sink().GetFirstMessageMatching( + ViewHostMsg_PasswordFormsFound::ID); + ASSERT_TRUE(msg != NULL); + + Tuple1<std::vector<PasswordForm> > forms; + ViewHostMsg_PasswordFormsFound::Read(msg, &forms); + ASSERT_EQ(1U, forms.a.size()); + PasswordForm password_form = forms.a[0]; + EXPECT_EQ(PasswordForm::SCHEME_HTML, password_form.scheme); + EXPECT_EQ(ASCIIToUTF16(kUsernameName), password_form.username_element); + EXPECT_EQ(ASCIIToUTF16(kPasswordName), password_form.password_element); + */ + + // Simulate the browser sending back the login info, it triggers the + // autocomplete. + SimulateOnFillPasswordForm(fill_data_); + + // The username and password should have been autocompleted. + CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); +} + +// Tests that changing the username does not fill a read-only password field. +TEST_F(PasswordAutocompleteManagerTest, + MAYBE_NoInitialAutocompleteForReadOnly) { + password_element_.setAttribute(WebString::fromUTF8("readonly"), + WebString::fromUTF8("true")); + + // Simulate the browser sending back the login info, it triggers the + // autocompleted. + SimulateOnFillPasswordForm(fill_data_); + + // Only the username should have been autocompleted. + // TODO(jcivelli): may be we should not event fill the username? + CheckTextFieldsState(kAliceUsername, true, "", false); +} + +// Tests that editing the password clears the autocompleted password field. +TEST_F(PasswordAutocompleteManagerTest, MAYBE_PasswordClearOnEdit) { + // Simulate the browser sending back the login info, it triggers the + // autocomplete. + SimulateOnFillPasswordForm(fill_data_); + + // Simulate the user changing the username to some unknown username. + SimulateUsernameChange("alicia", true); + + // The password should have been cleared. + CheckTextFieldsState("alicia", false, "", false); +} + +// Tests that we only autocomplete on focus lost and with a full username match +// when |wait_for_username| is true. +TEST_F(PasswordAutocompleteManagerTest, MAYBE_WaitUsername) { + // Simulate the browser sending back the login info. + fill_data_.wait_for_username = true; + SimulateOnFillPasswordForm(fill_data_); + + // No auto-fill should have taken place. + CheckTextFieldsState("", false, "", false); + + // No autocomplete should happen when text is entered in the username. + SimulateUsernameChange("a", true); + CheckTextFieldsState("a", false, "", false); + SimulateUsernameChange("al", true); + CheckTextFieldsState("al", false, "", false); + SimulateUsernameChange(kAliceUsername, true); + CheckTextFieldsState(kAliceUsername, false, "", false); + + // Autocomplete should happen only when the username textfield is blurred with + // a full match. + username_element_.setValue("a"); + view_->textFieldDidEndEditing(username_element_); + CheckTextFieldsState("a", false, "", false); + username_element_.setValue("al"); + view_->textFieldDidEndEditing(username_element_); + CheckTextFieldsState("al", false, "", false); + username_element_.setValue("alices"); + view_->textFieldDidEndEditing(username_element_); + CheckTextFieldsState("alices", false, "", false); + username_element_.setValue(ASCIIToUTF16(kAliceUsername)); + view_->textFieldDidEndEditing(username_element_); + CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); +} + +// Tests that inline autocompletion works properly. +TEST_F(PasswordAutocompleteManagerTest, MAYBE_InlineAutocomplete) { + // Simulate the browser sending back the login info. + SimulateOnFillPasswordForm(fill_data_); + + // Clear the textfields to start fresh. + ClearUsernameAndPasswordFields(); + + // Simulate the user typing in the first letter of 'alice', a stored username. + SimulateUsernameChange("a", true); + // Both the username and password textfields should reflect selection of the + // stored login. + CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); + // And the selection should have been set to 'lice', the last 4 letters. + CheckUsernameSelection(1, 5); + + // Now the user types the next letter of the same username, 'l'. + SimulateUsernameChange("al", true); + // Now the fields should have the same value, but the selection should have a + // different start value. + CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); + CheckUsernameSelection(2, 5); + + // Test that deleting does not trigger autocomplete. + SimulateKeyDownEvent(username_element_, base::VKEY_BACK); + SimulateUsernameChange("alic", true); + CheckTextFieldsState("alic", false, "", false); + CheckUsernameSelection(4, 4); // No selection. + // Reset the last pressed key to something other than backspace. + SimulateKeyDownEvent(username_element_, base::VKEY_A); + + // Now lets say the user goes astray from the stored username and types the + // letter 'f', spelling 'alf'. We don't know alf (that's just sad), so in + // practice the username should no longer be 'alice' and the selected range + // should be empty. + SimulateUsernameChange("alf", true); + CheckTextFieldsState("alf", false, "", false); + CheckUsernameSelection(3, 3); // No selection. + + // Ok, so now the user removes all the text and enters the letter 'b'. + SimulateUsernameChange("b", true); + // The username and password fields should match the 'bob' entry. + CheckTextFieldsState(kBobUsername, true, kBobPassword, true); + CheckUsernameSelection(1, 3); +} + +// Tests that selecting and item in the suggestion drop-down works. +TEST_F(PasswordAutocompleteManagerTest, MAYBE_SuggestionSelect) { + // Simulate the browser sending back the login info. + SimulateOnFillPasswordForm(fill_data_); + + // Clear the textfields to start fresh. + ClearUsernameAndPasswordFields(); + + // To simulate a selection in the suggestion drop-down we just mimick what the + // WebView does: it sets the element value then calls + // didAcceptAutocompleteSuggestion on the renderer. + username_element_.setValue(ASCIIToUTF16(kAliceUsername)); + view_->didAcceptAutocompleteSuggestion(username_element_); + + // Autocomplete should have kicked in. + CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); +} + +} // namespace diff --git a/chrome/renderer/render_view.cc b/chrome/renderer/render_view.cc index c539120..305057d 100644 --- a/chrome/renderer/render_view.cc +++ b/chrome/renderer/render_view.cc @@ -439,6 +439,7 @@ RenderView::RenderView(RenderThreadBase* render_thread, ALLOW_THIS_IN_INITIALIZER_LIST(pepper_delegate_(this)), ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), ALLOW_THIS_IN_INITIALIZER_LIST(translate_helper_(this)), + ALLOW_THIS_IN_INITIALIZER_LIST(password_autocomplete_manager_(this)), ALLOW_THIS_IN_INITIALIZER_LIST(cookie_jar_(this)), ALLOW_THIS_IN_INITIALIZER_LIST( notification_provider_(new NotificationProvider(this))), @@ -1797,6 +1798,34 @@ void RenderView::didExecuteCommand(const WebString& command_name) { UserMetricsRecordAction(name); } +void RenderView::textFieldDidBeginEditing( + const WebKit::WebInputElement& element) { +#if defined(WEBKIT_BUG_41283_IS_FIXED) + password_autocomplete_manager_.TextFieldDidBeginEditing(element); +#endif +} + +void RenderView::textFieldDidEndEditing( + const WebKit::WebInputElement& element) { +#if defined(WEBKIT_BUG_41283_IS_FIXED) + password_autocomplete_manager_.TextFieldDidEndEditing(element); +#endif +} + +void RenderView::textFieldDidChange(const WebKit::WebInputElement& element) { +#if defined(WEBKIT_BUG_41283_IS_FIXED) + password_autocomplete_manager_.TextDidChangeInTextField(element); +#endif +} + +void RenderView::textFieldDidReceiveKeyDown( + const WebKit::WebInputElement& element, + const WebKit::WebKeyboardEvent& event) { +#if defined(WEBKIT_BUG_41283_IS_FIXED) + password_autocomplete_manager_.TextFieldHandlingKeyDown(element, event); +#endif +} + bool RenderView::handleCurrentKeyboardEvent() { if (edit_commands_.empty()) return false; @@ -2168,6 +2197,16 @@ void RenderView::didClearAutoFillSelection(const WebKit::WebNode& node) { form_manager_.ClearPreviewedForm(form); } +void RenderView::didAcceptAutocompleteSuggestion( + const WebKit::WebInputElement& user_element) { +#if defined(WEBKIT_BUG_41283_IS_FIXED) + bool result = password_autocomplete_manager_.FillPassword(user_element); + // Since this user name was selected from a suggestion list, we should always + // have password for it. + DCHECK(result); +#endif +} + // WebKit::WebWidgetClient ---------------------------------------------------- // We are supposed to get a single call to Show for a newly created RenderView @@ -3867,7 +3906,10 @@ void RenderView::OnDragSourceSystemDragEnded() { void RenderView::OnFillPasswordForm( const webkit_glue::PasswordFormDomManager::FillData& form_data) { - webkit_glue::FillPasswordForm(this->webview(), form_data); +#if defined(WEBKIT_BUG_41283_IS_FIXED) + password_autocomplete_manager_.ReceivedPasswordFormFillData(webview(), + form_data); +#endif } void RenderView::OnDragTargetDragEnter(const WebDropData& drop_data, diff --git a/chrome/renderer/render_view.h b/chrome/renderer/render_view.h index 71b8693..f81d847 100644 --- a/chrome/renderer/render_view.h +++ b/chrome/renderer/render_view.h @@ -37,6 +37,7 @@ #include "chrome/renderer/external_host_bindings.h" #include "chrome/renderer/form_manager.h" #include "chrome/renderer/notification_provider.h" +#include "chrome/renderer/password_autocomplete_manager.h" #include "chrome/renderer/pepper_plugin_delegate_impl.h" #include "chrome/renderer/render_widget.h" #include "chrome/renderer/render_view_visitor.h" @@ -107,6 +108,8 @@ class WebDocument; class WebDragData; class WebGeolocationServiceInterface; class WebImage; +class WebInputElement; +class WebKeyboardEvent; class WebMediaPlayer; class WebMediaPlayerClient; class WebPlugin; @@ -330,6 +333,12 @@ class RenderView : public RenderWidget, virtual bool isSelectTrailingWhitespaceEnabled(); virtual void didChangeSelection(bool is_selection_empty); virtual void didExecuteCommand(const WebKit::WebString& command_name); + virtual void textFieldDidBeginEditing(const WebKit::WebInputElement& element); + virtual void textFieldDidEndEditing(const WebKit::WebInputElement& element); + virtual void textFieldDidChange(const WebKit::WebInputElement& element); + virtual void textFieldDidReceiveKeyDown( + const WebKit::WebInputElement& element, + const WebKit::WebKeyboardEvent& event); virtual bool handleCurrentKeyboardEvent(); virtual void spellCheck(const WebKit::WebString& text, int& offset, @@ -397,6 +406,8 @@ class RenderView : public RenderWidget, const WebKit::WebString& value, const WebKit::WebString& label); virtual void didClearAutoFillSelection(const WebKit::WebNode& node); + virtual void didAcceptAutocompleteSuggestion( + const WebKit::WebInputElement& element); virtual WebKit::WebGeolocationService* geolocationService(); // WebKit::WebFrameClient implementation ------------------------------------- @@ -1198,6 +1209,9 @@ class RenderView : public RenderWidget, // Responsible for translating the page contents to other languages. TranslateHelper translate_helper_; + // Responsible for automatically filling login and password textfields. + PasswordAutocompleteManager password_autocomplete_manager_; + RendererWebCookieJarImpl cookie_jar_; // Provides access to this renderer from the remote Inspector UI. |