1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * Copyright 2016-2024, Andrew Lindesay <apl@lindesay.co.nz>. 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 #include "Model.h" 8 9 #include <algorithm> 10 #include <ctime> 11 #include <vector> 12 13 #include <stdarg.h> 14 #include <time.h> 15 16 #include <Autolock.h> 17 #include <Catalog.h> 18 #include <Directory.h> 19 #include <Entry.h> 20 #include <File.h> 21 #include <KeyStore.h> 22 #include <Locale.h> 23 #include <LocaleRoster.h> 24 #include <Message.h> 25 #include <Path.h> 26 27 #include "HaikuDepotConstants.h" 28 #include "LocaleUtils.h" 29 #include "Logger.h" 30 #include "PackageUtils.h" 31 #include "StorageUtils.h" 32 33 34 #undef B_TRANSLATION_CONTEXT 35 #define B_TRANSLATION_CONTEXT "Model" 36 37 38 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password." 39 // this prefix is added before the nickname in the keystore 40 // so that HDS username/password pairs can be identified. 41 42 static const char* kHaikuDepotKeyring = "HaikuDepot"; 43 44 45 ModelListener::~ModelListener() 46 { 47 } 48 49 50 // #pragma mark - Model 51 52 53 Model::Model() 54 : 55 fPreferredLanguage(LanguageRef(new Language(LANGUAGE_DEFAULT_ID, "English", true), true)), 56 fDepots(), 57 fCategories(), 58 fPackageListViewMode(PROMINENT), 59 fCanShareAnonymousUsageData(false) 60 { 61 62 // setup the language into a default state with a hard-coded language so 63 // that there is a language at all. 64 fLanguageRepository = new LanguageRepository(); 65 fLanguageRepository->AddLanguage(fPreferredLanguage); 66 67 fPackageFilterModel = new PackageFilterModel(); 68 fPackageScreenshotRepository = new PackageScreenshotRepository( 69 PackageScreenshotRepositoryListenerRef(this), 70 &fWebAppInterface); 71 } 72 73 74 Model::~Model() 75 { 76 delete fPackageFilterModel; 77 delete fPackageScreenshotRepository; 78 delete fLanguageRepository; 79 } 80 81 82 LanguageRepository* 83 Model::Languages() 84 { 85 return fLanguageRepository; 86 } 87 88 89 PackageFilterModel* 90 Model::PackageFilter() 91 { 92 return fPackageFilterModel; 93 } 94 95 96 PackageIconRepository& 97 Model::GetPackageIconRepository() 98 { 99 return fPackageIconRepository; 100 } 101 102 103 status_t 104 Model::InitPackageIconRepository() 105 { 106 BPath tarPath; 107 status_t result = StorageUtils::IconTarPath(tarPath); 108 if (result == B_OK) 109 result = fPackageIconRepository.Init(tarPath); 110 return result; 111 } 112 113 114 PackageScreenshotRepository* 115 Model::GetPackageScreenshotRepository() 116 { 117 return fPackageScreenshotRepository; 118 } 119 120 121 void 122 Model::AddListener(const ModelListenerRef& listener) 123 { 124 fListeners.push_back(listener); 125 } 126 127 128 LanguageRef 129 Model::PreferredLanguage() const 130 { 131 return fPreferredLanguage; 132 } 133 134 135 void 136 Model::SetPreferredLanguage(LanguageRef value) 137 { 138 if (value.IsSet()) { 139 if (fPreferredLanguage != value) 140 fPreferredLanguage = value; 141 } 142 } 143 144 145 // TODO; part of a wider change; cope with the package being in more than one 146 // depot 147 PackageInfoRef 148 Model::PackageForName(const BString& name) 149 { 150 std::vector<DepotInfoRef>::iterator it; 151 for (it = fDepots.begin(); it != fDepots.end(); it++) { 152 DepotInfoRef depotInfoRef = *it; 153 PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name); 154 if (packageInfoRef.Get() != NULL) 155 return packageInfoRef; 156 } 157 return PackageInfoRef(); 158 } 159 160 161 void 162 Model::MergeOrAddDepot(const DepotInfoRef& depot) 163 { 164 BString depotName = depot->Name(); 165 for(uint32 i = 0; i < fDepots.size(); i++) { 166 if (fDepots[i]->Name() == depotName) { 167 DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true); 168 ersatzDepot->SyncPackagesFromDepot(depot); 169 fDepots[i] = ersatzDepot; 170 return; 171 } 172 } 173 fDepots.push_back(depot); 174 } 175 176 177 bool 178 Model::HasDepot(const BString& name) const 179 { 180 return NULL != DepotForName(name).Get(); 181 } 182 183 184 const DepotInfoRef 185 Model::DepotForName(const BString& name) const 186 { 187 std::vector<DepotInfoRef>::const_iterator it; 188 for (it = fDepots.begin(); it != fDepots.end(); it++) { 189 DepotInfoRef aDepot = *it; 190 if (aDepot->Name() == name) 191 return aDepot; 192 } 193 return DepotInfoRef(); 194 } 195 196 197 int32 198 Model::CountDepots() const 199 { 200 return fDepots.size(); 201 } 202 203 204 DepotInfoRef 205 Model::DepotAtIndex(int32 index) const 206 { 207 return fDepots[index]; 208 } 209 210 211 bool 212 Model::HasAnyProminentPackages() 213 { 214 std::vector<DepotInfoRef>::iterator it; 215 for (it = fDepots.begin(); it != fDepots.end(); it++) { 216 DepotInfoRef aDepot = *it; 217 if (aDepot->HasAnyProminentPackages()) 218 return true; 219 } 220 return false; 221 } 222 223 224 void 225 Model::Clear() 226 { 227 GetPackageIconRepository().Clear(); 228 fDepots.clear(); 229 fPopulatedPackageNames.MakeEmpty(); 230 } 231 232 233 void 234 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state) 235 { 236 for (int32 i = 0; i < packageNames.CountStrings(); i++) { 237 BString packageName = packageNames.StringAt(i); 238 PackageInfoRef packageInfo = PackageForName(packageName); 239 240 if (packageInfo.IsSet()) { 241 // TODO; make this immutable 242 243 PackageLocalInfoRef localInfo = PackageUtils::NewLocalInfo(packageInfo); 244 localInfo->SetState(state); 245 packageInfo->SetLocalInfo(localInfo); 246 247 HDINFO("did update package [%s] with state [%s]", packageName.String(), 248 PackageUtils::StateToString(state)); 249 } 250 else { 251 HDINFO("was unable to find package [%s] so was not possible to set the state to [%s]", 252 packageName.String(), PackageUtils::StateToString(state)); 253 } 254 } 255 } 256 257 258 void 259 Model::SetPackageListViewMode(package_list_view_mode mode) 260 { 261 fPackageListViewMode = mode; 262 } 263 264 265 void 266 Model::SetCanShareAnonymousUsageData(bool value) 267 { 268 fCanShareAnonymousUsageData = value; 269 } 270 271 272 // #pragma mark - information retrieval 273 274 /*! It may transpire that the package has no corresponding record on the 275 server side because the repository is not represented in the server. 276 In such a case, there is little point in communicating with the server 277 only to hear back that the package does not exist. 278 */ 279 280 bool 281 Model::CanPopulatePackage(const PackageInfoRef& package) 282 { 283 const BString& depotName = package->DepotName(); 284 285 if (depotName.IsEmpty()) 286 return false; 287 288 const DepotInfoRef& depot = DepotForName(depotName); 289 290 if (depot.Get() == NULL) 291 return false; 292 293 return !depot->WebAppRepositoryCode().IsEmpty(); 294 } 295 296 297 static void 298 model_remove_key_for_user(const BString& nickname) 299 { 300 if (nickname.IsEmpty()) 301 return; 302 BKeyStore keyStore; 303 BPasswordKey key; 304 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 305 << nickname; 306 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 307 passwordIdentifier, key); 308 309 switch (result) { 310 case B_OK: 311 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 312 if (result != B_OK) { 313 HDERROR("error occurred when removing password for nickname " 314 "[%s] : %s", nickname.String(), strerror(result)); 315 } 316 break; 317 case B_ENTRY_NOT_FOUND: 318 return; 319 default: 320 HDERROR("error occurred when finding password for nickname " 321 "[%s] : %s", nickname.String(), strerror(result)); 322 break; 323 } 324 } 325 326 327 void 328 Model::SetNickname(BString nickname) 329 { 330 BString password; 331 BString existingNickname = Nickname(); 332 333 // this happens when the user is logging out. Best to remove the password 334 // stored for the existing user since it is no longer required. 335 336 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 337 model_remove_key_for_user(existingNickname); 338 339 if (nickname.Length() > 0) { 340 BPasswordKey key; 341 BKeyStore keyStore; 342 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 343 << nickname; 344 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 345 passwordIdentifier, key) == B_OK) { 346 password = key.Password(); 347 } 348 if (password.IsEmpty()) 349 nickname = ""; 350 } 351 352 SetCredentials(nickname, password, false); 353 } 354 355 356 const BString& 357 Model::Nickname() 358 { 359 return fWebAppInterface.Nickname(); 360 } 361 362 363 void 364 Model::SetCredentials(const BString& nickname, const BString& passwordClear, 365 bool storePassword) 366 { 367 BString existingNickname = Nickname(); 368 369 if (storePassword) { 370 // no point continuing to store the password for the previous user. 371 372 if (!existingNickname.IsEmpty()) 373 model_remove_key_for_user(existingNickname); 374 375 // adding a key that is already there does not seem to override the 376 // existing key so the old key needs to be removed first. 377 378 if (!nickname.IsEmpty()) 379 model_remove_key_for_user(nickname); 380 381 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 382 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 383 << nickname; 384 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 385 BKeyStore keyStore; 386 keyStore.AddKeyring(kHaikuDepotKeyring); 387 keyStore.AddKey(kHaikuDepotKeyring, key); 388 } 389 } 390 391 BAutolock locker(&fLock); 392 fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear)); 393 394 if (nickname != existingNickname) 395 _NotifyAuthorizationChanged(); 396 } 397 398 399 // #pragma mark - listener notification methods 400 401 402 void 403 Model::_NotifyAuthorizationChanged() 404 { 405 std::vector<ModelListenerRef>::const_iterator it; 406 for (it = fListeners.begin(); it != fListeners.end(); it++) { 407 const ModelListenerRef& listener = *it; 408 if (listener.IsSet()) 409 listener->AuthorizationChanged(); 410 } 411 } 412 413 414 void 415 Model::_NotifyCategoryListChanged() 416 { 417 std::vector<ModelListenerRef>::const_iterator it; 418 for (it = fListeners.begin(); it != fListeners.end(); it++) { 419 const ModelListenerRef& listener = *it; 420 if (listener.IsSet()) 421 listener->CategoryListChanged(); 422 } 423 } 424 425 426 void 427 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 428 const char *sourceDescription) const 429 { 430 BMessage error; 431 BString errorMessage; 432 double errorCode; 433 434 if (responsePayload.FindMessage("error", &error) == B_OK 435 && error.FindString("message", &errorMessage) == B_OK 436 && error.FindDouble("code", &errorCode) == B_OK) { 437 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 438 errorMessage.String(), errorCode); 439 } else 440 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 441 } 442 443 444 // #pragma mark - Rating Stabilities 445 446 447 int32 448 Model::CountRatingStabilities() const 449 { 450 return fRatingStabilities.size(); 451 } 452 453 454 RatingStabilityRef 455 Model::RatingStabilityByCode(BString& code) const 456 { 457 std::vector<RatingStabilityRef>::const_iterator it; 458 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 459 it++) { 460 RatingStabilityRef aRatingStability = *it; 461 if (aRatingStability->Code() == code) 462 return aRatingStability; 463 } 464 return RatingStabilityRef(); 465 } 466 467 468 RatingStabilityRef 469 Model::RatingStabilityAtIndex(int32 index) const 470 { 471 return fRatingStabilities[index]; 472 } 473 474 475 void 476 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 477 { 478 std::vector<RatingStabilityRef>::const_iterator it; 479 for (it = values.begin(); it != values.end(); it++) 480 _AddRatingStability(*it); 481 } 482 483 484 void 485 Model::_AddRatingStability(const RatingStabilityRef& value) 486 { 487 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 488 = std::lower_bound( 489 fRatingStabilities.begin(), 490 fRatingStabilities.end(), 491 value, 492 &IsRatingStabilityBefore); 493 std::vector<RatingStabilityRef>::iterator itInsertionPt = 494 fRatingStabilities.begin() 495 + (itInsertionPtConst - fRatingStabilities.begin()); 496 497 if (itInsertionPt != fRatingStabilities.end() 498 && (*itInsertionPt)->Code() == value->Code()) { 499 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 500 // replace the one with the same code. 501 } 502 503 fRatingStabilities.insert(itInsertionPt, value); 504 } 505 506 507 // #pragma mark - Categories 508 509 510 int32 511 Model::CountCategories() const 512 { 513 return fCategories.size(); 514 } 515 516 517 CategoryRef 518 Model::CategoryByCode(BString& code) const 519 { 520 std::vector<CategoryRef>::const_iterator it; 521 for (it = fCategories.begin(); it != fCategories.end(); it++) { 522 CategoryRef aCategory = *it; 523 if (aCategory->Code() == code) 524 return aCategory; 525 } 526 return CategoryRef(); 527 } 528 529 530 CategoryRef 531 Model::CategoryAtIndex(int32 index) const 532 { 533 return fCategories[index]; 534 } 535 536 537 void 538 Model::AddCategories(std::vector<CategoryRef>& values) 539 { 540 std::vector<CategoryRef>::iterator it; 541 for (it = values.begin(); it != values.end(); it++) 542 _AddCategory(*it); 543 _NotifyCategoryListChanged(); 544 } 545 546 /*! This will insert the category in order. 547 */ 548 549 void 550 Model::_AddCategory(const CategoryRef& category) 551 { 552 std::vector<CategoryRef>::const_iterator itInsertionPtConst 553 = std::lower_bound( 554 fCategories.begin(), 555 fCategories.end(), 556 category, 557 &IsPackageCategoryBefore); 558 std::vector<CategoryRef>::iterator itInsertionPt = 559 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 560 561 if (itInsertionPt != fCategories.end() 562 && (*itInsertionPt)->Code() == category->Code()) { 563 itInsertionPt = fCategories.erase(itInsertionPt); 564 // replace the one with the same code. 565 } 566 567 fCategories.insert(itInsertionPt, category); 568 } 569 570 571 void 572 Model::ScreenshotCached(const ScreenshotCoordinate& coord) 573 { 574 std::vector<ModelListenerRef>::const_iterator it; 575 for (it = fListeners.begin(); it != fListeners.end(); it++) { 576 const ModelListenerRef& listener = *it; 577 if (listener.IsSet()) 578 listener->ScreenshotCached(coord); 579 } 580 } 581