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