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