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