| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "base/basictypes.h" | 5 #include "base/basictypes.h" |
| 6 #include "base/bind.h" | 6 #include "base/bind.h" |
| 7 #include "base/bind_helpers.h" | 7 #include "base/bind_helpers.h" |
| 8 #include "base/scoped_temp_dir.h" | 8 #include "base/scoped_temp_dir.h" |
| 9 #include "base/stl_util.h" | 9 #include "base/stl_util.h" |
| 10 #include "base/string_util.h" | 10 #include "base/string_util.h" |
| 11 #include "base/synchronization/waitable_event.h" | 11 #include "base/synchronization/waitable_event.h" |
| 12 #include "base/time.h" | 12 #include "base/time.h" |
| 13 #include "base/utf_string_conversions.h" | 13 #include "base/utf_string_conversions.h" |
| 14 #include "chrome/browser/password_manager/password_form_data.h" | 14 #include "chrome/browser/password_manager/password_form_data.h" |
| 15 #include "chrome/browser/password_manager/password_store_change.h" | 15 #include "chrome/browser/password_manager/password_store_change.h" |
| 16 #include "chrome/browser/password_manager/password_store_consumer.h" | 16 #include "chrome/browser/password_manager/password_store_consumer.h" |
| 17 #include "chrome/browser/password_manager/password_store_default.h" | 17 #include "chrome/browser/password_manager/password_store_default.h" |
| 18 #include "chrome/browser/prefs/pref_service.h" | 18 #include "chrome/browser/prefs/pref_service.h" |
| 19 #include "chrome/browser/webdata/web_data_service.h" | |
| 20 #include "chrome/common/chrome_notification_types.h" | 19 #include "chrome/common/chrome_notification_types.h" |
| 21 #include "chrome/common/pref_names.h" | 20 #include "chrome/common/pref_names.h" |
| 22 #include "chrome/test/base/testing_profile.h" | 21 #include "chrome/test/base/testing_profile.h" |
| 23 #include "content/public/browser/notification_details.h" | 22 #include "content/public/browser/notification_details.h" |
| 24 #include "content/public/browser/notification_registrar.h" | 23 #include "content/public/browser/notification_registrar.h" |
| 25 #include "content/public/browser/notification_source.h" | 24 #include "content/public/browser/notification_source.h" |
| 26 #include "content/test/notification_observer_mock.h" | 25 #include "content/test/notification_observer_mock.h" |
| 27 #include "content/test/test_browser_thread.h" | 26 #include "content/test/test_browser_thread.h" |
| 28 #include "testing/gmock/include/gmock/gmock.h" | 27 #include "testing/gmock/include/gmock/gmock.h" |
| 29 #include "testing/gtest/include/gtest/gtest.h" | 28 #include "testing/gtest/include/gtest/gtest.h" |
| (...skipping 10 matching lines...) Expand all Loading... |
| 40 | 39 |
| 41 namespace { | 40 namespace { |
| 42 | 41 |
| 43 class MockPasswordStoreConsumer : public PasswordStoreConsumer { | 42 class MockPasswordStoreConsumer : public PasswordStoreConsumer { |
| 44 public: | 43 public: |
| 45 MOCK_METHOD2(OnPasswordStoreRequestDone, | 44 MOCK_METHOD2(OnPasswordStoreRequestDone, |
| 46 void(CancelableRequestProvider::Handle, | 45 void(CancelableRequestProvider::Handle, |
| 47 const std::vector<webkit::forms::PasswordForm*>&)); | 46 const std::vector<webkit::forms::PasswordForm*>&)); |
| 48 }; | 47 }; |
| 49 | 48 |
| 50 class MockWebDataServiceConsumer : public WebDataServiceConsumer { | |
| 51 public: | |
| 52 MOCK_METHOD2(OnWebDataServiceRequestDone, void(WebDataService::Handle, | |
| 53 const WDTypedResult*)); | |
| 54 }; | |
| 55 | |
| 56 // This class will add and remove a mock notification observer from | 49 // This class will add and remove a mock notification observer from |
| 57 // the DB thread. | 50 // the DB thread. |
| 58 class DBThreadObserverHelper : | 51 class DBThreadObserverHelper : |
| 59 public base::RefCountedThreadSafe<DBThreadObserverHelper, | 52 public base::RefCountedThreadSafe<DBThreadObserverHelper, |
| 60 BrowserThread::DeleteOnDBThread> { | 53 BrowserThread::DeleteOnDBThread> { |
| 61 public: | 54 public: |
| 62 DBThreadObserverHelper() : done_event_(true, false) {} | 55 DBThreadObserverHelper() : done_event_(true, false) {} |
| 63 | 56 |
| 64 void Init(PasswordStore* password_store) { | 57 void Init(PasswordStore* password_store) { |
| 65 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 58 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 110 | 103 |
| 111 virtual void SetUp() { | 104 virtual void SetUp() { |
| 112 ASSERT_TRUE(db_thread_.Start()); | 105 ASSERT_TRUE(db_thread_.Start()); |
| 113 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); | 106 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| 114 | 107 |
| 115 profile_.reset(new TestingProfile()); | 108 profile_.reset(new TestingProfile()); |
| 116 | 109 |
| 117 login_db_.reset(new LoginDatabase()); | 110 login_db_.reset(new LoginDatabase()); |
| 118 ASSERT_TRUE(login_db_->Init(temp_dir_.path().Append( | 111 ASSERT_TRUE(login_db_->Init(temp_dir_.path().Append( |
| 119 FILE_PATH_LITERAL("login_test")))); | 112 FILE_PATH_LITERAL("login_test")))); |
| 120 | |
| 121 wds_ = new WebDataService(); | |
| 122 ASSERT_TRUE(wds_->Init(temp_dir_.path())); | |
| 123 } | 113 } |
| 124 | 114 |
| 125 virtual void TearDown() { | 115 virtual void TearDown() { |
| 126 wds_->Shutdown(); | |
| 127 MessageLoop::current()->PostTask(FROM_HERE, MessageLoop::QuitClosure()); | 116 MessageLoop::current()->PostTask(FROM_HERE, MessageLoop::QuitClosure()); |
| 128 MessageLoop::current()->Run(); | 117 MessageLoop::current()->Run(); |
| 129 db_thread_.Stop(); | 118 db_thread_.Stop(); |
| 130 } | 119 } |
| 131 | 120 |
| 132 MessageLoopForUI message_loop_; | 121 MessageLoopForUI message_loop_; |
| 133 content::TestBrowserThread ui_thread_; | 122 content::TestBrowserThread ui_thread_; |
| 134 // PasswordStore, WDS schedule work on this thread. | 123 // PasswordStore, WDS schedule work on this thread. |
| 135 content::TestBrowserThread db_thread_; | 124 content::TestBrowserThread db_thread_; |
| 136 | 125 |
| 137 scoped_ptr<LoginDatabase> login_db_; | 126 scoped_ptr<LoginDatabase> login_db_; |
| 138 scoped_ptr<TestingProfile> profile_; | 127 scoped_ptr<TestingProfile> profile_; |
| 139 scoped_refptr<WebDataService> wds_; | |
| 140 ScopedTempDir temp_dir_; | 128 ScopedTempDir temp_dir_; |
| 141 }; | 129 }; |
| 142 | 130 |
| 143 ACTION(STLDeleteElements0) { | 131 ACTION(STLDeleteElements0) { |
| 144 STLDeleteContainerPointers(arg0.begin(), arg0.end()); | 132 STLDeleteContainerPointers(arg0.begin(), arg0.end()); |
| 145 } | 133 } |
| 146 | 134 |
| 147 ACTION(QuitUIMessageLoop) { | 135 ACTION(QuitUIMessageLoop) { |
| 148 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 136 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 149 MessageLoop::current()->Quit(); | 137 MessageLoop::current()->Quit(); |
| 150 } | 138 } |
| 151 | 139 |
| 152 MATCHER(EmptyWDResult, "") { | |
| 153 return static_cast<const WDResult<std::vector<PasswordForm*> >*>( | |
| 154 arg)->GetValue().empty(); | |
| 155 } | |
| 156 | |
| 157 TEST_F(PasswordStoreDefaultTest, NonASCIIData) { | 140 TEST_F(PasswordStoreDefaultTest, NonASCIIData) { |
| 158 // Pretend that the migration has already taken place. | |
| 159 profile_->GetPrefs()->RegisterBooleanPref(prefs::kLoginDatabaseMigrated, | |
| 160 true, | |
| 161 PrefService::UNSYNCABLE_PREF); | |
| 162 | |
| 163 // Initializing the PasswordStore shouldn't trigger a migration. | |
| 164 scoped_refptr<PasswordStoreDefault> store( | 141 scoped_refptr<PasswordStoreDefault> store( |
| 165 new PasswordStoreDefault(login_db_.release(), profile_.get(), | 142 new PasswordStoreDefault(login_db_.release(), profile_.get())); |
| 166 wds_.get())); | |
| 167 store->Init(); | 143 store->Init(); |
| 168 | 144 |
| 169 // Some non-ASCII password form data. | 145 // Some non-ASCII password form data. |
| 170 PasswordFormData form_data[] = { | 146 PasswordFormData form_data[] = { |
| 171 { PasswordForm::SCHEME_HTML, | 147 { PasswordForm::SCHEME_HTML, |
| 172 "http://foo.example.com", | 148 "http://foo.example.com", |
| 173 "http://foo.example.com/origin", | 149 "http://foo.example.com/origin", |
| 174 "http://foo.example.com/action", | 150 "http://foo.example.com/action", |
| 175 L"มีสีสัน", | 151 L"มีสีสัน", |
| 176 L"お元気ですか?", | 152 L"お元気ですか?", |
| (...skipping 29 matching lines...) Expand all Loading... |
| 206 OnPasswordStoreRequestDone(_, | 182 OnPasswordStoreRequestDone(_, |
| 207 ContainsAllPasswordForms(expected_forms))) | 183 ContainsAllPasswordForms(expected_forms))) |
| 208 .WillOnce(DoAll(WithArg<1>(STLDeleteElements0()), QuitUIMessageLoop())); | 184 .WillOnce(DoAll(WithArg<1>(STLDeleteElements0()), QuitUIMessageLoop())); |
| 209 | 185 |
| 210 store->GetAutofillableLogins(&consumer); | 186 store->GetAutofillableLogins(&consumer); |
| 211 MessageLoop::current()->Run(); | 187 MessageLoop::current()->Run(); |
| 212 | 188 |
| 213 STLDeleteElements(&expected_forms); | 189 STLDeleteElements(&expected_forms); |
| 214 } | 190 } |
| 215 | 191 |
| 216 TEST_F(PasswordStoreDefaultTest, Migration) { | 192 TEST_F(PasswordStoreDefaultTest, Notifications) { |
| 217 PasswordFormData autofillable_data[] = { | |
| 218 { PasswordForm::SCHEME_HTML, | |
| 219 "http://foo.example.com", | |
| 220 "http://foo.example.com/origin", | |
| 221 "http://foo.example.com/action", | |
| 222 L"submit_element", | |
| 223 L"username_element", | |
| 224 L"password_element", | |
| 225 L"username_value", | |
| 226 L"password_value", | |
| 227 true, false, 1 }, | |
| 228 { PasswordForm::SCHEME_HTML, | |
| 229 "http://bar.example.com", | |
| 230 "http://bar.example.com/origin", | |
| 231 "http://bar.example.com/action", | |
| 232 L"submit_element", | |
| 233 L"username_element", | |
| 234 L"password_element", | |
| 235 L"username_value", | |
| 236 L"password_value", | |
| 237 true, false, 2 }, | |
| 238 { PasswordForm::SCHEME_HTML, | |
| 239 "http://baz.example.com", | |
| 240 "http://baz.example.com/origin", | |
| 241 "http://baz.example.com/action", | |
| 242 L"submit_element", | |
| 243 L"username_element", | |
| 244 L"password_element", | |
| 245 L"username_value", | |
| 246 L"password_value", | |
| 247 true, false, 3 }, | |
| 248 }; | |
| 249 PasswordFormData blacklisted_data[] = { | |
| 250 { PasswordForm::SCHEME_HTML, | |
| 251 "http://blacklisted.example.com", | |
| 252 "http://blacklisted.example.com/origin", | |
| 253 "http://blacklisted.example.com/action", | |
| 254 L"submit_element", | |
| 255 L"username_element", | |
| 256 L"password_element", | |
| 257 NULL, | |
| 258 NULL, | |
| 259 false, false, 1 }, | |
| 260 { PasswordForm::SCHEME_HTML, | |
| 261 "http://blacklisted2.example.com", | |
| 262 "http://blacklisted2.example.com/origin", | |
| 263 "http://blacklisted2.example.com/action", | |
| 264 L"submit_element", | |
| 265 L"username_element", | |
| 266 L"password_element", | |
| 267 NULL, | |
| 268 NULL, | |
| 269 false, false, 2 }, | |
| 270 }; | |
| 271 | |
| 272 // Build the expected forms vectors and populate the WDS with logins. | |
| 273 VectorOfForms expected_autofillable; | |
| 274 for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(autofillable_data); ++i) { | |
| 275 PasswordForm* form = CreatePasswordFormFromData(autofillable_data[i]); | |
| 276 expected_autofillable.push_back(form); | |
| 277 wds_->AddLogin(*form); | |
| 278 } | |
| 279 VectorOfForms expected_blacklisted; | |
| 280 for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(blacklisted_data); ++i) { | |
| 281 PasswordForm* form = CreatePasswordFormFromData(blacklisted_data[i]); | |
| 282 expected_blacklisted.push_back(form); | |
| 283 wds_->AddLogin(*form); | |
| 284 } | |
| 285 | |
| 286 // The WDS schedules tasks to run on the DB thread so we schedule yet another | |
| 287 // task to notify us that it's safe to carry on with the test. | |
| 288 WaitableEvent done(false, false); | |
| 289 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | |
| 290 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | |
| 291 done.Wait(); | |
| 292 | |
| 293 // Initializing the PasswordStore should trigger a migration. | |
| 294 scoped_refptr<PasswordStore> store( | 193 scoped_refptr<PasswordStore> store( |
| 295 new PasswordStoreDefault(login_db_.release(), | 194 new PasswordStoreDefault(login_db_.release(), profile_.get())); |
| 296 profile_.get(), wds_.get())); | |
| 297 store->Init(); | |
| 298 | |
| 299 // Check that the migration preference has not been initialized; | |
| 300 ASSERT_TRUE(NULL == profile_->GetPrefs()->FindPreference( | |
| 301 prefs::kLoginDatabaseMigrated)); | |
| 302 | |
| 303 // Again, the WDS schedules tasks to run on the DB thread, so schedule a task | |
| 304 // to signal us when it is safe to continue. | |
| 305 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | |
| 306 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | |
| 307 done.Wait(); | |
| 308 | |
| 309 // Let the WDS callbacks proceed so the logins can be migrated. | |
| 310 MessageLoop::current()->RunAllPending(); | |
| 311 | |
| 312 MockPasswordStoreConsumer consumer; | |
| 313 | |
| 314 // Make sure we quit the MessageLoop even if the test fails. | |
| 315 ON_CALL(consumer, OnPasswordStoreRequestDone(_, _)) | |
| 316 .WillByDefault(QuitUIMessageLoop()); | |
| 317 | |
| 318 // The autofillable forms should have been migrated from the WDS to the login | |
| 319 // database. | |
| 320 EXPECT_CALL(consumer, | |
| 321 OnPasswordStoreRequestDone(_, | |
| 322 ContainsAllPasswordForms(expected_autofillable))) | |
| 323 .WillOnce(DoAll(WithArg<1>(STLDeleteElements0()), QuitUIMessageLoop())); | |
| 324 | |
| 325 store->GetAutofillableLogins(&consumer); | |
| 326 MessageLoop::current()->Run(); | |
| 327 | |
| 328 // The blacklisted forms should have been migrated from the WDS to the login | |
| 329 // database. | |
| 330 EXPECT_CALL(consumer, | |
| 331 OnPasswordStoreRequestDone(_, | |
| 332 ContainsAllPasswordForms(expected_blacklisted))) | |
| 333 .WillOnce(DoAll(WithArg<1>(STLDeleteElements0()), QuitUIMessageLoop())); | |
| 334 | |
| 335 store->GetBlacklistLogins(&consumer); | |
| 336 MessageLoop::current()->Run(); | |
| 337 | |
| 338 // Check that the migration updated the migrated preference. | |
| 339 ASSERT_TRUE(profile_->GetPrefs()->GetBoolean(prefs::kLoginDatabaseMigrated)); | |
| 340 | |
| 341 MockWebDataServiceConsumer wds_consumer; | |
| 342 | |
| 343 // No autofillable logins should be left in the WDS. | |
| 344 EXPECT_CALL(wds_consumer, | |
| 345 OnWebDataServiceRequestDone(_, EmptyWDResult())); | |
| 346 | |
| 347 wds_->GetAutofillableLogins(&wds_consumer); | |
| 348 | |
| 349 // Wait for the WDS methods to execute on the DB thread. | |
| 350 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | |
| 351 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | |
| 352 done.Wait(); | |
| 353 | |
| 354 // Handle the callback from the WDS. | |
| 355 MessageLoop::current()->RunAllPending(); | |
| 356 | |
| 357 // Likewise, no blacklisted logins should be left in the WDS. | |
| 358 EXPECT_CALL(wds_consumer, | |
| 359 OnWebDataServiceRequestDone(_, EmptyWDResult())); | |
| 360 | |
| 361 wds_->GetBlacklistLogins(&wds_consumer); | |
| 362 | |
| 363 // Wait for the WDS methods to execute on the DB thread. | |
| 364 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | |
| 365 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | |
| 366 done.Wait(); | |
| 367 | |
| 368 // Handle the callback from the WDS. | |
| 369 MessageLoop::current()->RunAllPending(); | |
| 370 | |
| 371 STLDeleteElements(&expected_autofillable); | |
| 372 STLDeleteElements(&expected_blacklisted); | |
| 373 | |
| 374 store->ShutdownOnUIThread(); | |
| 375 } | |
| 376 | |
| 377 TEST_F(PasswordStoreDefaultTest, MigrationAlreadyDone) { | |
| 378 PasswordFormData wds_data[] = { | |
| 379 { PasswordForm::SCHEME_HTML, | |
| 380 "http://bar.example.com", | |
| 381 "http://bar.example.com/origin", | |
| 382 "http://bar.example.com/action", | |
| 383 L"submit_element", | |
| 384 L"username_element", | |
| 385 L"password_element", | |
| 386 L"username_value", | |
| 387 L"password_value", | |
| 388 true, false, 1 }, | |
| 389 }; | |
| 390 | |
| 391 // Build the expected forms vector and populate the WDS with logins. | |
| 392 VectorOfForms unexpected_autofillable; | |
| 393 for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(wds_data); ++i) { | |
| 394 PasswordForm* form = CreatePasswordFormFromData(wds_data[i]); | |
| 395 unexpected_autofillable.push_back(form); | |
| 396 wds_->AddLogin(*form); | |
| 397 } | |
| 398 | |
| 399 // The WDS schedules tasks to run on the DB thread so we schedule yet another | |
| 400 // task to notify us that it's safe to carry on with the test. | |
| 401 WaitableEvent done(false, false); | |
| 402 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | |
| 403 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | |
| 404 done.Wait(); | |
| 405 | |
| 406 // Pretend that the migration has already taken place. | |
| 407 profile_->GetPrefs()->RegisterBooleanPref(prefs::kLoginDatabaseMigrated, | |
| 408 true, | |
| 409 PrefService::UNSYNCABLE_PREF); | |
| 410 | |
| 411 // Initializing the PasswordStore shouldn't trigger a migration. | |
| 412 scoped_refptr<PasswordStore> store( | |
| 413 new PasswordStoreDefault(login_db_.release(), profile_.get(), | |
| 414 wds_.get())); | |
| 415 store->Init(); | |
| 416 | |
| 417 MockPasswordStoreConsumer consumer; | |
| 418 // Make sure we quit the MessageLoop even if the test fails. | |
| 419 ON_CALL(consumer, OnPasswordStoreRequestDone(_, _)) | |
| 420 .WillByDefault(QuitUIMessageLoop()); | |
| 421 | |
| 422 // No forms should be migrated. | |
| 423 VectorOfForms empty; | |
| 424 EXPECT_CALL(consumer, | |
| 425 OnPasswordStoreRequestDone(_, | |
| 426 ContainsAllPasswordForms(empty))) | |
| 427 .WillOnce(QuitUIMessageLoop()); | |
| 428 | |
| 429 store->GetAutofillableLogins(&consumer); | |
| 430 MessageLoop::current()->Run(); | |
| 431 | |
| 432 STLDeleteElements(&unexpected_autofillable); | |
| 433 | |
| 434 store->ShutdownOnUIThread(); | |
| 435 } | |
| 436 | |
| 437 TEST_F(PasswordStoreDefaultTest, Notifications) { | |
| 438 // Pretend that the migration has already taken place. | |
| 439 profile_->GetPrefs()->RegisterBooleanPref(prefs::kLoginDatabaseMigrated, | |
| 440 true, | |
| 441 PrefService::UNSYNCABLE_PREF); | |
| 442 | |
| 443 // Initializing the PasswordStore shouldn't trigger a migration. | |
| 444 scoped_refptr<PasswordStore> store( | |
| 445 new PasswordStoreDefault(login_db_.release(), profile_.get(), | |
| 446 wds_.get())); | |
| 447 store->Init(); | 195 store->Init(); |
| 448 | 196 |
| 449 PasswordFormData form_data = | 197 PasswordFormData form_data = |
| 450 { PasswordForm::SCHEME_HTML, | 198 { PasswordForm::SCHEME_HTML, |
| 451 "http://bar.example.com", | 199 "http://bar.example.com", |
| 452 "http://bar.example.com/origin", | 200 "http://bar.example.com/origin", |
| 453 "http://bar.example.com/action", | 201 "http://bar.example.com/action", |
| 454 L"submit_element", | 202 L"submit_element", |
| 455 L"username_element", | 203 L"username_element", |
| 456 L"password_element", | 204 L"password_element", |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 519 // Deleting the login should trigger a notification. | 267 // Deleting the login should trigger a notification. |
| 520 store->RemoveLogin(*form); | 268 store->RemoveLogin(*form); |
| 521 | 269 |
| 522 // Wait for PasswordStore to send the notification. | 270 // Wait for PasswordStore to send the notification. |
| 523 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, | 271 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, |
| 524 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); | 272 base::Bind(&WaitableEvent::Signal, base::Unretained(&done))); |
| 525 done.Wait(); | 273 done.Wait(); |
| 526 | 274 |
| 527 store->ShutdownOnUIThread(); | 275 store->ShutdownOnUIThread(); |
| 528 } | 276 } |
| OLD | NEW |