| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "chrome/browser/password_manager/password_store_gnome.h" | |
| 6 | |
| 7 #if defined(DLOPEN_GNOME_KEYRING) | |
| 8 #include <dlfcn.h> | |
| 9 #endif | |
| 10 | |
| 11 #include <map> | |
| 12 #include <string> | |
| 13 | |
| 14 #include "base/logging.h" | |
| 15 #include "base/string_util.h" | |
| 16 #include "base/task.h" | |
| 17 #include "base/time.h" | |
| 18 #include "base/utf_string_conversions.h" | |
| 19 | |
| 20 using std::map; | |
| 21 using std::string; | |
| 22 using std::vector; | |
| 23 using webkit_glue::PasswordForm; | |
| 24 | |
| 25 /* Many of the gnome_keyring_* functions use variable arguments, which makes | |
| 26 * them difficult if not impossible to wrap in C. Therefore, we want the | |
| 27 * actual uses below to either call the functions directly (if we are linking | |
| 28 * against libgnome-keyring), or call them via appropriately-typed function | |
| 29 * pointers (if we are dynamically loading libgnome-keyring). | |
| 30 * | |
| 31 * Thus, instead of making a wrapper class with two implementations, we use | |
| 32 * the preprocessor to rename the calls below in the dynamic load case, and | |
| 33 * provide a function to initialize a set of function pointers that have the | |
| 34 * alternate names. We also make sure the types are correct, since otherwise | |
| 35 * dynamic loading like this would leave us vulnerable to signature changes. */ | |
| 36 | |
| 37 #if defined(DLOPEN_GNOME_KEYRING) | |
| 38 | |
| 39 namespace { | |
| 40 | |
| 41 gboolean (*wrap_gnome_keyring_is_available)(); | |
| 42 GnomeKeyringResult (*wrap_gnome_keyring_store_password_sync)( // NOLINT | |
| 43 const GnomeKeyringPasswordSchema* schema, const gchar* keyring, | |
| 44 const gchar* display_name, const gchar* password, ...); | |
| 45 GnomeKeyringResult (*wrap_gnome_keyring_delete_password_sync)( // NOLINT | |
| 46 const GnomeKeyringPasswordSchema* schema, ...); | |
| 47 GnomeKeyringResult (*wrap_gnome_keyring_find_itemsv_sync)( // NOLINT | |
| 48 GnomeKeyringItemType type, GList** found, ...); | |
| 49 const gchar* (*wrap_gnome_keyring_result_to_message)(GnomeKeyringResult res); | |
| 50 void (*wrap_gnome_keyring_found_list_free)(GList* found_list); | |
| 51 | |
| 52 /* Cause the compiler to complain if the types of the above function pointers | |
| 53 * do not correspond to the types of the actual gnome_keyring_* functions. */ | |
| 54 #define GNOME_KEYRING_VERIFY_TYPE(name) \ | |
| 55 typeof(&gnome_keyring_##name) name = wrap_gnome_keyring_##name; name = name | |
| 56 | |
| 57 inline void VerifyGnomeKeyringTypes() { | |
| 58 GNOME_KEYRING_VERIFY_TYPE(is_available); | |
| 59 GNOME_KEYRING_VERIFY_TYPE(store_password_sync); | |
| 60 GNOME_KEYRING_VERIFY_TYPE(delete_password_sync); | |
| 61 GNOME_KEYRING_VERIFY_TYPE(find_itemsv_sync); | |
| 62 GNOME_KEYRING_VERIFY_TYPE(result_to_message); | |
| 63 GNOME_KEYRING_VERIFY_TYPE(found_list_free); | |
| 64 } | |
| 65 #undef GNOME_KEYRING_VERIFY_TYPE | |
| 66 | |
| 67 /* Make it easy to initialize the function pointers above with a loop below. */ | |
| 68 #define GNOME_KEYRING_FUNCTION(name) \ | |
| 69 {#name, reinterpret_cast<void**>(&wrap_##name)} | |
| 70 const struct { | |
| 71 const char* name; | |
| 72 void** pointer; | |
| 73 } gnome_keyring_functions[] = { | |
| 74 GNOME_KEYRING_FUNCTION(gnome_keyring_is_available), | |
| 75 GNOME_KEYRING_FUNCTION(gnome_keyring_store_password_sync), | |
| 76 GNOME_KEYRING_FUNCTION(gnome_keyring_delete_password_sync), | |
| 77 GNOME_KEYRING_FUNCTION(gnome_keyring_find_itemsv_sync), | |
| 78 GNOME_KEYRING_FUNCTION(gnome_keyring_result_to_message), | |
| 79 GNOME_KEYRING_FUNCTION(gnome_keyring_found_list_free), | |
| 80 {NULL, NULL} | |
| 81 }; | |
| 82 #undef GNOME_KEYRING_FUNCTION | |
| 83 | |
| 84 /* Allow application code below to use the normal function names, but actually | |
| 85 * end up using the function pointers above instead. */ | |
| 86 #define gnome_keyring_is_available \ | |
| 87 wrap_gnome_keyring_is_available | |
| 88 #define gnome_keyring_store_password_sync \ | |
| 89 wrap_gnome_keyring_store_password_sync | |
| 90 #define gnome_keyring_delete_password_sync \ | |
| 91 wrap_gnome_keyring_delete_password_sync | |
| 92 #define gnome_keyring_find_itemsv_sync \ | |
| 93 wrap_gnome_keyring_find_itemsv_sync | |
| 94 #define gnome_keyring_result_to_message \ | |
| 95 wrap_gnome_keyring_result_to_message | |
| 96 #define gnome_keyring_found_list_free \ | |
| 97 wrap_gnome_keyring_found_list_free | |
| 98 | |
| 99 /* Load the library and initialize the function pointers. */ | |
| 100 bool LoadGnomeKeyring() { | |
| 101 void* handle = dlopen("libgnome-keyring.so.0", RTLD_NOW | RTLD_GLOBAL); | |
| 102 if (!handle) { | |
| 103 LOG(INFO) << "Could not find libgnome-keyring.so.0"; | |
| 104 return false; | |
| 105 } | |
| 106 for (size_t i = 0; gnome_keyring_functions[i].name; ++i) { | |
| 107 dlerror(); | |
| 108 *gnome_keyring_functions[i].pointer = | |
| 109 dlsym(handle, gnome_keyring_functions[i].name); | |
| 110 const char* error = dlerror(); | |
| 111 if (error) { | |
| 112 LOG(ERROR) << "Unable to load symbol " << | |
| 113 gnome_keyring_functions[i].name << ": " << error; | |
| 114 dlclose(handle); | |
| 115 return false; | |
| 116 } | |
| 117 } | |
| 118 // We leak the library handle. That's OK: this function is called only once. | |
| 119 return true; | |
| 120 } | |
| 121 | |
| 122 } // namespace | |
| 123 | |
| 124 #else // DLOPEN_GNOME_KEYRING | |
| 125 | |
| 126 namespace { | |
| 127 | |
| 128 bool LoadGnomeKeyring() { | |
| 129 return true; | |
| 130 } | |
| 131 | |
| 132 } // namespace | |
| 133 | |
| 134 #endif // DLOPEN_GNOME_KEYRING | |
| 135 | |
| 136 #define GNOME_KEYRING_APPLICATION_CHROME "chrome" | |
| 137 | |
| 138 // Schema is analagous to the fields in PasswordForm. | |
| 139 const GnomeKeyringPasswordSchema PasswordStoreGnome::kGnomeSchema = { | |
| 140 GNOME_KEYRING_ITEM_GENERIC_SECRET, { | |
| 141 { "origin_url", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 142 { "action_url", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 143 { "username_element", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 144 { "username_value", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 145 { "password_element", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 146 { "submit_element", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 147 { "signon_realm", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 148 { "ssl_valid", GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 }, | |
| 149 { "preferred", GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 }, | |
| 150 { "date_created", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 151 { "blacklisted_by_user", GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 }, | |
| 152 { "scheme", GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 }, | |
| 153 // This field is always "chrome" so that we can search for it. | |
| 154 { "application", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING }, | |
| 155 { NULL } | |
| 156 } | |
| 157 }; | |
| 158 | |
| 159 PasswordStoreGnome::PasswordStoreGnome(LoginDatabase* login_db, | |
| 160 Profile* profile, | |
| 161 WebDataService* web_data_service) { | |
| 162 } | |
| 163 | |
| 164 PasswordStoreGnome::~PasswordStoreGnome() { | |
| 165 } | |
| 166 | |
| 167 bool PasswordStoreGnome::Init() { | |
| 168 return PasswordStore::Init() && | |
| 169 LoadGnomeKeyring() && | |
| 170 gnome_keyring_is_available(); | |
| 171 } | |
| 172 | |
| 173 void PasswordStoreGnome::AddLoginImpl(const PasswordForm& form) { | |
| 174 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 175 AddLoginHelper(form, base::Time::Now()); | |
| 176 } | |
| 177 | |
| 178 void PasswordStoreGnome::UpdateLoginImpl(const PasswordForm& form) { | |
| 179 // Based on LoginDatabase::UpdateLogin(), we search for forms to update by | |
| 180 // origin_url, username_element, username_value, password_element, and | |
| 181 // signon_realm. We then compare the result to the updated form. If they | |
| 182 // differ in any of the action, password_value, ssl_valid, or preferred | |
| 183 // fields, then we add a new login with those fields updated and only delete | |
| 184 // the original on success. | |
| 185 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 186 GList* found = NULL; | |
| 187 // Search gnome keyring for matching passwords. | |
| 188 GnomeKeyringResult result = gnome_keyring_find_itemsv_sync( | |
| 189 GNOME_KEYRING_ITEM_GENERIC_SECRET, | |
| 190 &found, | |
| 191 "origin_url", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 192 form.origin.spec().c_str(), | |
| 193 "username_element", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 194 UTF16ToUTF8(form.username_element).c_str(), | |
| 195 "username_value", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 196 UTF16ToUTF8(form.username_value).c_str(), | |
| 197 "password_element", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 198 UTF16ToUTF8(form.password_element).c_str(), | |
| 199 "signon_realm", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 200 form.signon_realm.c_str(), | |
| 201 "application", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 202 GNOME_KEYRING_APPLICATION_CHROME, | |
| 203 NULL); | |
| 204 vector<PasswordForm*> forms; | |
| 205 if (result == GNOME_KEYRING_RESULT_OK) { | |
| 206 FillFormVector(found, &forms); | |
| 207 for (size_t i = 0; i < forms.size(); ++i) { | |
| 208 if (forms[i]->action != form.action || | |
| 209 forms[i]->password_value != form.password_value || | |
| 210 forms[i]->ssl_valid != form.ssl_valid || | |
| 211 forms[i]->preferred != form.preferred) { | |
| 212 PasswordForm updated = *forms[i]; | |
| 213 updated.action = form.action; | |
| 214 updated.password_value = form.password_value; | |
| 215 updated.ssl_valid = form.ssl_valid; | |
| 216 updated.preferred = form.preferred; | |
| 217 if (AddLoginHelper(updated, updated.date_created)) | |
| 218 RemoveLoginImpl(*forms[i]); | |
| 219 } | |
| 220 delete forms[i]; | |
| 221 } | |
| 222 } else { | |
| 223 LOG(ERROR) << "Keyring find failed: " | |
| 224 << gnome_keyring_result_to_message(result); | |
| 225 } | |
| 226 } | |
| 227 | |
| 228 void PasswordStoreGnome::RemoveLoginImpl(const PasswordForm& form) { | |
| 229 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 230 // We find forms using the same fields as LoginDatabase::RemoveLogin(). | |
| 231 GnomeKeyringResult result = gnome_keyring_delete_password_sync( | |
| 232 &kGnomeSchema, | |
| 233 "origin_url", form.origin.spec().c_str(), | |
| 234 "action_url", form.action.spec().c_str(), | |
| 235 "username_element", UTF16ToUTF8(form.username_element).c_str(), | |
| 236 "username_value", UTF16ToUTF8(form.username_value).c_str(), | |
| 237 "password_element", UTF16ToUTF8(form.password_element).c_str(), | |
| 238 "submit_element", UTF16ToUTF8(form.submit_element).c_str(), | |
| 239 "signon_realm", form.signon_realm.c_str(), | |
| 240 NULL); | |
| 241 if (result != GNOME_KEYRING_RESULT_OK) { | |
| 242 LOG(ERROR) << "Keyring delete failed: " | |
| 243 << gnome_keyring_result_to_message(result); | |
| 244 } | |
| 245 } | |
| 246 | |
| 247 void PasswordStoreGnome::RemoveLoginsCreatedBetweenImpl( | |
| 248 const base::Time& delete_begin, | |
| 249 const base::Time& delete_end) { | |
| 250 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 251 GList* found = NULL; | |
| 252 // Search GNOME keyring for all passwords, then delete the ones in the range. | |
| 253 // We need to search for something, otherwise we get no results - so we search | |
| 254 // for the fixed application string. | |
| 255 GnomeKeyringResult result = gnome_keyring_find_itemsv_sync( | |
| 256 GNOME_KEYRING_ITEM_GENERIC_SECRET, | |
| 257 &found, | |
| 258 "application", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 259 GNOME_KEYRING_APPLICATION_CHROME, | |
| 260 NULL); | |
| 261 if (result == GNOME_KEYRING_RESULT_OK) { | |
| 262 // We could walk the list and delete items as we find them, but it is much | |
| 263 // easier to build the vector and use RemoveLoginImpl() to delete them. | |
| 264 vector<PasswordForm*> forms; | |
| 265 FillFormVector(found, &forms); | |
| 266 for (size_t i = 0; i < forms.size(); ++i) { | |
| 267 if (delete_begin <= forms[i]->date_created && | |
| 268 (delete_end.is_null() || forms[i]->date_created < delete_end)) { | |
| 269 RemoveLoginImpl(*forms[i]); | |
| 270 } | |
| 271 delete forms[i]; | |
| 272 } | |
| 273 } else if (result != GNOME_KEYRING_RESULT_NO_MATCH) { | |
| 274 LOG(ERROR) << "Keyring find failed: " | |
| 275 << gnome_keyring_result_to_message(result); | |
| 276 } | |
| 277 } | |
| 278 | |
| 279 void PasswordStoreGnome::GetLoginsImpl(GetLoginsRequest* request, | |
| 280 const PasswordForm& form) { | |
| 281 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 282 GList* found = NULL; | |
| 283 // Search gnome keyring for matching passwords. | |
| 284 GnomeKeyringResult result = gnome_keyring_find_itemsv_sync( | |
| 285 GNOME_KEYRING_ITEM_GENERIC_SECRET, | |
| 286 &found, | |
| 287 "signon_realm", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 288 form.signon_realm.c_str(), | |
| 289 "application", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 290 GNOME_KEYRING_APPLICATION_CHROME, | |
| 291 NULL); | |
| 292 vector<PasswordForm*> forms; | |
| 293 if (result == GNOME_KEYRING_RESULT_OK) { | |
| 294 FillFormVector(found, &forms); | |
| 295 } else if (result != GNOME_KEYRING_RESULT_NO_MATCH) { | |
| 296 LOG(ERROR) << "Keyring find failed: " | |
| 297 << gnome_keyring_result_to_message(result); | |
| 298 } | |
| 299 NotifyConsumer(request, forms); | |
| 300 } | |
| 301 | |
| 302 void PasswordStoreGnome::GetAutofillableLoginsImpl( | |
| 303 GetLoginsRequest* request) { | |
| 304 std::vector<PasswordForm*> forms; | |
| 305 FillAutofillableLogins(&forms); | |
| 306 NotifyConsumer(request, forms); | |
| 307 } | |
| 308 | |
| 309 void PasswordStoreGnome::GetBlacklistLoginsImpl( | |
| 310 GetLoginsRequest* request) { | |
| 311 std::vector<PasswordForm*> forms; | |
| 312 FillBlacklistLogins(&forms); | |
| 313 NotifyConsumer(request, forms); | |
| 314 } | |
| 315 | |
| 316 bool PasswordStoreGnome::FillAutofillableLogins( | |
| 317 std::vector<PasswordForm*>* forms) { | |
| 318 return FillSomeLogins(true, forms); | |
| 319 } | |
| 320 | |
| 321 bool PasswordStoreGnome::FillBlacklistLogins( | |
| 322 std::vector<PasswordForm*>* forms) { | |
| 323 return FillSomeLogins(false, forms); | |
| 324 } | |
| 325 | |
| 326 bool PasswordStoreGnome::AddLoginHelper(const PasswordForm& form, | |
| 327 const base::Time& date_created) { | |
| 328 GnomeKeyringResult result = gnome_keyring_store_password_sync( | |
| 329 &kGnomeSchema, | |
| 330 NULL, // Default keyring. | |
| 331 form.origin.spec().c_str(), // Display name. | |
| 332 UTF16ToUTF8(form.password_value).c_str(), | |
| 333 "origin_url", form.origin.spec().c_str(), | |
| 334 "action_url", form.action.spec().c_str(), | |
| 335 "username_element", UTF16ToUTF8(form.username_element).c_str(), | |
| 336 "username_value", UTF16ToUTF8(form.username_value).c_str(), | |
| 337 "password_element", UTF16ToUTF8(form.password_element).c_str(), | |
| 338 "submit_element", UTF16ToUTF8(form.submit_element).c_str(), | |
| 339 "signon_realm", form.signon_realm.c_str(), | |
| 340 "ssl_valid", form.ssl_valid, | |
| 341 "preferred", form.preferred, | |
| 342 "date_created", Int64ToString(date_created.ToTimeT()).c_str(), | |
| 343 "blacklisted_by_user", form.blacklisted_by_user, | |
| 344 "scheme", form.scheme, | |
| 345 "application", GNOME_KEYRING_APPLICATION_CHROME, | |
| 346 NULL); | |
| 347 | |
| 348 if (result != GNOME_KEYRING_RESULT_OK) { | |
| 349 LOG(ERROR) << "Keyring save failed: " | |
| 350 << gnome_keyring_result_to_message(result); | |
| 351 return false; | |
| 352 } | |
| 353 return true; | |
| 354 } | |
| 355 | |
| 356 bool PasswordStoreGnome::FillSomeLogins( | |
| 357 bool autofillable, | |
| 358 std::vector<PasswordForm*>* forms) { | |
| 359 DCHECK(ChromeThread::CurrentlyOn(ChromeThread::DB)); | |
| 360 GList* found = NULL; | |
| 361 uint32_t blacklisted_by_user = !autofillable; | |
| 362 // Search gnome keyring for matching passwords. | |
| 363 GnomeKeyringResult result = gnome_keyring_find_itemsv_sync( | |
| 364 GNOME_KEYRING_ITEM_GENERIC_SECRET, | |
| 365 &found, | |
| 366 "blacklisted_by_user", GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32, | |
| 367 blacklisted_by_user, | |
| 368 "application", GNOME_KEYRING_ATTRIBUTE_TYPE_STRING, | |
| 369 GNOME_KEYRING_APPLICATION_CHROME, | |
| 370 NULL); | |
| 371 if (result == GNOME_KEYRING_RESULT_OK) { | |
| 372 FillFormVector(found, forms); | |
| 373 } else if (result != GNOME_KEYRING_RESULT_NO_MATCH) { | |
| 374 LOG(ERROR) << "Keyring find failed: " | |
| 375 << gnome_keyring_result_to_message(result); | |
| 376 return false; | |
| 377 } | |
| 378 return true; | |
| 379 } | |
| 380 | |
| 381 void PasswordStoreGnome::FillFormVector(GList* found, | |
| 382 std::vector<PasswordForm*>* forms) { | |
| 383 GList* element = g_list_first(found); | |
| 384 while (element != NULL) { | |
| 385 GnomeKeyringFound* data = static_cast<GnomeKeyringFound*>(element->data); | |
| 386 char* password = data->secret; | |
| 387 | |
| 388 GnomeKeyringAttributeList* attributes = data->attributes; | |
| 389 // Read the string and int attributes into the appropriate map. | |
| 390 map<string, string> string_attribute_map; | |
| 391 map<string, uint32> uint_attribute_map; | |
| 392 for (unsigned int i = 0; i < attributes->len; ++i) { | |
| 393 GnomeKeyringAttribute attribute = | |
| 394 gnome_keyring_attribute_list_index(attributes, i); | |
| 395 if (attribute.type == GNOME_KEYRING_ATTRIBUTE_TYPE_STRING) { | |
| 396 string_attribute_map[string(attribute.name)] = | |
| 397 string(attribute.value.string); | |
| 398 } else if (attribute.type == GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32) { | |
| 399 uint_attribute_map[string(attribute.name)] = attribute.value.integer; | |
| 400 } | |
| 401 } | |
| 402 | |
| 403 PasswordForm* form = new PasswordForm(); | |
| 404 form->origin = GURL(string_attribute_map["origin_url"]); | |
| 405 form->action = GURL(string_attribute_map["action_url"]); | |
| 406 form->username_element = | |
| 407 UTF8ToUTF16(string(string_attribute_map["username_element"])); | |
| 408 form->username_value = | |
| 409 UTF8ToUTF16(string(string_attribute_map["username_value"])); | |
| 410 form->password_element = | |
| 411 UTF8ToUTF16(string(string_attribute_map["password_element"])); | |
| 412 form->password_value = UTF8ToUTF16(string(password)); | |
| 413 form->submit_element = | |
| 414 UTF8ToUTF16(string(string_attribute_map["submit_element"])); | |
| 415 form->signon_realm = string_attribute_map["signon_realm"]; | |
| 416 form->ssl_valid = uint_attribute_map["ssl_valid"]; | |
| 417 form->preferred = uint_attribute_map["preferred"]; | |
| 418 string date = string_attribute_map["date_created"]; | |
| 419 int64 date_created = 0; | |
| 420 bool date_ok = StringToInt64(date, &date_created); | |
| 421 DCHECK(date_ok); | |
| 422 DCHECK_NE(date_created, 0); | |
| 423 form->date_created = base::Time::FromTimeT(date_created); | |
| 424 form->blacklisted_by_user = uint_attribute_map["blacklisted_by_user"]; | |
| 425 form->scheme = static_cast<PasswordForm::Scheme>( | |
| 426 uint_attribute_map["scheme"]); | |
| 427 | |
| 428 forms->push_back(form); | |
| 429 | |
| 430 element = g_list_next(element); | |
| 431 } | |
| 432 gnome_keyring_found_list_free(found); | |
| 433 } | |
| OLD | NEW |