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