summaryrefslogtreecommitdiffstats
path: root/chrome/browser/password_form_manager.cc
diff options
context:
space:
mode:
authorinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
committerinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
commit09911bf300f1a419907a9412154760efd0b7abc3 (patch)
treef131325fb4e2ad12c6d3504ab75b16dd92facfed /chrome/browser/password_form_manager.cc
parent586acc5fe142f498261f52c66862fa417c3d52d2 (diff)
downloadchromium_src-09911bf300f1a419907a9412154760efd0b7abc3.zip
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.gz
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.bz2
Add chrome to the repository.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/password_form_manager.cc')
-rw-r--r--chrome/browser/password_form_manager.cc536
1 files changed, 536 insertions, 0 deletions
diff --git a/chrome/browser/password_form_manager.cc b/chrome/browser/password_form_manager.cc
new file mode 100644
index 0000000..4cd12d4
--- /dev/null
+++ b/chrome/browser/password_form_manager.cc
@@ -0,0 +1,536 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/password_form_manager.h"
+
+#include <algorithm>
+
+#include "base/string_util.h"
+#include "chrome/browser/ie7_password.h"
+#include "chrome/browser/password_manager.h"
+#include "chrome/browser/profile.h"
+#include "webkit/glue/password_form_dom_manager.h"
+
+PasswordFormManager::PasswordFormManager(Profile* profile,
+ PasswordManager* password_manager,
+ const PasswordForm& observed_form,
+ bool ssl_valid)
+ : observed_form_(observed_form),
+ password_manager_(password_manager),
+ profile_(profile),
+ is_new_login_(true),
+ pending_login_query_(NULL),
+ preferred_match_(NULL),
+ best_matches_deleter_(&best_matches_),
+ state_(PRE_MATCHING_PHASE) {
+ DCHECK(profile_);
+ if (observed_form_.origin.is_valid())
+ SplitString(observed_form_.origin.path(), '/', &form_path_tokens_);
+ observed_form_.ssl_valid = ssl_valid;
+}
+
+PasswordFormManager::~PasswordFormManager() {
+ CancelLoginsQuery();
+}
+
+// TODO(timsteele): use a hash of some sort in the future?
+bool PasswordFormManager::DoesManage(const PasswordForm& form) const {
+ if (form.scheme != PasswordForm::SCHEME_HTML)
+ return observed_form_.signon_realm == form.signon_realm;
+
+ // HTML form case.
+ // At a minimum, username and password element must match.
+ if (!((form.username_element == observed_form_.username_element) &&
+ (form.password_element == observed_form_.password_element))) {
+ return false;
+ }
+
+ // The action URL must also match, but the form is allowed to have an empty
+ // action URL (See bug 1107719).
+ if (form.action.is_valid() && (form.action != observed_form_.action))
+ return false;
+
+ // If this is a replay of the same form in the case a user entered an invalid
+ // password, the origin of the new form may equal the action of the "first"
+ // form.
+ if (!((form.origin == observed_form_.origin) ||
+ (form.origin == observed_form_.action))) {
+ if (form.origin.SchemeIsSecure() &&
+ !observed_form_.origin.SchemeIsSecure()) {
+ // Compare origins, ignoring scheme. There is no easy way to do this
+ // with GURL because clearing the scheme would result in an invalid url.
+ // This is for some sites (such as Hotmail) that begin on an http page and
+ // head to https for the retry when password was invalid.
+ std::string::const_iterator after_scheme1 = form.origin.spec().begin() +
+ form.origin.scheme().length();
+ std::string::const_iterator after_scheme2 =
+ observed_form_.origin.spec().begin() +
+ observed_form_.origin.scheme().length();
+ return std::search(after_scheme1,
+ form.origin.spec().end(),
+ after_scheme2,
+ observed_form_.origin.spec().end())
+ != form.origin.spec().end();
+ }
+ return false;
+ }
+ return true;
+}
+
+bool PasswordFormManager::IsBlacklisted() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ if (preferred_match_ && preferred_match_->blacklisted_by_user)
+ return true;
+ return false;
+}
+
+void PasswordFormManager::PermanentlyBlacklist() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+
+ // Configure the form about to be saved for blacklist status.
+ pending_credentials_.preferred = true;
+ pending_credentials_.blacklisted_by_user = true;
+ pending_credentials_.username_value.clear();
+ pending_credentials_.password_value.clear();
+
+ // Retroactively forget existing matches for this form, so we NEVER prompt or
+ // autofill it again.
+ if (!best_matches_.empty()) {
+ PasswordFormMap::const_iterator iter;
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+ for (iter = best_matches_.begin(); iter != best_matches_.end(); ++iter) {
+ // We want to remove existing matches for this form so that the exact
+ // origin match with |blackisted_by_user == true| is the only result that
+ // shows up in the future for this origin URL. However, we don't want to
+ // delete logins that were actually saved on a different page (hence with
+ // different origin URL) and just happened to match this form because of
+ // the scoring algorithm. See bug 1204493.
+ if (iter->second->origin == observed_form_.origin)
+ web_data_service->RemoveLogin(*iter->second);
+ }
+ }
+
+ // Save the pending_credentials_ entry marked as blacklisted.
+ SaveAsNewLogin();
+}
+
+bool PasswordFormManager::IsNewLogin() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ return is_new_login_;
+}
+
+void PasswordFormManager::ProvisionallySave(const PasswordForm& credentials) {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ DCHECK(DoesManage(credentials));
+
+ // Make sure the important fields stay the same as the initially observed or
+ // autofilled ones, as they may have changed if the user experienced a login
+ // failure.
+ // Look for these credentials in the list containing auto-fill entries.
+ PasswordFormMap::const_iterator it =
+ best_matches_.find(credentials.username_value);
+ if (it != best_matches_.end()) {
+ // The user signed in with a login we autofilled.
+ pending_credentials_ = *it->second;
+ is_new_login_ = false;
+ // If the user selected credentials we autofilled from a PasswordForm
+ // that contained no action URL (IE6/7 imported passwords, for example),
+ // bless it with the action URL from the observed form. See bug 1107719.
+ if (pending_credentials_.action.is_empty())
+ pending_credentials_.action = observed_form_.action;
+ } else {
+ pending_credentials_ = observed_form_;
+ pending_credentials_.username_value = credentials.username_value;
+ }
+
+ pending_credentials_.password_value = credentials.password_value;
+ pending_credentials_.preferred = credentials.preferred;
+}
+
+void PasswordFormManager::Save() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ DCHECK(!profile_->IsOffTheRecord());
+
+ if (IsNewLogin())
+ SaveAsNewLogin();
+ else
+ UpdateLogin();
+}
+
+void PasswordFormManager::FetchMatchingLoginsFromWebDatabase() {
+ DCHECK_EQ(state_, PRE_MATCHING_PHASE);
+ DCHECK(!pending_login_query_);
+ state_ = MATCHING_PHASE;
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+ pending_login_query_ = web_data_service->GetLogins(observed_form_, this);
+}
+
+void PasswordFormManager::FetchMatchingIE7LoginFromWebDatabase() {
+ DCHECK_EQ(state_, PRE_MATCHING_PHASE);
+ DCHECK(!pending_login_query_);
+ state_ = MATCHING_PHASE;
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+
+ IE7PasswordInfo info;
+ std::wstring url = ASCIIToWide(observed_form_.origin.spec());
+ info.url_hash = ie7_password::GetUrlHash(url);
+ pending_login_query_ = web_data_service->GetIE7Login(info, this);
+}
+
+bool PasswordFormManager::HasCompletedMatching() {
+ return state_ == POST_MATCHING_PHASE;
+}
+
+void PasswordFormManager::OnRequestDone(WebDataService::Handle h,
+ const WDTypedResult* result) {
+ // Get the result from the database into a usable form.
+ const WDResult<std::vector<PasswordForm*> >* r =
+ static_cast<const WDResult<std::vector<PasswordForm*> >*>(result);
+ std::vector<PasswordForm*> logins_result = r->GetValue();
+ // Note that the result gets deleted after this call completes, but we own
+ // the PasswordForm objects pointed to by the result vector, thus we keep
+ // copies to a minimum here.
+
+ int best_score = 0;
+ std::vector<PasswordForm> empties; // Empty-path matches in result set.
+ for (size_t i = 0; i < logins_result.size(); i++) {
+ if (IgnoreResult(*logins_result[i])) {
+ delete logins_result[i];
+ continue;
+ }
+ // Score and update best matches.
+ int current_score = ScoreResult(*logins_result[i]);
+ // This check is here so we can append empty path matches in the event
+ // they don't score as high as others and aren't added to best_matches_.
+ // This is most commonly imported firefox logins. We skip blacklisted
+ // ones because clearly we don't want to autofill them, and secondly
+ // because they only mean something when we have no other matches already
+ // saved in Chrome - in which case they'll make it through the regular
+ // scoring flow below by design. Note signon_realm == origin implies empty
+ // path logins_result, since signon_realm is a prefix of origin for HTML
+ // password forms.
+ // TODO(timsteele): Bug 1269400. We probably should do something more
+ // elegant for any shorter-path match instead of explicitly handling empty
+ // path matches.
+ if ((observed_form_.scheme == PasswordForm::SCHEME_HTML) &&
+ (observed_form_.signon_realm == logins_result[i]->origin.spec()) &&
+ (current_score > 0) && (!logins_result[i]->blacklisted_by_user)){
+ empties.push_back(*logins_result[i]);
+ }
+
+ if (current_score < best_score) {
+ delete logins_result[i];
+ continue;
+ }
+ if (current_score == best_score) {
+ best_matches_[logins_result[i]->username_value] = logins_result[i];
+ } else if (current_score > best_score) {
+ best_score = current_score;
+ // This new login has a better score than all those up to this point
+ // Note 'this' owns all the PasswordForms in best_matches_.
+ STLDeleteValues(&best_matches_);
+ best_matches_.clear();
+ preferred_match_ = NULL; // Don't delete, its owned by best_matches_.
+ best_matches_[logins_result[i]->username_value] = logins_result[i];
+ }
+ preferred_match_ = logins_result[i]->preferred ? logins_result[i]
+ : preferred_match_;
+ }
+ // We're done matching now.
+ state_ = POST_MATCHING_PHASE;
+
+ if (best_score <= 0) {
+ state_ = PRE_MATCHING_PHASE;
+ FetchMatchingIE7LoginFromWebDatabase();
+ return;
+ }
+
+ for (std::vector<PasswordForm>::const_iterator it = empties.begin();
+ it != empties.end(); ++it) {
+ // If we don't already have a result with the same username, add the
+ // lower-scored empty-path match (if it had equal score it would already be
+ // in best_matches_).
+ if (best_matches_.find(it->username_value) == best_matches_.end())
+ best_matches_[it->username_value] = new PasswordForm(*it);
+ }
+
+ // Its possible we have at least one match but have no preferred_match_,
+ // because a user may have chosen to 'Forget' the preferred match. So we
+ // just pick the first one and whichever the user selects for submit will
+ // be saved as preferred.
+ DCHECK(!best_matches_.empty());
+ if (!preferred_match_)
+ preferred_match_ = best_matches_.begin()->second;
+
+ // Now we determine if the user told us to ignore this site in the past.
+ // If they haven't, we proceed to auto-fill.
+ if (!preferred_match_->blacklisted_by_user) {
+ password_manager_->Autofill(observed_form_, best_matches_,
+ preferred_match_);
+ }
+}
+
+void PasswordFormManager::OnIE7RequestDone(WebDataService::Handle h,
+ const WDTypedResult* result) {
+ // Get the result from the database into a usable form.
+ const WDResult<IE7PasswordInfo>* r =
+ static_cast<const WDResult<IE7PasswordInfo>*>(result);
+ IE7PasswordInfo result_value = r->GetValue();
+
+ state_ = POST_MATCHING_PHASE;
+
+ if (!result_value.encrypted_data.empty()) {
+ // We got a result.
+ // Delete the entry. If it's good we will add it to the real saved password
+ // table.
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+ web_data_service->RemoveIE7Login(result_value);
+
+ std::wstring username;
+ std::wstring password;
+ std::wstring url = ASCIIToWide(observed_form_.origin.spec());
+ if (!ie7_password::DecryptPassword(url, result_value.encrypted_data,
+ &username, &password)) {
+ return;
+ }
+
+ PasswordForm* auto_fill = new PasswordForm(observed_form_);
+ auto_fill->username_value = username;
+ auto_fill->password_value = password;
+ auto_fill->preferred = true;
+ auto_fill->ssl_valid = observed_form_.origin.SchemeIsSecure();
+ auto_fill->date_created = result_value.date_created;
+ // Add this PasswordForm to the saved password table.
+ web_data_service->AddLogin(*auto_fill);
+
+ if (IgnoreResult(*auto_fill)) {
+ delete auto_fill;
+ return;
+ }
+
+ best_matches_[auto_fill->username_value] = auto_fill;
+ preferred_match_ = auto_fill;
+ password_manager_->Autofill(observed_form_, best_matches_,
+ preferred_match_);
+ }
+}
+
+void PasswordFormManager::OnWebDataServiceRequestDone(WebDataService::Handle h,
+ const WDTypedResult* result) {
+ DCHECK_EQ(state_, MATCHING_PHASE);
+ DCHECK_EQ(pending_login_query_, h);
+ DCHECK(result);
+ pending_login_query_ = NULL;
+
+ if (!result)
+ return;
+
+ switch (result->GetType()) {
+ case PASSWORD_RESULT: {
+ OnRequestDone(h, result);
+ break;
+ }
+ case PASSWORD_IE7_RESULT: {
+ OnIE7RequestDone(h, result);
+ break;
+ }
+ default:
+ NOTREACHED();
+ }
+}
+
+bool PasswordFormManager::IgnoreResult(const PasswordForm& form) const {
+ // Ignore change password forms until we have some change password
+ // functionality
+ if (observed_form_.old_password_element.length() != 0) {
+ return true;
+ }
+ // Don't match an invalid SSL form with one saved under secure
+ // circumstances.
+ if (form.ssl_valid && !observed_form_.ssl_valid) {
+ return true;
+ }
+ return false;
+}
+
+void PasswordFormManager::SaveAsNewLogin() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ DCHECK(IsNewLogin());
+ // The new_form is being used to sign in, so it is preferred.
+ DCHECK(pending_credentials_.preferred);
+ // new_form contains the same basic data as observed_form_ (because its the
+ // same form), but with the newly added credentials.
+
+ DCHECK(!profile_->IsOffTheRecord());
+
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::IMPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+ pending_credentials_.date_created = Time::Now();
+ web_data_service->AddLogin(pending_credentials_);
+}
+
+void PasswordFormManager::UpdateLogin() {
+ DCHECK_EQ(state_, POST_MATCHING_PHASE);
+ DCHECK(preferred_match_);
+ // If we're doing an Update, its because we autofilled a form and the user
+ // submitted it with a possibly new password value, page security, or selected
+ // one of the non-preferred matches, thus requiring a swap of preferred bits.
+ DCHECK(!IsNewLogin() && pending_credentials_.preferred);
+ DCHECK(!profile_->IsOffTheRecord());
+
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::IMPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+
+ // Update all matches to reflect new preferred status.
+ PasswordFormMap::iterator iter;
+ for (iter = best_matches_.begin(); iter != best_matches_.end(); iter++) {
+ if ((iter->second->username_value != pending_credentials_.username_value) &&
+ iter->second->preferred) {
+ // This wasn't the selected login but it used to be preferred.
+ iter->second->preferred = false;
+ web_data_service->UpdateLogin(*iter->second);
+ }
+ }
+ // Update the new preferred login.
+ // Note origin.spec().length > signon_realm.length implies the origin has a
+ // path, since signon_realm is a prefix of origin for HTML password forms.
+ if ((observed_form_.scheme == PasswordForm::SCHEME_HTML) &&
+ (observed_form_.origin.spec().length() >
+ observed_form_.signon_realm.length()) &&
+ (observed_form_.signon_realm == pending_credentials_.origin.spec())) {
+ // The user logged in successfully with one of our autofilled logins on a
+ // page with non-empty path, but the autofilled entry was initially saved/
+ // imported with an empty path. Rather than just mark this entry preferred,
+ // we create a more specific copy for this exact page and leave the "master"
+ // unchanged. This is to prevent the case where that master login is used
+ // on several sites (e.g site.com/a and site.com/b) but the user actually
+ // has a different preference on each site. For example, on /a, he wants the
+ // general empty-path login so it is flagged as preferred, but on /b he logs
+ // in with a different saved entry - we don't want to remove the preferred
+ // status of the former because upon return to /a it won't be the default-
+ // fill match.
+ // TODO(timsteele): Bug 1188626 - expire the master copies.
+ PasswordForm copy(pending_credentials_);
+ copy.origin = observed_form_.origin;
+ copy.action = observed_form_.action;
+ web_data_service->AddLogin(copy);
+ } else {
+ web_data_service->UpdateLogin(pending_credentials_);
+ }
+}
+
+void PasswordFormManager::CancelLoginsQuery() {
+ if (!pending_login_query_)
+ return;
+ WebDataService* web_data_service =
+ profile_->GetWebDataService(Profile::EXPLICIT_ACCESS);
+ if (!web_data_service) {
+ NOTREACHED();
+ return;
+ }
+ web_data_service->CancelRequest(pending_login_query_);
+ pending_login_query_ = NULL;
+}
+
+int PasswordFormManager::ScoreResult(const PasswordForm& candidate) const {
+ DCHECK_EQ(state_, MATCHING_PHASE);
+ // For scoring of candidate login data:
+ // The most important element that should match is the origin, followed by
+ // the action, the password name, the submit button name, and finally the
+ // username input field name.
+ // Exact origin match gives an addition of 32 (1 << 5) + # of matching url
+ // dirs.
+ // Partial match gives an addition of 16 (1 << 4) + # matching url dirs
+ // That way, a partial match cannot trump an exact match even if
+ // the partial one matches all other attributes (action, elements) (and
+ // regardless of the matching depth in the URL path).
+ int score = 0;
+ if (candidate.origin == observed_form_.origin) {
+ // This check is here for the most common case which
+ // is we have a single match in the db for the given host,
+ // so we don't generally need to walk the entire URL path (the else
+ // clause).
+ score += (1 << 5) + static_cast<int>(form_path_tokens_.size());
+ } else {
+ // Walk the origin URL paths one directory at a time to see how
+ // deep the two match.
+ std::vector<std::string> candidate_path_tokens;
+ SplitString(candidate.origin.path(), '/', &candidate_path_tokens);
+ size_t depth = 0;
+ size_t max_dirs = std::min(form_path_tokens_.size(),
+ candidate_path_tokens.size());
+ while ((depth < max_dirs) && (form_path_tokens_[depth] ==
+ candidate_path_tokens[depth])) {
+ depth++;
+ score++;
+ }
+ // do we have a partial match?
+ score += (depth > 0) ? 1 << 4 : 0;
+ }
+ if (observed_form_.scheme == PasswordForm::SCHEME_HTML) {
+ if (candidate.action == observed_form_.action)
+ score += 1 << 3;
+ if (candidate.password_element == observed_form_.password_element)
+ score += 1 << 2;
+ if (candidate.submit_element == observed_form_.submit_element)
+ score += 1 << 1;
+ if (candidate.username_element == observed_form_.username_element)
+ score += 1 << 0;
+ }
+
+ return score;
+}