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