1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * Copyright 2016-2020, Andrew Lindesay <apl@lindesay.co.nz>. 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 8 #include "Model.h" 9 10 #include <ctime> 11 #include <stdarg.h> 12 #include <time.h> 13 14 #include <Autolock.h> 15 #include <Catalog.h> 16 #include <Collator.h> 17 #include <Directory.h> 18 #include <Entry.h> 19 #include <File.h> 20 #include <KeyStore.h> 21 #include <Locale.h> 22 #include <LocaleRoster.h> 23 #include <Message.h> 24 #include <Path.h> 25 26 #include "HaikuDepotConstants.h" 27 #include "Logger.h" 28 #include "LocaleUtils.h" 29 #include "StorageUtils.h" 30 #include "RepositoryUrlUtils.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 PackageFilter::~PackageFilter() 45 { 46 } 47 48 49 ModelListener::~ModelListener() 50 { 51 } 52 53 54 // #pragma mark - PackageFilters 55 56 57 class AnyFilter : public PackageFilter { 58 public: 59 virtual bool AcceptsPackage(const PackageInfoRef& package) const 60 { 61 return true; 62 } 63 }; 64 65 66 class DepotFilter : public PackageFilter { 67 public: 68 DepotFilter(const DepotInfo& depot) 69 : 70 fDepot(depot) 71 { 72 } 73 74 virtual bool AcceptsPackage(const PackageInfoRef& package) const 75 { 76 // TODO: Maybe a PackageInfo ought to know the Depot it came from? 77 // But right now the same package could theoretically be provided 78 // from different depots and the filter would work correctly. 79 // Also the PackageList could actually contain references to packages 80 // instead of the packages as objects. The equal operator is quite 81 // expensive as is. 82 return fDepot.Packages().Contains(package); 83 } 84 85 const BString& Depot() const 86 { 87 return fDepot.Name(); 88 } 89 90 private: 91 DepotInfo fDepot; 92 }; 93 94 95 class CategoryFilter : public PackageFilter { 96 public: 97 CategoryFilter(const BString& category) 98 : 99 fCategory(category) 100 { 101 } 102 103 virtual bool AcceptsPackage(const PackageInfoRef& package) const 104 { 105 if (package.Get() == NULL) 106 return false; 107 108 const CategoryList& categories = package->Categories(); 109 for (int i = categories.CountItems() - 1; i >= 0; i--) { 110 const CategoryRef& category = categories.ItemAtFast(i); 111 if (category.Get() == NULL) 112 continue; 113 if (category->Code() == fCategory) 114 return true; 115 } 116 return false; 117 } 118 119 const BString& Category() const 120 { 121 return fCategory; 122 } 123 124 private: 125 BString fCategory; 126 }; 127 128 129 class ContainedInFilter : public PackageFilter { 130 public: 131 ContainedInFilter(const PackageList& packageList) 132 : 133 fPackageList(packageList) 134 { 135 } 136 137 virtual bool AcceptsPackage(const PackageInfoRef& package) const 138 { 139 return fPackageList.Contains(package); 140 } 141 142 private: 143 const PackageList& fPackageList; 144 }; 145 146 147 class ContainedInEitherFilter : public PackageFilter { 148 public: 149 ContainedInEitherFilter(const PackageList& packageListA, 150 const PackageList& packageListB) 151 : 152 fPackageListA(packageListA), 153 fPackageListB(packageListB) 154 { 155 } 156 157 virtual bool AcceptsPackage(const PackageInfoRef& package) const 158 { 159 return fPackageListA.Contains(package) 160 || fPackageListB.Contains(package); 161 } 162 163 private: 164 const PackageList& fPackageListA; 165 const PackageList& fPackageListB; 166 }; 167 168 169 class NotContainedInFilter : public PackageFilter { 170 public: 171 NotContainedInFilter(const PackageList* packageList, ...) 172 { 173 va_list args; 174 va_start(args, packageList); 175 while (true) { 176 const PackageList* packageList = va_arg(args, const PackageList*); 177 if (packageList == NULL) 178 break; 179 fPackageLists.Add(packageList); 180 } 181 va_end(args); 182 } 183 184 virtual bool AcceptsPackage(const PackageInfoRef& package) const 185 { 186 if (package.Get() == NULL) 187 return false; 188 189 for (int32 i = 0; i < fPackageLists.CountItems(); i++) { 190 if (fPackageLists.ItemAtFast(i)->Contains(package)) 191 return false; 192 } 193 return true; 194 } 195 196 private: 197 List<const PackageList*, true> fPackageLists; 198 }; 199 200 201 class StateFilter : public PackageFilter { 202 public: 203 StateFilter(PackageState state) 204 : 205 fState(state) 206 { 207 } 208 209 virtual bool AcceptsPackage(const PackageInfoRef& package) const 210 { 211 return package->State() == NONE; 212 } 213 214 private: 215 PackageState fState; 216 }; 217 218 219 class SearchTermsFilter : public PackageFilter { 220 public: 221 SearchTermsFilter(const BString& searchTerms) 222 { 223 // Separate the string into terms at spaces 224 int32 index = 0; 225 while (index < searchTerms.Length()) { 226 int32 nextSpace = searchTerms.FindFirst(" ", index); 227 if (nextSpace < 0) 228 nextSpace = searchTerms.Length(); 229 if (nextSpace > index) { 230 BString term; 231 searchTerms.CopyInto(term, index, nextSpace - index); 232 term.ToLower(); 233 fSearchTerms.Add(term); 234 } 235 index = nextSpace + 1; 236 } 237 } 238 239 virtual bool AcceptsPackage(const PackageInfoRef& package) const 240 { 241 if (package.Get() == NULL) 242 return false; 243 // Every search term must be found in one of the package texts 244 for (int32 i = fSearchTerms.CountItems() - 1; i >= 0; i--) { 245 const BString& term = fSearchTerms.ItemAtFast(i); 246 if (!_TextContains(package->Name(), term) 247 && !_TextContains(package->Title(), term) 248 && !_TextContains(package->Publisher().Name(), term) 249 && !_TextContains(package->ShortDescription(), term) 250 && !_TextContains(package->FullDescription(), term)) { 251 return false; 252 } 253 } 254 return true; 255 } 256 257 BString SearchTerms() const 258 { 259 BString searchTerms; 260 for (int32 i = 0; i < fSearchTerms.CountItems(); i++) { 261 const BString& term = fSearchTerms.ItemAtFast(i); 262 if (term.IsEmpty()) 263 continue; 264 if (!searchTerms.IsEmpty()) 265 searchTerms.Append(" "); 266 searchTerms.Append(term); 267 } 268 return searchTerms; 269 } 270 271 private: 272 bool _TextContains(BString text, const BString& string) const 273 { 274 text.ToLower(); 275 int32 index = text.FindFirst(string); 276 return index >= 0; 277 } 278 279 private: 280 StringList fSearchTerms; 281 }; 282 283 284 static inline bool 285 is_source_package(const PackageInfoRef& package) 286 { 287 const BString& packageName = package->Name(); 288 return packageName.EndsWith("_source"); 289 } 290 291 292 static inline bool 293 is_develop_package(const PackageInfoRef& package) 294 { 295 const BString& packageName = package->Name(); 296 return packageName.EndsWith("_devel") 297 || packageName.EndsWith("_debuginfo"); 298 } 299 300 301 // #pragma mark - Model 302 303 304 static int32 305 PackageCategoryCompareFn(const CategoryRef& c1, const CategoryRef& c2) 306 { 307 BCollator* collator = LocaleUtils::GetSharedCollator(); 308 int32 result = collator->Compare(c1->Name().String(), 309 c2->Name().String()); 310 if (result == 0) 311 result = c1->Code().Compare(c2->Code()); 312 return result; 313 } 314 315 316 Model::Model() 317 : 318 fDepots(), 319 fCategories(&PackageCategoryCompareFn, NULL), 320 fCategoryFilter(PackageFilterRef(new AnyFilter(), true)), 321 fDepotFilter(""), 322 fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)), 323 fPackageListViewMode(PROMINENT), 324 fShowAvailablePackages(true), 325 fShowInstalledPackages(true), 326 fShowSourcePackages(false), 327 fShowDevelopPackages(false) 328 { 329 } 330 331 332 Model::~Model() 333 { 334 } 335 336 337 LanguageModel& 338 Model::Language() 339 { 340 return fLanguageModel; 341 } 342 343 344 bool 345 Model::AddListener(const ModelListenerRef& listener) 346 { 347 return fListeners.Add(listener); 348 } 349 350 351 // TODO; part of a wider change; cope with the package being in more than one 352 // depot 353 PackageInfoRef 354 Model::PackageForName(const BString& name) 355 { 356 DepotList depots = Depots(); 357 for (int32 d = 0; d < depots.CountItems(); d++) { 358 const DepotInfo& depot = depots.ItemAtFast(d); 359 int32 packageIndex = depot.PackageIndexByName(name); 360 if (packageIndex >= 0) 361 return depot.Packages().ItemAtFast(packageIndex); 362 } 363 return PackageInfoRef(); 364 } 365 366 367 bool 368 Model::MatchesFilter(const PackageInfoRef& package) const 369 { 370 return fCategoryFilter->AcceptsPackage(package) 371 && fSearchTermsFilter->AcceptsPackage(package) 372 && (fDepotFilter.IsEmpty() || fDepotFilter == package->DepotName()) 373 && (fShowAvailablePackages || package->State() != NONE) 374 && (fShowInstalledPackages || package->State() != ACTIVATED) 375 && (fShowSourcePackages || !is_source_package(package)) 376 && (fShowDevelopPackages || !is_develop_package(package)); 377 } 378 379 380 bool 381 Model::AddDepot(const DepotInfo& depot) 382 { 383 return fDepots.Add(depot); 384 } 385 386 387 bool 388 Model::HasDepot(const BString& name) const 389 { 390 return NULL != DepotForName(name); 391 } 392 393 394 const DepotInfo* 395 Model::DepotForName(const BString& name) const 396 { 397 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 398 if (fDepots.ItemAtFast(i).Name() == name) 399 return &fDepots.ItemAtFast(i); 400 } 401 return NULL; 402 } 403 404 405 bool 406 Model::SyncDepot(const DepotInfo& depot) 407 { 408 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 409 const DepotInfo& existingDepot = fDepots.ItemAtFast(i); 410 if (existingDepot.Name() == depot.Name()) { 411 DepotInfo mergedDepot(existingDepot); 412 mergedDepot.SyncPackages(depot.Packages()); 413 fDepots.Replace(i, mergedDepot); 414 return true; 415 } 416 } 417 return false; 418 } 419 420 421 bool 422 Model::HasAnyProminentPackages() 423 { 424 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 425 const DepotInfo& existingDepot = fDepots.ItemAtFast(i); 426 if (existingDepot.HasAnyProminentPackages()) 427 return true; 428 } 429 return false; 430 } 431 432 433 void 434 Model::Clear() 435 { 436 fDepots.Clear(); 437 } 438 439 440 void 441 Model::SetPackageState(const PackageInfoRef& package, PackageState state) 442 { 443 switch (state) { 444 default: 445 case NONE: 446 fInstalledPackages.Remove(package); 447 fActivatedPackages.Remove(package); 448 fUninstalledPackages.Remove(package); 449 break; 450 case INSTALLED: 451 if (!fInstalledPackages.Contains(package)) 452 fInstalledPackages.Add(package); 453 fActivatedPackages.Remove(package); 454 fUninstalledPackages.Remove(package); 455 break; 456 case ACTIVATED: 457 if (!fInstalledPackages.Contains(package)) 458 fInstalledPackages.Add(package); 459 if (!fActivatedPackages.Contains(package)) 460 fActivatedPackages.Add(package); 461 fUninstalledPackages.Remove(package); 462 break; 463 case UNINSTALLED: 464 fInstalledPackages.Remove(package); 465 fActivatedPackages.Remove(package); 466 if (!fUninstalledPackages.Contains(package)) 467 fUninstalledPackages.Add(package); 468 break; 469 } 470 471 package->SetState(state); 472 } 473 474 475 // #pragma mark - filters 476 477 478 void 479 Model::SetCategory(const BString& category) 480 { 481 PackageFilter* filter; 482 483 if (category.Length() == 0) 484 filter = new AnyFilter(); 485 else 486 filter = new CategoryFilter(category); 487 488 fCategoryFilter.SetTo(filter, true); 489 } 490 491 492 BString 493 Model::Category() const 494 { 495 CategoryFilter* filter 496 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 497 if (filter == NULL) 498 return ""; 499 return filter->Category(); 500 } 501 502 503 void 504 Model::SetDepot(const BString& depot) 505 { 506 fDepotFilter = depot; 507 } 508 509 510 BString 511 Model::Depot() const 512 { 513 return fDepotFilter; 514 } 515 516 517 void 518 Model::SetSearchTerms(const BString& searchTerms) 519 { 520 PackageFilter* filter; 521 522 if (searchTerms.Length() == 0) 523 filter = new AnyFilter(); 524 else 525 filter = new SearchTermsFilter(searchTerms); 526 527 fSearchTermsFilter.SetTo(filter, true); 528 } 529 530 531 BString 532 Model::SearchTerms() const 533 { 534 SearchTermsFilter* filter 535 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 536 if (filter == NULL) 537 return ""; 538 return filter->SearchTerms(); 539 } 540 541 542 void 543 Model::SetPackageListViewMode(package_list_view_mode mode) 544 { 545 fPackageListViewMode = mode; 546 } 547 548 549 void 550 Model::SetShowAvailablePackages(bool show) 551 { 552 fShowAvailablePackages = show; 553 } 554 555 556 void 557 Model::SetShowInstalledPackages(bool show) 558 { 559 fShowInstalledPackages = show; 560 } 561 562 563 void 564 Model::SetShowSourcePackages(bool show) 565 { 566 fShowSourcePackages = show; 567 } 568 569 570 void 571 Model::SetShowDevelopPackages(bool show) 572 { 573 fShowDevelopPackages = show; 574 } 575 576 577 // #pragma mark - information retrieval 578 579 580 /*! Initially only superficial data is loaded from the server into the data 581 model of the packages. When the package is viewed, additional data needs 582 to be populated including ratings. This method takes care of that. 583 */ 584 585 void 586 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 587 { 588 // TODO: There should probably also be a way to "unpopulate" the 589 // package information. Maybe a cache of populated packages, so that 590 // packages loose their extra information after a certain amount of 591 // time when they have not been accessed/displayed in the UI. Otherwise 592 // HaikuDepot will consume more and more resources in the packages. 593 // Especially screen-shots will be a problem eventually. 594 { 595 BAutolock locker(&fLock); 596 bool alreadyPopulated = fPopulatedPackages.Contains(package); 597 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 598 return; 599 if (!alreadyPopulated) 600 fPopulatedPackages.Add(package); 601 } 602 603 if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) { 604 _PopulatePackageChangelog(package); 605 } 606 607 if ((flags & POPULATE_USER_RATINGS) != 0) { 608 // Retrieve info from web-app 609 BMessage info; 610 611 BString packageName; 612 BString webAppRepositoryCode; 613 { 614 BAutolock locker(&fLock); 615 packageName = package->Name(); 616 const DepotInfo* depot = DepotForName(package->DepotName()); 617 618 if (depot != NULL) 619 webAppRepositoryCode = depot->WebAppRepositoryCode(); 620 } 621 622 status_t status = fWebAppInterface 623 .RetreiveUserRatingsForPackageForDisplay(packageName, 624 webAppRepositoryCode, 0, PACKAGE_INFO_MAX_USER_RATINGS, 625 info); 626 if (status == B_OK) { 627 // Parse message 628 BMessage result; 629 BMessage items; 630 if (info.FindMessage("result", &result) == B_OK 631 && result.FindMessage("items", &items) == B_OK) { 632 633 BAutolock locker(&fLock); 634 package->ClearUserRatings(); 635 636 int32 index = 0; 637 while (true) { 638 BString name; 639 name << index++; 640 641 BMessage item; 642 if (items.FindMessage(name, &item) != B_OK) 643 break; 644 645 BString code; 646 if (item.FindString("code", &code) != B_OK) { 647 HDERROR("corrupt user rating at index %" B_PRIi32, 648 index) 649 continue; 650 } 651 652 BString user; 653 BMessage userInfo; 654 if (item.FindMessage("user", &userInfo) != B_OK 655 || userInfo.FindString("nickname", &user) != B_OK) { 656 HDERROR("ignored user rating [%s] without a user " 657 "nickname", code.String()) 658 continue; 659 } 660 661 // Extract basic info, all items are optional 662 BString languageCode; 663 BString comment; 664 double rating; 665 item.FindString("naturalLanguageCode", &languageCode); 666 item.FindString("comment", &comment); 667 if (item.FindDouble("rating", &rating) != B_OK) 668 rating = -1; 669 if (comment.Length() == 0 && rating == -1) { 670 HDERROR("rating [%s] has no comment or rating so will" 671 " be ignored", code.String()) 672 continue; 673 } 674 675 // For which version of the package was the rating? 676 BString major = "?"; 677 BString minor = "?"; 678 BString micro = ""; 679 double revision = -1; 680 BString architectureCode = ""; 681 BMessage version; 682 if (item.FindMessage("pkgVersion", &version) == B_OK) { 683 version.FindString("major", &major); 684 version.FindString("minor", &minor); 685 version.FindString("micro", µ); 686 version.FindDouble("revision", &revision); 687 version.FindString("architectureCode", 688 &architectureCode); 689 } 690 BString versionString = major; 691 versionString << "."; 692 versionString << minor; 693 if (!micro.IsEmpty()) { 694 versionString << "."; 695 versionString << micro; 696 } 697 if (revision > 0) { 698 versionString << "-"; 699 versionString << (int) revision; 700 } 701 702 if (!architectureCode.IsEmpty()) { 703 versionString << " " << STR_MDASH << " "; 704 versionString << architectureCode; 705 } 706 707 double createTimestamp; 708 item.FindDouble("createTimestamp", &createTimestamp); 709 710 // Add the rating to the PackageInfo 711 UserRating userRating = UserRating(UserInfo(user), rating, 712 comment, languageCode, versionString, 713 (uint64) createTimestamp); 714 package->AddUserRating(userRating); 715 HDDEBUG("rating [%s] retrieved from server", code.String()) 716 } 717 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 718 index - 1, packageName.String()) 719 } else { 720 _MaybeLogJsonRpcError(info, "retrieve user ratings"); 721 } 722 } else { 723 HDERROR("unable to retrieve user ratings") 724 } 725 } 726 727 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 728 ScreenshotInfoList screenshotInfos; 729 { 730 BAutolock locker(&fLock); 731 screenshotInfos = package->ScreenshotInfos(); 732 package->ClearScreenshots(); 733 } 734 for (int i = 0; i < screenshotInfos.CountItems(); i++) { 735 const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i); 736 _PopulatePackageScreenshot(package, info, 320, false); 737 } 738 } 739 } 740 741 742 void 743 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 744 { 745 BMessage info; 746 BString packageName; 747 748 { 749 BAutolock locker(&fLock); 750 packageName = package->Name(); 751 } 752 753 status_t status = fWebAppInterface.GetChangelog(packageName, info); 754 755 if (status == B_OK) { 756 // Parse message 757 BMessage result; 758 BString content; 759 if (info.FindMessage("result", &result) == B_OK) { 760 if (result.FindString("content", &content) == B_OK 761 && 0 != content.Length()) { 762 BAutolock locker(&fLock); 763 package->SetChangelog(content); 764 HDDEBUG("changelog populated for [%s]", packageName.String()) 765 } else { 766 HDDEBUG("no changelog present for [%s]", packageName.String()) 767 } 768 } else { 769 _MaybeLogJsonRpcError(info, "populate package changelog"); 770 } 771 } else { 772 HDERROR("unable to obtain the changelog for the package [%s]", 773 packageName.String()) 774 } 775 } 776 777 778 static void 779 model_remove_key_for_user(const BString& nickname) 780 { 781 if (nickname.IsEmpty()) 782 return; 783 BKeyStore keyStore; 784 BPasswordKey key; 785 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 786 << nickname; 787 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 788 passwordIdentifier, key); 789 790 switch (result) { 791 case B_OK: 792 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 793 if (result != B_OK) { 794 HDERROR("error occurred when removing password for nickname " 795 "[%s] : %s", nickname.String(), strerror(result)) 796 } 797 break; 798 case B_ENTRY_NOT_FOUND: 799 return; 800 default: 801 HDERROR("error occurred when finding password for nickname " 802 "[%s] : %s", nickname.String(), strerror(result)) 803 break; 804 } 805 } 806 807 808 void 809 Model::SetNickname(BString nickname) 810 { 811 BString password; 812 BString existingNickname = Nickname(); 813 814 // this happens when the user is logging out. Best to remove the password 815 // stored for the existing user since it is no longer required. 816 817 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 818 model_remove_key_for_user(existingNickname); 819 820 if (nickname.Length() > 0) { 821 BPasswordKey key; 822 BKeyStore keyStore; 823 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 824 << nickname; 825 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 826 passwordIdentifier, key) == B_OK) { 827 password = key.Password(); 828 } 829 if (password.IsEmpty()) 830 nickname = ""; 831 } 832 833 SetAuthorization(nickname, password, false); 834 } 835 836 837 const BString& 838 Model::Nickname() const 839 { 840 return fWebAppInterface.Nickname(); 841 } 842 843 844 void 845 Model::SetAuthorization(const BString& nickname, const BString& passwordClear, 846 bool storePassword) 847 { 848 BString existingNickname = Nickname(); 849 850 if (storePassword) { 851 // no point continuing to store the password for the previous user. 852 853 if (!existingNickname.IsEmpty()) 854 model_remove_key_for_user(existingNickname); 855 856 // adding a key that is already there does not seem to override the 857 // existing key so the old key needs to be removed first. 858 859 if (!nickname.IsEmpty()) 860 model_remove_key_for_user(nickname); 861 862 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 863 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 864 << nickname; 865 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 866 BKeyStore keyStore; 867 keyStore.AddKeyring(kHaikuDepotKeyring); 868 keyStore.AddKey(kHaikuDepotKeyring, key); 869 } 870 } 871 872 BAutolock locker(&fLock); 873 fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); 874 875 if (nickname != existingNickname) 876 _NotifyAuthorizationChanged(); 877 } 878 879 880 /*! When bulk repository data comes down from the server, it will 881 arrive as a json.gz payload. This is stored locally as a cache 882 and this method will provide the on-disk storage location for 883 this file. 884 */ 885 886 status_t 887 Model::DumpExportRepositoryDataPath(BPath& path) const 888 { 889 BString leaf; 890 leaf.SetToFormat("repository-all_%s.json.gz", 891 LanguageModel().PreferredLanguage()->Code()); 892 return StorageUtils::LocalWorkingFilesPath(leaf, path); 893 } 894 895 896 /*! When the system downloads reference data (eg; categories) from the server 897 then the downloaded data is stored and cached at the path defined by this 898 method. 899 */ 900 901 status_t 902 Model::DumpExportReferenceDataPath(BPath& path) const 903 { 904 BString leaf; 905 leaf.SetToFormat("reference-all_%s.json.gz", 906 LanguageModel().PreferredLanguage()->Code()); 907 return StorageUtils::LocalWorkingFilesPath(leaf, path); 908 } 909 910 911 status_t 912 Model::IconStoragePath(BPath& path) const 913 { 914 return StorageUtils::LocalWorkingDirectoryPath("__allicons", path); 915 } 916 917 918 status_t 919 Model::DumpExportPkgDataPath(BPath& path, 920 const BString& repositorySourceCode) const 921 { 922 BString leaf; 923 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 924 LanguageModel().PreferredLanguage()->Code()); 925 return StorageUtils::LocalWorkingFilesPath(leaf, path); 926 } 927 928 929 void 930 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 931 const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly) 932 { 933 // See if there is a cached screenshot 934 BFile screenshotFile; 935 BPath screenshotCachePath; 936 937 status_t result = StorageUtils::LocalWorkingDirectoryPath( 938 "Screenshots", screenshotCachePath); 939 940 if (result != B_OK) { 941 HDERROR("unable to get the screenshot dir - unable to proceed") 942 return; 943 } 944 945 bool fileExists = false; 946 BString screenshotName(info.Code()); 947 screenshotName << "@" << scaledWidth; 948 screenshotName << ".png"; 949 time_t modifiedTime; 950 if (screenshotCachePath.Append(screenshotName) == B_OK) { 951 // Try opening the file in read-only mode, which will fail if its 952 // not a file or does not exist. 953 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 954 B_READ_ONLY) == B_OK; 955 if (fileExists) 956 screenshotFile.GetModificationTime(&modifiedTime); 957 } 958 959 if (fileExists) { 960 time_t now; 961 time(&now); 962 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 963 // Cache file is recent enough, just use it and return. 964 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 965 true); 966 BAutolock locker(&fLock); 967 package->AddScreenshot(bitmapRef); 968 return; 969 } 970 } 971 972 if (fromCacheOnly) 973 return; 974 975 // Retrieve screenshot from web-app 976 BMallocIO buffer; 977 978 int32 scaledHeight = scaledWidth * info.Height() / info.Width(); 979 980 status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(), 981 scaledWidth, scaledHeight, &buffer); 982 if (status == B_OK) { 983 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 984 BAutolock locker(&fLock); 985 package->AddScreenshot(bitmapRef); 986 locker.Unlock(); 987 if (screenshotFile.SetTo(screenshotCachePath.Path(), 988 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 989 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 990 } 991 } else { 992 HDERROR("Failed to retrieve screenshot for code '%s' " 993 "at %" B_PRIi32 "x%" B_PRIi32 ".", info.Code().String(), 994 scaledWidth, scaledHeight) 995 } 996 } 997 998 999 // #pragma mark - listener notification methods 1000 1001 1002 void 1003 Model::_NotifyAuthorizationChanged() 1004 { 1005 for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) { 1006 const ModelListenerRef& listener = fListeners.ItemAtFast(i); 1007 if (listener.Get() != NULL) 1008 listener->AuthorizationChanged(); 1009 } 1010 } 1011 1012 1013 void 1014 Model::_NotifyCategoryListChanged() 1015 { 1016 for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) { 1017 const ModelListenerRef& listener = fListeners.ItemAtFast(i); 1018 if (listener.Get() != NULL) 1019 listener->CategoryListChanged(); 1020 } 1021 } 1022 1023 1024 1025 /*! This method will find the stored 'DepotInfo' that correlates to the 1026 supplied 'identifier' and will invoke the mapper function in order 1027 to get a replacement for the 'DepotInfo'. The 'identifier' holds 1028 across mirrors. 1029 */ 1030 1031 void 1032 Model::ReplaceDepotByIdentifier(const BString& identifier, 1033 DepotMapper* depotMapper, void* context) 1034 { 1035 for (int32 i = 0; i < fDepots.CountItems(); i++) { 1036 DepotInfo depotInfo = fDepots.ItemAtFast(i); 1037 1038 if (identifier == depotInfo.URL()) { 1039 BAutolock locker(&fLock); 1040 fDepots.Replace(i, depotMapper->MapDepot(depotInfo, context)); 1041 } 1042 } 1043 } 1044 1045 1046 void 1047 Model::LogDepotsWithNoWebAppRepositoryCode() const 1048 { 1049 int32 i; 1050 1051 for (i = 0; i < fDepots.CountItems(); i++) { 1052 const DepotInfo& depot = fDepots.ItemAt(i); 1053 1054 if (depot.WebAppRepositoryCode().Length() == 0) { 1055 if (depot.URL().Length() > 0) { 1056 HDINFO("depot [%s] (%s) correlates with no repository in the" 1057 " the haiku depot server system", depot.Name().String(), 1058 depot.URL().String()) 1059 } 1060 else { 1061 HDINFO("depot [%s] correlates with no repository in the" 1062 " the haiku depot server system", depot.Name().String()) 1063 } 1064 } 1065 } 1066 } 1067 1068 1069 void 1070 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 1071 const char *sourceDescription) const 1072 { 1073 BMessage error; 1074 BString errorMessage; 1075 double errorCode; 1076 1077 if (responsePayload.FindMessage("error", &error) == B_OK 1078 && error.FindString("message", &errorMessage) == B_OK 1079 && error.FindDouble("code", &errorCode) == B_OK) { 1080 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 1081 errorMessage.String(), errorCode) 1082 } else { 1083 HDERROR("[%s] --> an undefined error has occurred", sourceDescription) 1084 } 1085 } 1086 1087 1088 void 1089 Model::AddCategories(const CategoryList& categories) 1090 { 1091 int32 i; 1092 for (i = 0; i < categories.CountItems(); i++) 1093 _AddCategory(categories.ItemAt(i)); 1094 _NotifyCategoryListChanged(); 1095 } 1096 1097 1098 void 1099 Model::_AddCategory(const CategoryRef& category) 1100 { 1101 int32 i; 1102 for (i = 0; i < fCategories.CountItems(); i++) { 1103 if (fCategories.ItemAt(i)->Code() == category->Code()) { 1104 fCategories.Replace(i, category); 1105 return; 1106 } 1107 } 1108 1109 fCategories.Add(category); 1110 } 1111