1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * Copyright 2016-2018, 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 357 fShowFeaturedPackages(true), 358 fShowAvailablePackages(true), 359 fShowInstalledPackages(true), 360 fShowSourcePackages(false), 361 fShowDevelopPackages(false) 362 { 363 _UpdateIsFeaturedFilter(); 364 365 // Don't forget to add new categories to this list: 366 fCategories.Add(fCategoryGames); 367 fCategories.Add(fCategoryBusiness); 368 fCategories.Add(fCategoryAudio); 369 fCategories.Add(fCategoryVideo); 370 fCategories.Add(fCategoryGraphics); 371 fCategories.Add(fCategoryEducation); 372 fCategories.Add(fCategoryProductivity); 373 fCategories.Add(fCategorySystemAndUtilities); 374 fCategories.Add(fCategoryInternetAndNetwork); 375 fCategories.Add(fCategoryDevelopment); 376 fCategories.Add(fCategoryScienceAndMathematics); 377 // TODO: The server will eventually support an API to 378 // get the defined categories and their translated names. 379 // This should then be used instead of hard-coded 380 // categories and translations in the app. 381 382 fPreferredLanguage = "en"; 383 BLocaleRoster* localeRoster = BLocaleRoster::Default(); 384 if (localeRoster != NULL) { 385 BMessage preferredLanguages; 386 if (localeRoster->GetPreferredLanguages(&preferredLanguages) == B_OK) { 387 BString language; 388 if (preferredLanguages.FindString("language", 0, &language) == B_OK) 389 language.CopyInto(fPreferredLanguage, 0, 2); 390 } 391 } 392 393 // TODO: Fetch this from the web-app. 394 fSupportedLanguages.Add("en"); 395 fSupportedLanguages.Add("de"); 396 fSupportedLanguages.Add("fr"); 397 fSupportedLanguages.Add("ja"); 398 fSupportedLanguages.Add("es"); 399 fSupportedLanguages.Add("zh"); 400 fSupportedLanguages.Add("pt"); 401 fSupportedLanguages.Add("ru"); 402 403 if (!fSupportedLanguages.Contains(fPreferredLanguage)) { 404 // Force the preferred language to one of the currently supported 405 // ones, until the web application supports all ISO language codes. 406 printf("User preferred language '%s' not currently supported, " 407 "defaulting to 'en'.", fPreferredLanguage.String()); 408 fPreferredLanguage = "en"; 409 } 410 fWebAppInterface.SetPreferredLanguage(fPreferredLanguage); 411 } 412 413 414 Model::~Model() 415 { 416 } 417 418 419 bool 420 Model::AddListener(const ModelListenerRef& listener) 421 { 422 return fListeners.Add(listener); 423 } 424 425 426 PackageList 427 Model::CreatePackageList() const 428 { 429 // Iterate all packages from all depots. 430 // If configured, restrict depot, filter by search terms, status, name ... 431 PackageList resultList; 432 433 for (int32 i = 0; i < fDepots.CountItems(); i++) { 434 const DepotInfo& depot = fDepots.ItemAtFast(i); 435 436 if (fDepotFilter.Length() > 0 && fDepotFilter != depot.Name()) 437 continue; 438 439 const PackageList& packages = depot.Packages(); 440 441 for (int32 j = 0; j < packages.CountItems(); j++) { 442 const PackageInfoRef& package = packages.ItemAtFast(j); 443 if (MatchesFilter(package)) 444 resultList.Add(package); 445 } 446 } 447 448 return resultList; 449 } 450 451 452 bool 453 Model::MatchesFilter(const PackageInfoRef& package) const 454 { 455 return fCategoryFilter->AcceptsPackage(package) 456 && fSearchTermsFilter->AcceptsPackage(package) 457 && fIsFeaturedFilter->AcceptsPackage(package) 458 && (fShowAvailablePackages || package->State() != NONE) 459 && (fShowInstalledPackages || package->State() != ACTIVATED) 460 && (fShowSourcePackages || !is_source_package(package)) 461 && (fShowDevelopPackages || !is_develop_package(package)); 462 } 463 464 465 bool 466 Model::AddDepot(const DepotInfo& depot) 467 { 468 return fDepots.Add(depot); 469 } 470 471 472 bool 473 Model::HasDepot(const BString& name) const 474 { 475 return NULL != DepotForName(name); 476 } 477 478 479 const DepotInfo* 480 Model::DepotForName(const BString& name) const 481 { 482 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 483 if (fDepots.ItemAtFast(i).Name() == name) 484 return &fDepots.ItemAtFast(i); 485 } 486 return NULL; 487 } 488 489 490 bool 491 Model::SyncDepot(const DepotInfo& depot) 492 { 493 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 494 const DepotInfo& existingDepot = fDepots.ItemAtFast(i); 495 if (existingDepot.Name() == depot.Name()) { 496 DepotInfo mergedDepot(existingDepot); 497 mergedDepot.SyncPackages(depot.Packages()); 498 fDepots.Replace(i, mergedDepot); 499 return true; 500 } 501 } 502 return false; 503 } 504 505 506 void 507 Model::Clear() 508 { 509 fDepots.Clear(); 510 } 511 512 513 void 514 Model::SetPackageState(const PackageInfoRef& package, PackageState state) 515 { 516 switch (state) { 517 default: 518 case NONE: 519 fInstalledPackages.Remove(package); 520 fActivatedPackages.Remove(package); 521 fUninstalledPackages.Remove(package); 522 break; 523 case INSTALLED: 524 if (!fInstalledPackages.Contains(package)) 525 fInstalledPackages.Add(package); 526 fActivatedPackages.Remove(package); 527 fUninstalledPackages.Remove(package); 528 break; 529 case ACTIVATED: 530 if (!fInstalledPackages.Contains(package)) 531 fInstalledPackages.Add(package); 532 if (!fActivatedPackages.Contains(package)) 533 fActivatedPackages.Add(package); 534 fUninstalledPackages.Remove(package); 535 break; 536 case UNINSTALLED: 537 fInstalledPackages.Remove(package); 538 fActivatedPackages.Remove(package); 539 if (!fUninstalledPackages.Contains(package)) 540 fUninstalledPackages.Add(package); 541 break; 542 } 543 544 package->SetState(state); 545 } 546 547 548 // #pragma mark - filters 549 550 551 void 552 Model::SetCategory(const BString& category) 553 { 554 PackageFilter* filter; 555 556 if (category.Length() == 0) 557 filter = new AnyFilter(); 558 else 559 filter = new CategoryFilter(category); 560 561 fCategoryFilter.SetTo(filter, true); 562 } 563 564 565 BString 566 Model::Category() const 567 { 568 CategoryFilter* filter 569 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 570 if (filter == NULL) 571 return ""; 572 return filter->Category(); 573 } 574 575 576 void 577 Model::SetDepot(const BString& depot) 578 { 579 fDepotFilter = depot; 580 } 581 582 583 BString 584 Model::Depot() const 585 { 586 return fDepotFilter; 587 } 588 589 590 void 591 Model::SetSearchTerms(const BString& searchTerms) 592 { 593 PackageFilter* filter; 594 595 if (searchTerms.Length() == 0) 596 filter = new AnyFilter(); 597 else 598 filter = new SearchTermsFilter(searchTerms); 599 600 fSearchTermsFilter.SetTo(filter, true); 601 _UpdateIsFeaturedFilter(); 602 } 603 604 605 BString 606 Model::SearchTerms() const 607 { 608 SearchTermsFilter* filter 609 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 610 if (filter == NULL) 611 return ""; 612 return filter->SearchTerms(); 613 } 614 615 616 void 617 Model::SetShowFeaturedPackages(bool show) 618 { 619 fShowFeaturedPackages = show; 620 _UpdateIsFeaturedFilter(); 621 } 622 623 624 void 625 Model::SetShowAvailablePackages(bool show) 626 { 627 fShowAvailablePackages = show; 628 } 629 630 631 void 632 Model::SetShowInstalledPackages(bool show) 633 { 634 fShowInstalledPackages = show; 635 } 636 637 638 void 639 Model::SetShowSourcePackages(bool show) 640 { 641 fShowSourcePackages = show; 642 } 643 644 645 void 646 Model::SetShowDevelopPackages(bool show) 647 { 648 fShowDevelopPackages = show; 649 } 650 651 652 // #pragma mark - information retrieval 653 654 655 /*! Initially only superficial data is loaded from the server into the data 656 model of the packages. When the package is viewed, additional data needs 657 to be populated including ratings. This method takes care of that. 658 */ 659 660 void 661 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 662 { 663 // TODO: There should probably also be a way to "unpopulate" the 664 // package information. Maybe a cache of populated packages, so that 665 // packages loose their extra information after a certain amount of 666 // time when they have not been accessed/displayed in the UI. Otherwise 667 // HaikuDepot will consume more and more resources in the packages. 668 // Especially screen-shots will be a problem eventually. 669 { 670 BAutolock locker(&fLock); 671 bool alreadyPopulated = fPopulatedPackages.Contains(package); 672 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 673 return; 674 if (!alreadyPopulated) 675 fPopulatedPackages.Add(package); 676 } 677 678 if ((flags & POPULATE_CHANGELOG) != 0) { 679 _PopulatePackageChangelog(package); 680 } 681 682 if ((flags & POPULATE_USER_RATINGS) != 0) { 683 // Retrieve info from web-app 684 BMessage info; 685 686 BString packageName; 687 BString architecture; 688 { 689 BAutolock locker(&fLock); 690 packageName = package->Name(); 691 architecture = package->Architecture(); 692 } 693 694 status_t status = fWebAppInterface.RetrieveUserRatings(packageName, 695 architecture, 0, 50, info); 696 if (status == B_OK) { 697 // Parse message 698 BMessage result; 699 BMessage items; 700 if (info.FindMessage("result", &result) == B_OK 701 && result.FindMessage("items", &items) == B_OK) { 702 703 BAutolock locker(&fLock); 704 package->ClearUserRatings(); 705 706 int32 index = 0; 707 while (true) { 708 BString name; 709 name << index++; 710 711 BMessage item; 712 if (items.FindMessage(name, &item) != B_OK) 713 break; 714 715 BString code; 716 if (item.FindString("code", &code) != B_OK) { 717 printf("corrupt user rating at index %" B_PRIi32 "\n", 718 index); 719 continue; 720 } 721 722 BString user; 723 BMessage userInfo; 724 if (item.FindMessage("user", &userInfo) != B_OK 725 || userInfo.FindString("nickname", &user) != B_OK) { 726 printf("ignored user rating [%s] without a user " 727 "nickname\n", code.String()); 728 continue; 729 } 730 731 // Extract basic info, all items are optional 732 BString languageCode; 733 BString comment; 734 double rating; 735 item.FindString("naturalLanguageCode", &languageCode); 736 item.FindString("comment", &comment); 737 if (item.FindDouble("rating", &rating) != B_OK) 738 rating = -1; 739 if (comment.Length() == 0 && rating == -1) { 740 printf("rating [%s] has no comment or rating so will be" 741 "ignored\n", code.String()); 742 continue; 743 } 744 745 // For which version of the package was the rating? 746 BString major = "?"; 747 BString minor = "?"; 748 BString micro = ""; 749 double revision = -1; 750 BMessage version; 751 if (item.FindMessage("pkgVersion", &version) == B_OK) { 752 version.FindString("major", &major); 753 version.FindString("minor", &minor); 754 version.FindString("micro", µ); 755 version.FindDouble("revision", &revision); 756 } 757 BString versionString = major; 758 versionString << "."; 759 versionString << minor; 760 if (micro.Length() > 0) { 761 versionString << "."; 762 versionString << micro; 763 } 764 if (revision > 0) { 765 versionString << "-"; 766 versionString << (int) revision; 767 } 768 769 BDateTime createTimestamp; 770 double createTimestampMillisF; 771 if (item.FindDouble("createTimestamp", 772 &createTimestampMillisF) == B_OK) { 773 double createTimestampSecsF = 774 createTimestampMillisF / 1000.0; 775 time_t createTimestampSecs = 776 (time_t) createTimestampSecsF; 777 createTimestamp.SetTime_t(createTimestampSecs); 778 } 779 780 // Add the rating to the PackageInfo 781 UserRating userRating = UserRating(UserInfo(user), rating, 782 comment, languageCode, versionString, 0, 0, 783 createTimestamp); 784 package->AddUserRating(userRating); 785 786 if (Logger::IsDebugEnabled()) { 787 printf("rating [%s] retrieved from server\n", 788 code.String()); 789 } 790 } 791 792 if (Logger::IsDebugEnabled()) { 793 printf("did retrieve %" B_PRIi32 " user ratings for [%s]\n", 794 index - 1, packageName.String()); 795 } 796 } else { 797 _MaybeLogJsonRpcError(info, "retrieve user ratings"); 798 } 799 } else { 800 printf("unable to retrieve user ratings\n"); 801 } 802 } 803 804 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 805 ScreenshotInfoList screenshotInfos; 806 { 807 BAutolock locker(&fLock); 808 screenshotInfos = package->ScreenshotInfos(); 809 package->ClearScreenshots(); 810 } 811 for (int i = 0; i < screenshotInfos.CountItems(); i++) { 812 const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i); 813 _PopulatePackageScreenshot(package, info, 320, false); 814 } 815 } 816 } 817 818 819 void 820 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 821 { 822 BMessage info; 823 BString packageName; 824 825 { 826 BAutolock locker(&fLock); 827 packageName = package->Name(); 828 } 829 830 status_t status = fWebAppInterface.GetChangelog(packageName, info); 831 832 if (status == B_OK) { 833 // Parse message 834 BMessage result; 835 BString content; 836 if (info.FindMessage("result", &result) == B_OK) { 837 if (result.FindString("content", &content) == B_OK 838 && 0 != content.Length()) { 839 BAutolock locker(&fLock); 840 package->SetChangelog(content); 841 if (Logger::IsDebugEnabled()) { 842 fprintf(stdout, "changelog populated for [%s]\n", 843 packageName.String()); 844 } 845 } else { 846 if (Logger::IsDebugEnabled()) { 847 fprintf(stdout, "no changelog present for [%s]\n", 848 packageName.String()); 849 } 850 } 851 } else { 852 _MaybeLogJsonRpcError(info, "populate package changelog"); 853 } 854 } else { 855 fprintf(stdout, "unable to obtain the changelog for the package" 856 " [%s]\n", packageName.String()); 857 } 858 } 859 860 861 void 862 Model::SetUsername(BString username) 863 { 864 BString password; 865 if (username.Length() > 0) { 866 BPasswordKey key; 867 BKeyStore keyStore; 868 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, username, 869 key) == B_OK) { 870 password = key.Password(); 871 } else { 872 username = ""; 873 } 874 } 875 SetAuthorization(username, password, false); 876 } 877 878 879 const BString& 880 Model::Username() const 881 { 882 return fWebAppInterface.Username(); 883 } 884 885 886 void 887 Model::SetAuthorization(const BString& username, const BString& password, 888 bool storePassword) 889 { 890 if (storePassword && username.Length() > 0 && password.Length() > 0) { 891 BPasswordKey key(password, B_KEY_PURPOSE_WEB, username); 892 BKeyStore keyStore; 893 keyStore.AddKeyring(kHaikuDepotKeyring); 894 keyStore.AddKey(kHaikuDepotKeyring, key); 895 } 896 897 BAutolock locker(&fLock); 898 fWebAppInterface.SetAuthorization(username, password); 899 900 _NotifyAuthorizationChanged(); 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 BPath repoDataPath; 914 915 if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK 916 && repoDataPath.Append("HaikuDepot") == B_OK 917 && create_directory(repoDataPath.Path(), 0777) == B_OK 918 && repoDataPath.Append("repository-all_en.json.gz") == B_OK) { 919 path.SetTo(repoDataPath.Path()); 920 return B_OK; 921 } 922 923 path.Unset(); 924 fprintf(stdout, "unable to find the user cache file for repositories'" 925 " data"); 926 return B_ERROR; 927 } 928 929 930 status_t 931 Model::IconStoragePath(BPath& path) const 932 { 933 BPath iconStoragePath; 934 935 if (find_directory(B_USER_CACHE_DIRECTORY, &iconStoragePath) == B_OK 936 && iconStoragePath.Append("HaikuDepot") == B_OK 937 && iconStoragePath.Append("__allicons") == B_OK 938 && create_directory(iconStoragePath.Path(), 0777) == B_OK) { 939 path.SetTo(iconStoragePath.Path()); 940 return B_OK; 941 } 942 943 path.Unset(); 944 fprintf(stdout, "unable to find the user cache directory for icons"); 945 return B_ERROR; 946 } 947 948 949 status_t 950 Model::DumpExportPkgDataPath(BPath& path, 951 const BString& repositorySourceCode) const 952 { 953 BPath repoDataPath; 954 BString leafName; 955 956 leafName.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 957 fPreferredLanguage.String()); 958 959 if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK 960 && repoDataPath.Append("HaikuDepot") == B_OK 961 && create_directory(repoDataPath.Path(), 0777) == B_OK 962 && repoDataPath.Append(leafName.String()) == B_OK) { 963 path.SetTo(repoDataPath.Path()); 964 return B_OK; 965 } 966 967 path.Unset(); 968 fprintf(stdout, "unable to find the user cache file for pkgs' data"); 969 return B_ERROR; 970 } 971 972 973 void 974 Model::_UpdateIsFeaturedFilter() 975 { 976 if (fShowFeaturedPackages && SearchTerms().IsEmpty()) 977 fIsFeaturedFilter = PackageFilterRef(new IsFeaturedFilter(), true); 978 else 979 fIsFeaturedFilter = PackageFilterRef(new AnyFilter(), true); 980 } 981 982 983 void 984 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 985 const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly) 986 { 987 // See if there is a cached screenshot 988 BFile screenshotFile; 989 BPath screenshotCachePath; 990 bool fileExists = false; 991 BString screenshotName(info.Code()); 992 screenshotName << "@" << scaledWidth; 993 screenshotName << ".png"; 994 time_t modifiedTime; 995 if (find_directory(B_USER_CACHE_DIRECTORY, &screenshotCachePath) == B_OK 996 && screenshotCachePath.Append("HaikuDepot/Screenshots") == B_OK 997 && create_directory(screenshotCachePath.Path(), 0777) == B_OK 998 && screenshotCachePath.Append(screenshotName) == B_OK) { 999 // Try opening the file in read-only mode, which will fail if its 1000 // not a file or does not exist. 1001 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 1002 B_READ_ONLY) == B_OK; 1003 if (fileExists) 1004 screenshotFile.GetModificationTime(&modifiedTime); 1005 } 1006 1007 if (fileExists) { 1008 time_t now; 1009 time(&now); 1010 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 1011 // Cache file is recent enough, just use it and return. 1012 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 1013 true); 1014 BAutolock locker(&fLock); 1015 package->AddScreenshot(bitmapRef); 1016 return; 1017 } 1018 } 1019 1020 if (fromCacheOnly) 1021 return; 1022 1023 // Retrieve screenshot from web-app 1024 BMallocIO buffer; 1025 1026 int32 scaledHeight = scaledWidth * info.Height() / info.Width(); 1027 1028 status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(), 1029 scaledWidth, scaledHeight, &buffer); 1030 if (status == B_OK) { 1031 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 1032 BAutolock locker(&fLock); 1033 package->AddScreenshot(bitmapRef); 1034 locker.Unlock(); 1035 if (screenshotFile.SetTo(screenshotCachePath.Path(), 1036 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 1037 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 1038 } 1039 } else { 1040 fprintf(stderr, "Failed to retrieve screenshot for code '%s' " 1041 "at %" B_PRIi32 "x%" B_PRIi32 ".\n", info.Code().String(), 1042 scaledWidth, scaledHeight); 1043 } 1044 } 1045 1046 1047 // #pragma mark - listener notification methods 1048 1049 1050 void 1051 Model::_NotifyAuthorizationChanged() 1052 { 1053 for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) { 1054 const ModelListenerRef& listener = fListeners.ItemAtFast(i); 1055 if (listener.Get() != NULL) 1056 listener->AuthorizationChanged(); 1057 } 1058 } 1059 1060 1061 void 1062 Model::ForAllDepots(void (*func)(const DepotInfo& depot, void* context), 1063 void* context) 1064 { 1065 for (int32 i = 0; i < fDepots.CountItems(); i++) { 1066 DepotInfo depotInfo = fDepots.ItemAtFast(i); 1067 func(depotInfo, context); 1068 } 1069 } 1070 1071 1072 /*! This method will find the stored 'DepotInfo' that correlates to the 1073 supplied 'url' or 'baseUrl' and will invoke the mapper function in 1074 order to get a replacement for the 'DepotInfo'. The two URLs are 1075 different. The 'url' is a unique identifier for the repository that 1076 holds across mirrors. The 'baseUrl' is the URL stem that was used 1077 to access the repository data in the first place. The 'baseUrl' is 1078 a legacy construct that exists from a time where the identifying 1079 'url' was not being relayed properly. 1080 */ 1081 1082 void 1083 Model::ReplaceDepotByUrl( 1084 const BString& URL, 1085 const BString& baseURL, 1086 // deprecated 1087 DepotMapper* depotMapper, void* context) 1088 { 1089 for (int32 i = 0; i < fDepots.CountItems(); i++) { 1090 DepotInfo depotInfo = fDepots.ItemAtFast(i); 1091 1092 if (RepositoryUrlUtils::EqualsOnUrlOrBaseUrl(URL, depotInfo.URL(), 1093 baseURL, depotInfo.BaseURL())) { 1094 BAutolock locker(&fLock); 1095 fDepots.Replace(i, depotMapper->MapDepot(depotInfo, context)); 1096 } 1097 } 1098 } 1099 1100 1101 void 1102 Model::ForAllPackages(PackageConsumer* packageConsumer, void* context) 1103 { 1104 for (int32 i = 0; i < fDepots.CountItems(); i++) { 1105 DepotInfo depotInfo = fDepots.ItemAtFast(i); 1106 PackageList packages = depotInfo.Packages(); 1107 for(int32 j = 0; j < packages.CountItems(); j++) { 1108 const PackageInfoRef& packageInfoRef = packages.ItemAtFast(j); 1109 1110 if (packageInfoRef != NULL) { 1111 BAutolock locker(&fLock); 1112 if (!packageConsumer->ConsumePackage(packageInfoRef, context)) 1113 return; 1114 } 1115 } 1116 } 1117 } 1118 1119 1120 void 1121 Model::ForPackageByNameInDepot(const BString& depotName, 1122 const BString& packageName, PackageConsumer* packageConsumer, void* context) 1123 { 1124 int32 depotCount = fDepots.CountItems(); 1125 1126 for (int32 i = 0; i < depotCount; i++) { 1127 DepotInfo depotInfo = fDepots.ItemAtFast(i); 1128 1129 if (depotInfo.Name() == depotName) { 1130 int32 packageIndex = depotInfo.PackageIndexByName(packageName); 1131 1132 if (-1 != packageIndex) { 1133 PackageList packages = depotInfo.Packages(); 1134 const PackageInfoRef& packageInfoRef = 1135 packages.ItemAtFast(packageIndex); 1136 1137 BAutolock locker(&fLock); 1138 packageConsumer->ConsumePackage(packageInfoRef, 1139 context); 1140 } 1141 1142 return; 1143 } 1144 } 1145 } 1146 1147 1148 void 1149 Model::LogDepotsWithNoWebAppRepositoryCode() const 1150 { 1151 int32 i; 1152 1153 for (i = 0; i < fDepots.CountItems(); i++) { 1154 const DepotInfo& depot = fDepots.ItemAt(i); 1155 1156 if (depot.WebAppRepositoryCode().Length() == 0) { 1157 printf("depot [%s]", depot.Name().String()); 1158 1159 if (depot.BaseURL().Length() > 0) 1160 printf(" (%s)", depot.BaseURL().String()); 1161 1162 printf(" correlates with no repository in the haiku" 1163 "depot server system\n"); 1164 } 1165 } 1166 } 1167 1168 1169 void 1170 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 1171 const char *sourceDescription) const 1172 { 1173 BMessage error; 1174 BString errorMessage; 1175 double errorCode; 1176 1177 if (responsePayload.FindMessage("error", &error) == B_OK 1178 && error.FindString("message", &errorMessage) == B_OK 1179 && error.FindDouble("code", &errorCode) == B_OK) { 1180 printf("[%s] --> error : [%s] (%f)\n", sourceDescription, 1181 errorMessage.String(), errorCode); 1182 1183 } else { 1184 printf("[%s] --> an undefined error has occurred\n", sourceDescription); 1185 } 1186 } 1187