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