1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
|
// Copyright (c) 2006-2008 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/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"
using base::Time;
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;
}
|