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 "Logger.h" 29 #include "LocaleUtils.h" 30 #include "StorageUtils.h" 31 32 33 #undef B_TRANSLATION_CONTEXT 34 #define B_TRANSLATION_CONTEXT "Model" 35 36 37 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password." 38 // this prefix is added before the nickname in the keystore 39 // so that HDS username/password pairs can be identified. 40 41 static const char* kHaikuDepotKeyring = "HaikuDepot"; 42 43 44 ModelListener::~ModelListener() 45 { 46 } 47 48 49 // #pragma mark - Model 50 51 52 Model::Model() 53 : 54 fDepots(), 55 fCategories(), 56 fPackageListViewMode(PROMINENT), 57 fCanShareAnonymousUsageData(false) 58 { 59 fPackageFilterModel = new PackageFilterModel(); 60 fPackageScreenshotRepository = new PackageScreenshotRepository( 61 PackageScreenshotRepositoryListenerRef(this), 62 &fWebAppInterface); 63 } 64 65 66 Model::~Model() 67 { 68 delete fPackageFilterModel; 69 delete fPackageScreenshotRepository; 70 } 71 72 73 LanguageModel* 74 Model::Language() 75 { 76 return &fLanguageModel; 77 } 78 79 80 PackageFilterModel* 81 Model::PackageFilter() 82 { 83 return fPackageFilterModel; 84 } 85 86 87 PackageIconRepository& 88 Model::GetPackageIconRepository() 89 { 90 return fPackageIconRepository; 91 } 92 93 94 status_t 95 Model::InitPackageIconRepository() 96 { 97 BPath tarPath; 98 status_t result = IconTarPath(tarPath); 99 if (result == B_OK) 100 result = fPackageIconRepository.Init(tarPath); 101 return result; 102 } 103 104 105 PackageScreenshotRepository* 106 Model::GetPackageScreenshotRepository() 107 { 108 return fPackageScreenshotRepository; 109 } 110 111 112 void 113 Model::AddListener(const ModelListenerRef& listener) 114 { 115 fListeners.push_back(listener); 116 } 117 118 119 // TODO; part of a wider change; cope with the package being in more than one 120 // depot 121 PackageInfoRef 122 Model::PackageForName(const BString& name) 123 { 124 std::vector<DepotInfoRef>::iterator it; 125 for (it = fDepots.begin(); it != fDepots.end(); it++) { 126 DepotInfoRef depotInfoRef = *it; 127 PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name); 128 if (packageInfoRef.Get() != NULL) 129 return packageInfoRef; 130 } 131 return PackageInfoRef(); 132 } 133 134 135 void 136 Model::MergeOrAddDepot(const DepotInfoRef& depot) 137 { 138 BString depotName = depot->Name(); 139 for(uint32 i = 0; i < fDepots.size(); i++) { 140 if (fDepots[i]->Name() == depotName) { 141 DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true); 142 ersatzDepot->SyncPackagesFromDepot(depot); 143 fDepots[i] = ersatzDepot; 144 return; 145 } 146 } 147 fDepots.push_back(depot); 148 } 149 150 151 bool 152 Model::HasDepot(const BString& name) const 153 { 154 return NULL != DepotForName(name).Get(); 155 } 156 157 158 const DepotInfoRef 159 Model::DepotForName(const BString& name) const 160 { 161 std::vector<DepotInfoRef>::const_iterator it; 162 for (it = fDepots.begin(); it != fDepots.end(); it++) { 163 DepotInfoRef aDepot = *it; 164 if (aDepot->Name() == name) 165 return aDepot; 166 } 167 return DepotInfoRef(); 168 } 169 170 171 int32 172 Model::CountDepots() const 173 { 174 return fDepots.size(); 175 } 176 177 178 DepotInfoRef 179 Model::DepotAtIndex(int32 index) const 180 { 181 return fDepots[index]; 182 } 183 184 185 bool 186 Model::HasAnyProminentPackages() 187 { 188 std::vector<DepotInfoRef>::iterator it; 189 for (it = fDepots.begin(); it != fDepots.end(); it++) { 190 DepotInfoRef aDepot = *it; 191 if (aDepot->HasAnyProminentPackages()) 192 return true; 193 } 194 return false; 195 } 196 197 198 void 199 Model::Clear() 200 { 201 GetPackageIconRepository().Clear(); 202 fDepots.clear(); 203 fPopulatedPackageNames.MakeEmpty(); 204 } 205 206 207 void 208 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state) 209 { 210 for (int32 i = 0; i < packageNames.CountStrings(); i++) { 211 BString packageName = packageNames.StringAt(i); 212 PackageInfoRef packageInfo = PackageForName(packageName); 213 214 if (packageInfo.IsSet()) { 215 packageInfo->SetState(state); 216 HDINFO("did update package [%s] with state [%s]", 217 packageName.String(), package_state_to_string(state)); 218 } 219 else { 220 HDINFO("was unable to find package [%s] so was not possible to set" 221 " the state to [%s]", packageName.String(), 222 package_state_to_string(state)); 223 } 224 } 225 } 226 227 228 void 229 Model::SetPackageListViewMode(package_list_view_mode mode) 230 { 231 fPackageListViewMode = mode; 232 } 233 234 235 void 236 Model::SetCanShareAnonymousUsageData(bool value) 237 { 238 fCanShareAnonymousUsageData = value; 239 } 240 241 242 // #pragma mark - information retrieval 243 244 /*! It may transpire that the package has no corresponding record on the 245 server side because the repository is not represented in the server. 246 In such a case, there is little point in communicating with the server 247 only to hear back that the package does not exist. 248 */ 249 250 bool 251 Model::CanPopulatePackage(const PackageInfoRef& package) 252 { 253 const BString& depotName = package->DepotName(); 254 255 if (depotName.IsEmpty()) 256 return false; 257 258 const DepotInfoRef& depot = DepotForName(depotName); 259 260 if (depot.Get() == NULL) 261 return false; 262 263 return !depot->WebAppRepositoryCode().IsEmpty(); 264 } 265 266 267 /*! Initially only superficial data is loaded from the server into the data 268 model of the packages. When the package is viewed, additional data needs 269 to be populated including ratings. This method takes care of that. 270 */ 271 272 void 273 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 274 { 275 HDTRACE("will populate package for [%s]", package->Name().String()); 276 277 if (!CanPopulatePackage(package)) { 278 HDINFO("unable to populate package [%s]", package->Name().String()); 279 return; 280 } 281 282 // TODO: There should probably also be a way to "unpopulate" the 283 // package information. Maybe a cache of populated packages, so that 284 // packages loose their extra information after a certain amount of 285 // time when they have not been accessed/displayed in the UI. Otherwise 286 // HaikuDepot will consume more and more resources in the packages. 287 { 288 BAutolock locker(&fLock); 289 bool alreadyPopulated = fPopulatedPackageNames.HasString( 290 package->Name()); 291 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 292 return; 293 if (!alreadyPopulated) 294 fPopulatedPackageNames.Add(package->Name()); 295 } 296 297 if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) { 298 _PopulatePackageChangelog(package); 299 } 300 301 if ((flags & POPULATE_USER_RATINGS) != 0) { 302 // Retrieve info from web-app 303 BMessage info; 304 305 BString packageName; 306 BString webAppRepositoryCode; 307 BString webAppRepositorySourceCode; 308 309 { 310 BAutolock locker(&fLock); 311 packageName = package->Name(); 312 const DepotInfo* depot = DepotForName(package->DepotName()); 313 314 if (depot != NULL) { 315 webAppRepositoryCode = depot->WebAppRepositoryCode(); 316 webAppRepositorySourceCode 317 = depot->WebAppRepositorySourceCode(); 318 } 319 } 320 321 status_t status = fWebAppInterface 322 .RetrieveUserRatingsForPackageForDisplay(packageName, 323 webAppRepositoryCode, webAppRepositorySourceCode, 0, 324 PACKAGE_INFO_MAX_USER_RATINGS, info); 325 if (status == B_OK) { 326 // Parse message 327 BMessage result; 328 BMessage items; 329 if (info.FindMessage("result", &result) == B_OK 330 && result.FindMessage("items", &items) == B_OK) { 331 332 BAutolock locker(&fLock); 333 package->ClearUserRatings(); 334 335 int32 index = 0; 336 while (true) { 337 BString name; 338 name << index++; 339 340 BMessage item; 341 if (items.FindMessage(name, &item) != B_OK) 342 break; 343 344 BString code; 345 if (item.FindString("code", &code) != B_OK) { 346 HDERROR("corrupt user rating at index %" B_PRIi32, 347 index); 348 continue; 349 } 350 351 BString user; 352 BMessage userInfo; 353 if (item.FindMessage("user", &userInfo) != B_OK 354 || userInfo.FindString("nickname", &user) != B_OK) { 355 HDERROR("ignored user rating [%s] without a user " 356 "nickname", code.String()); 357 continue; 358 } 359 360 // Extract basic info, all items are optional 361 BString languageCode; 362 BString comment; 363 double rating; 364 item.FindString("naturalLanguageCode", &languageCode); 365 item.FindString("comment", &comment); 366 if (item.FindDouble("rating", &rating) != B_OK) 367 rating = -1; 368 if (comment.Length() == 0 && rating == -1) { 369 HDERROR("rating [%s] has no comment or rating so will" 370 " be ignored", code.String()); 371 continue; 372 } 373 374 // For which version of the package was the rating? 375 BString major = "?"; 376 BString minor = "?"; 377 BString micro = ""; 378 double revision = -1; 379 BString architectureCode = ""; 380 BMessage version; 381 if (item.FindMessage("pkgVersion", &version) == B_OK) { 382 version.FindString("major", &major); 383 version.FindString("minor", &minor); 384 version.FindString("micro", µ); 385 version.FindDouble("revision", &revision); 386 version.FindString("architectureCode", 387 &architectureCode); 388 } 389 BString versionString = major; 390 versionString << "."; 391 versionString << minor; 392 if (!micro.IsEmpty()) { 393 versionString << "."; 394 versionString << micro; 395 } 396 if (revision > 0) { 397 versionString << "-"; 398 versionString << (int) revision; 399 } 400 401 if (!architectureCode.IsEmpty()) { 402 versionString << " " << STR_MDASH << " "; 403 versionString << architectureCode; 404 } 405 406 double createTimestamp; 407 item.FindDouble("createTimestamp", &createTimestamp); 408 409 // Add the rating to the PackageInfo 410 UserRatingRef userRating(new UserRating( 411 UserInfo(user), rating, 412 comment, 413 languageCode, 414 // note that language identifiers are "code" in HDS and "id" in Haiku 415 versionString, 416 (uint64) createTimestamp), true); 417 package->AddUserRating(userRating); 418 HDDEBUG("rating [%s] retrieved from server", code.String()); 419 } 420 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 421 index - 1, packageName.String()); 422 } else { 423 BString message; 424 message.SetToFormat("failure to retrieve user ratings for [%s]", 425 packageName.String()); 426 _MaybeLogJsonRpcError(info, message.String()); 427 } 428 } else 429 HDERROR("unable to retrieve user ratings"); 430 } 431 } 432 433 434 void 435 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 436 { 437 BMessage info; 438 BString packageName; 439 440 { 441 BAutolock locker(&fLock); 442 packageName = package->Name(); 443 } 444 445 status_t status = fWebAppInterface.GetChangelog(packageName, info); 446 447 if (status == B_OK) { 448 // Parse message 449 BMessage result; 450 BString content; 451 if (info.FindMessage("result", &result) == B_OK) { 452 if (result.FindString("content", &content) == B_OK 453 && 0 != content.Length()) { 454 BAutolock locker(&fLock); 455 package->SetChangelog(content); 456 HDDEBUG("changelog populated for [%s]", packageName.String()); 457 } else 458 HDDEBUG("no changelog present for [%s]", packageName.String()); 459 } else 460 _MaybeLogJsonRpcError(info, "populate package changelog"); 461 } else { 462 HDERROR("unable to obtain the changelog for the package [%s]", 463 packageName.String()); 464 } 465 } 466 467 468 static void 469 model_remove_key_for_user(const BString& nickname) 470 { 471 if (nickname.IsEmpty()) 472 return; 473 BKeyStore keyStore; 474 BPasswordKey key; 475 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 476 << nickname; 477 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 478 passwordIdentifier, key); 479 480 switch (result) { 481 case B_OK: 482 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 483 if (result != B_OK) { 484 HDERROR("error occurred when removing password for nickname " 485 "[%s] : %s", nickname.String(), strerror(result)); 486 } 487 break; 488 case B_ENTRY_NOT_FOUND: 489 return; 490 default: 491 HDERROR("error occurred when finding password for nickname " 492 "[%s] : %s", nickname.String(), strerror(result)); 493 break; 494 } 495 } 496 497 498 void 499 Model::SetNickname(BString nickname) 500 { 501 BString password; 502 BString existingNickname = Nickname(); 503 504 // this happens when the user is logging out. Best to remove the password 505 // stored for the existing user since it is no longer required. 506 507 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 508 model_remove_key_for_user(existingNickname); 509 510 if (nickname.Length() > 0) { 511 BPasswordKey key; 512 BKeyStore keyStore; 513 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 514 << nickname; 515 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 516 passwordIdentifier, key) == B_OK) { 517 password = key.Password(); 518 } 519 if (password.IsEmpty()) 520 nickname = ""; 521 } 522 523 SetCredentials(nickname, password, false); 524 } 525 526 527 const BString& 528 Model::Nickname() 529 { 530 return fWebAppInterface.Nickname(); 531 } 532 533 534 void 535 Model::SetCredentials(const BString& nickname, const BString& passwordClear, 536 bool storePassword) 537 { 538 BString existingNickname = Nickname(); 539 540 if (storePassword) { 541 // no point continuing to store the password for the previous user. 542 543 if (!existingNickname.IsEmpty()) 544 model_remove_key_for_user(existingNickname); 545 546 // adding a key that is already there does not seem to override the 547 // existing key so the old key needs to be removed first. 548 549 if (!nickname.IsEmpty()) 550 model_remove_key_for_user(nickname); 551 552 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 553 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 554 << nickname; 555 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 556 BKeyStore keyStore; 557 keyStore.AddKeyring(kHaikuDepotKeyring); 558 keyStore.AddKey(kHaikuDepotKeyring, key); 559 } 560 } 561 562 BAutolock locker(&fLock); 563 fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear)); 564 565 if (nickname != existingNickname) 566 _NotifyAuthorizationChanged(); 567 } 568 569 570 /*! When bulk repository data comes down from the server, it will 571 arrive as a json.gz payload. This is stored locally as a cache 572 and this method will provide the on-disk storage location for 573 this file. 574 */ 575 576 status_t 577 Model::DumpExportRepositoryDataPath(BPath& path) 578 { 579 BString leaf; 580 leaf.SetToFormat("repository-all_%s.json.gz", 581 Language()->PreferredLanguage()->ID()); 582 return StorageUtils::LocalWorkingFilesPath(leaf, path); 583 } 584 585 586 /*! When the system downloads reference data (eg; categories) from the server 587 then the downloaded data is stored and cached at the path defined by this 588 method. 589 */ 590 591 status_t 592 Model::DumpExportReferenceDataPath(BPath& path) 593 { 594 BString leaf; 595 leaf.SetToFormat("reference-all_%s.json.gz", 596 Language()->PreferredLanguage()->ID()); 597 return StorageUtils::LocalWorkingFilesPath(leaf, path); 598 } 599 600 601 status_t 602 Model::IconTarPath(BPath& path) const 603 { 604 return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path); 605 } 606 607 608 status_t 609 Model::DumpExportPkgDataPath(BPath& path, 610 const BString& repositorySourceCode) 611 { 612 BString leaf; 613 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 614 Language()->PreferredLanguage()->ID()); 615 return StorageUtils::LocalWorkingFilesPath(leaf, path); 616 } 617 618 619 // #pragma mark - listener notification methods 620 621 622 void 623 Model::_NotifyAuthorizationChanged() 624 { 625 std::vector<ModelListenerRef>::const_iterator it; 626 for (it = fListeners.begin(); it != fListeners.end(); it++) { 627 const ModelListenerRef& listener = *it; 628 if (listener.IsSet()) 629 listener->AuthorizationChanged(); 630 } 631 } 632 633 634 void 635 Model::_NotifyCategoryListChanged() 636 { 637 std::vector<ModelListenerRef>::const_iterator it; 638 for (it = fListeners.begin(); it != fListeners.end(); it++) { 639 const ModelListenerRef& listener = *it; 640 if (listener.IsSet()) 641 listener->CategoryListChanged(); 642 } 643 } 644 645 646 void 647 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 648 const char *sourceDescription) const 649 { 650 BMessage error; 651 BString errorMessage; 652 double errorCode; 653 654 if (responsePayload.FindMessage("error", &error) == B_OK 655 && error.FindString("message", &errorMessage) == B_OK 656 && error.FindDouble("code", &errorCode) == B_OK) { 657 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 658 errorMessage.String(), errorCode); 659 } else 660 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 661 } 662 663 664 // #pragma mark - Rating Stabilities 665 666 667 int32 668 Model::CountRatingStabilities() const 669 { 670 return fRatingStabilities.size(); 671 } 672 673 674 RatingStabilityRef 675 Model::RatingStabilityByCode(BString& code) const 676 { 677 std::vector<RatingStabilityRef>::const_iterator it; 678 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 679 it++) { 680 RatingStabilityRef aRatingStability = *it; 681 if (aRatingStability->Code() == code) 682 return aRatingStability; 683 } 684 return RatingStabilityRef(); 685 } 686 687 688 RatingStabilityRef 689 Model::RatingStabilityAtIndex(int32 index) const 690 { 691 return fRatingStabilities[index]; 692 } 693 694 695 void 696 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 697 { 698 std::vector<RatingStabilityRef>::const_iterator it; 699 for (it = values.begin(); it != values.end(); it++) 700 _AddRatingStability(*it); 701 } 702 703 704 void 705 Model::_AddRatingStability(const RatingStabilityRef& value) 706 { 707 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 708 = std::lower_bound( 709 fRatingStabilities.begin(), 710 fRatingStabilities.end(), 711 value, 712 &IsRatingStabilityBefore); 713 std::vector<RatingStabilityRef>::iterator itInsertionPt = 714 fRatingStabilities.begin() 715 + (itInsertionPtConst - fRatingStabilities.begin()); 716 717 if (itInsertionPt != fRatingStabilities.end() 718 && (*itInsertionPt)->Code() == value->Code()) { 719 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 720 // replace the one with the same code. 721 } 722 723 fRatingStabilities.insert(itInsertionPt, value); 724 } 725 726 727 // #pragma mark - Categories 728 729 730 int32 731 Model::CountCategories() const 732 { 733 return fCategories.size(); 734 } 735 736 737 CategoryRef 738 Model::CategoryByCode(BString& code) const 739 { 740 std::vector<CategoryRef>::const_iterator it; 741 for (it = fCategories.begin(); it != fCategories.end(); it++) { 742 CategoryRef aCategory = *it; 743 if (aCategory->Code() == code) 744 return aCategory; 745 } 746 return CategoryRef(); 747 } 748 749 750 CategoryRef 751 Model::CategoryAtIndex(int32 index) const 752 { 753 return fCategories[index]; 754 } 755 756 757 void 758 Model::AddCategories(std::vector<CategoryRef>& values) 759 { 760 std::vector<CategoryRef>::iterator it; 761 for (it = values.begin(); it != values.end(); it++) 762 _AddCategory(*it); 763 _NotifyCategoryListChanged(); 764 } 765 766 /*! This will insert the category in order. 767 */ 768 769 void 770 Model::_AddCategory(const CategoryRef& category) 771 { 772 std::vector<CategoryRef>::const_iterator itInsertionPtConst 773 = std::lower_bound( 774 fCategories.begin(), 775 fCategories.end(), 776 category, 777 &IsPackageCategoryBefore); 778 std::vector<CategoryRef>::iterator itInsertionPt = 779 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 780 781 if (itInsertionPt != fCategories.end() 782 && (*itInsertionPt)->Code() == category->Code()) { 783 itInsertionPt = fCategories.erase(itInsertionPt); 784 // replace the one with the same code. 785 } 786 787 fCategories.insert(itInsertionPt, category); 788 } 789 790 791 void 792 Model::ScreenshotCached(const ScreenshotCoordinate& coord) 793 { 794 std::vector<ModelListenerRef>::const_iterator it; 795 for (it = fListeners.begin(); it != fListeners.end(); it++) { 796 const ModelListenerRef& listener = *it; 797 if (listener.IsSet()) 798 listener->ScreenshotCached(coord); 799 } 800 } 801