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