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, 669 languageCode, 670 // note that language identifiers are "code" in HDS and "id" in Haiku 671 versionString, 672 (uint64) createTimestamp), true); 673 package->AddUserRating(userRating); 674 HDDEBUG("rating [%s] retrieved from server", code.String()); 675 } 676 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 677 index - 1, packageName.String()); 678 } else { 679 BString message; 680 message.SetToFormat("failure to retrieve user ratings for [%s]", 681 packageName.String()); 682 _MaybeLogJsonRpcError(info, message.String()); 683 } 684 } else 685 HDERROR("unable to retrieve user ratings"); 686 } 687 } 688 689 690 void 691 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 692 { 693 BMessage info; 694 BString packageName; 695 696 { 697 BAutolock locker(&fLock); 698 packageName = package->Name(); 699 } 700 701 status_t status = fWebAppInterface.GetChangelog(packageName, info); 702 703 if (status == B_OK) { 704 // Parse message 705 BMessage result; 706 BString content; 707 if (info.FindMessage("result", &result) == B_OK) { 708 if (result.FindString("content", &content) == B_OK 709 && 0 != content.Length()) { 710 BAutolock locker(&fLock); 711 package->SetChangelog(content); 712 HDDEBUG("changelog populated for [%s]", packageName.String()); 713 } else 714 HDDEBUG("no changelog present for [%s]", packageName.String()); 715 } else 716 _MaybeLogJsonRpcError(info, "populate package changelog"); 717 } else { 718 HDERROR("unable to obtain the changelog for the package [%s]", 719 packageName.String()); 720 } 721 } 722 723 724 static void 725 model_remove_key_for_user(const BString& nickname) 726 { 727 if (nickname.IsEmpty()) 728 return; 729 BKeyStore keyStore; 730 BPasswordKey key; 731 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 732 << nickname; 733 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 734 passwordIdentifier, key); 735 736 switch (result) { 737 case B_OK: 738 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 739 if (result != B_OK) { 740 HDERROR("error occurred when removing password for nickname " 741 "[%s] : %s", nickname.String(), strerror(result)); 742 } 743 break; 744 case B_ENTRY_NOT_FOUND: 745 return; 746 default: 747 HDERROR("error occurred when finding password for nickname " 748 "[%s] : %s", nickname.String(), strerror(result)); 749 break; 750 } 751 } 752 753 754 void 755 Model::SetNickname(BString nickname) 756 { 757 BString password; 758 BString existingNickname = Nickname(); 759 760 // this happens when the user is logging out. Best to remove the password 761 // stored for the existing user since it is no longer required. 762 763 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 764 model_remove_key_for_user(existingNickname); 765 766 if (nickname.Length() > 0) { 767 BPasswordKey key; 768 BKeyStore keyStore; 769 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 770 << nickname; 771 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 772 passwordIdentifier, key) == B_OK) { 773 password = key.Password(); 774 } 775 if (password.IsEmpty()) 776 nickname = ""; 777 } 778 779 SetCredentials(nickname, password, false); 780 } 781 782 783 const BString& 784 Model::Nickname() 785 { 786 return fWebAppInterface.Nickname(); 787 } 788 789 790 void 791 Model::SetCredentials(const BString& nickname, const BString& passwordClear, 792 bool storePassword) 793 { 794 BString existingNickname = Nickname(); 795 796 if (storePassword) { 797 // no point continuing to store the password for the previous user. 798 799 if (!existingNickname.IsEmpty()) 800 model_remove_key_for_user(existingNickname); 801 802 // adding a key that is already there does not seem to override the 803 // existing key so the old key needs to be removed first. 804 805 if (!nickname.IsEmpty()) 806 model_remove_key_for_user(nickname); 807 808 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 809 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 810 << nickname; 811 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 812 BKeyStore keyStore; 813 keyStore.AddKeyring(kHaikuDepotKeyring); 814 keyStore.AddKey(kHaikuDepotKeyring, key); 815 } 816 } 817 818 BAutolock locker(&fLock); 819 fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear)); 820 821 if (nickname != existingNickname) 822 _NotifyAuthorizationChanged(); 823 } 824 825 826 /*! When bulk repository data comes down from the server, it will 827 arrive as a json.gz payload. This is stored locally as a cache 828 and this method will provide the on-disk storage location for 829 this file. 830 */ 831 832 status_t 833 Model::DumpExportRepositoryDataPath(BPath& path) 834 { 835 BString leaf; 836 leaf.SetToFormat("repository-all_%s.json.gz", 837 Language()->PreferredLanguage()->ID()); 838 return StorageUtils::LocalWorkingFilesPath(leaf, path); 839 } 840 841 842 /*! When the system downloads reference data (eg; categories) from the server 843 then the downloaded data is stored and cached at the path defined by this 844 method. 845 */ 846 847 status_t 848 Model::DumpExportReferenceDataPath(BPath& path) 849 { 850 BString leaf; 851 leaf.SetToFormat("reference-all_%s.json.gz", 852 Language()->PreferredLanguage()->ID()); 853 return StorageUtils::LocalWorkingFilesPath(leaf, path); 854 } 855 856 857 status_t 858 Model::IconTarPath(BPath& path) const 859 { 860 return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path); 861 } 862 863 864 status_t 865 Model::DumpExportPkgDataPath(BPath& path, 866 const BString& repositorySourceCode) 867 { 868 BString leaf; 869 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 870 Language()->PreferredLanguage()->ID()); 871 return StorageUtils::LocalWorkingFilesPath(leaf, path); 872 } 873 874 875 // #pragma mark - listener notification methods 876 877 878 void 879 Model::_NotifyAuthorizationChanged() 880 { 881 std::vector<ModelListenerRef>::const_iterator it; 882 for (it = fListeners.begin(); it != fListeners.end(); it++) { 883 const ModelListenerRef& listener = *it; 884 if (listener.IsSet()) 885 listener->AuthorizationChanged(); 886 } 887 } 888 889 890 void 891 Model::_NotifyCategoryListChanged() 892 { 893 std::vector<ModelListenerRef>::const_iterator it; 894 for (it = fListeners.begin(); it != fListeners.end(); it++) { 895 const ModelListenerRef& listener = *it; 896 if (listener.IsSet()) 897 listener->CategoryListChanged(); 898 } 899 } 900 901 902 void 903 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 904 const char *sourceDescription) const 905 { 906 BMessage error; 907 BString errorMessage; 908 double errorCode; 909 910 if (responsePayload.FindMessage("error", &error) == B_OK 911 && error.FindString("message", &errorMessage) == B_OK 912 && error.FindDouble("code", &errorCode) == B_OK) { 913 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 914 errorMessage.String(), errorCode); 915 } else 916 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 917 } 918 919 920 // #pragma mark - Rating Stabilities 921 922 923 int32 924 Model::CountRatingStabilities() const 925 { 926 return fRatingStabilities.size(); 927 } 928 929 930 RatingStabilityRef 931 Model::RatingStabilityByCode(BString& code) const 932 { 933 std::vector<RatingStabilityRef>::const_iterator it; 934 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 935 it++) { 936 RatingStabilityRef aRatingStability = *it; 937 if (aRatingStability->Code() == code) 938 return aRatingStability; 939 } 940 return RatingStabilityRef(); 941 } 942 943 944 RatingStabilityRef 945 Model::RatingStabilityAtIndex(int32 index) const 946 { 947 return fRatingStabilities[index]; 948 } 949 950 951 void 952 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 953 { 954 std::vector<RatingStabilityRef>::const_iterator it; 955 for (it = values.begin(); it != values.end(); it++) 956 _AddRatingStability(*it); 957 } 958 959 960 void 961 Model::_AddRatingStability(const RatingStabilityRef& value) 962 { 963 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 964 = std::lower_bound( 965 fRatingStabilities.begin(), 966 fRatingStabilities.end(), 967 value, 968 &IsRatingStabilityBefore); 969 std::vector<RatingStabilityRef>::iterator itInsertionPt = 970 fRatingStabilities.begin() 971 + (itInsertionPtConst - fRatingStabilities.begin()); 972 973 if (itInsertionPt != fRatingStabilities.end() 974 && (*itInsertionPt)->Code() == value->Code()) { 975 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 976 // replace the one with the same code. 977 } 978 979 fRatingStabilities.insert(itInsertionPt, value); 980 } 981 982 983 // #pragma mark - Categories 984 985 986 int32 987 Model::CountCategories() const 988 { 989 return fCategories.size(); 990 } 991 992 993 CategoryRef 994 Model::CategoryByCode(BString& code) const 995 { 996 std::vector<CategoryRef>::const_iterator it; 997 for (it = fCategories.begin(); it != fCategories.end(); it++) { 998 CategoryRef aCategory = *it; 999 if (aCategory->Code() == code) 1000 return aCategory; 1001 } 1002 return CategoryRef(); 1003 } 1004 1005 1006 CategoryRef 1007 Model::CategoryAtIndex(int32 index) const 1008 { 1009 return fCategories[index]; 1010 } 1011 1012 1013 void 1014 Model::AddCategories(std::vector<CategoryRef>& values) 1015 { 1016 std::vector<CategoryRef>::iterator it; 1017 for (it = values.begin(); it != values.end(); it++) 1018 _AddCategory(*it); 1019 _NotifyCategoryListChanged(); 1020 } 1021 1022 /*! This will insert the category in order. 1023 */ 1024 1025 void 1026 Model::_AddCategory(const CategoryRef& category) 1027 { 1028 std::vector<CategoryRef>::const_iterator itInsertionPtConst 1029 = std::lower_bound( 1030 fCategories.begin(), 1031 fCategories.end(), 1032 category, 1033 &IsPackageCategoryBefore); 1034 std::vector<CategoryRef>::iterator itInsertionPt = 1035 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 1036 1037 if (itInsertionPt != fCategories.end() 1038 && (*itInsertionPt)->Code() == category->Code()) { 1039 itInsertionPt = fCategories.erase(itInsertionPt); 1040 // replace the one with the same code. 1041 } 1042 1043 fCategories.insert(itInsertionPt, category); 1044 } 1045 1046 1047 void 1048 Model::ScreenshotCached(const ScreenshotCoordinate& coord) 1049 { 1050 std::vector<ModelListenerRef>::const_iterator it; 1051 for (it = fListeners.begin(); it != fListeners.end(); it++) { 1052 const ModelListenerRef& listener = *it; 1053 if (listener.IsSet()) 1054 listener->ScreenshotCached(coord); 1055 } 1056 } 1057