xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 6f80a9801fedbe7355c4360bd204ba746ec3ec2d)
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 	if (!CanPopulatePackage(package)) {
523 		HDINFO("unable to populate package [%s]", package->Name().String());
524 		return;
525 	}
526 
527 	// TODO: There should probably also be a way to "unpopulate" the
528 	// package information. Maybe a cache of populated packages, so that
529 	// packages loose their extra information after a certain amount of
530 	// time when they have not been accessed/displayed in the UI. Otherwise
531 	// HaikuDepot will consume more and more resources in the packages.
532 	// Especially screen-shots will be a problem eventually.
533 	{
534 		BAutolock locker(&fLock);
535 		bool alreadyPopulated = fPopulatedPackageNames.HasString(
536 			package->Name());
537 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
538 			return;
539 		if (!alreadyPopulated)
540 			fPopulatedPackageNames.Add(package->Name());
541 	}
542 
543 	if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) {
544 		_PopulatePackageChangelog(package);
545 	}
546 
547 	if ((flags & POPULATE_USER_RATINGS) != 0) {
548 		// Retrieve info from web-app
549 		BMessage info;
550 
551 		BString packageName;
552 		BString webAppRepositoryCode;
553 		BString webAppRepositorySourceCode;
554 
555 		{
556 			BAutolock locker(&fLock);
557 			packageName = package->Name();
558 			const DepotInfo* depot = DepotForName(package->DepotName());
559 
560 			if (depot != NULL) {
561 				webAppRepositoryCode = depot->WebAppRepositoryCode();
562 				webAppRepositorySourceCode
563 					= depot->WebAppRepositorySourceCode();
564 			}
565 		}
566 
567 		status_t status = fWebAppInterface
568 			.RetreiveUserRatingsForPackageForDisplay(packageName,
569 				webAppRepositoryCode, webAppRepositorySourceCode, 0,
570 				PACKAGE_INFO_MAX_USER_RATINGS, info);
571 		if (status == B_OK) {
572 			// Parse message
573 			BMessage result;
574 			BMessage items;
575 			if (info.FindMessage("result", &result) == B_OK
576 				&& result.FindMessage("items", &items) == B_OK) {
577 
578 				BAutolock locker(&fLock);
579 				package->ClearUserRatings();
580 
581 				int32 index = 0;
582 				while (true) {
583 					BString name;
584 					name << index++;
585 
586 					BMessage item;
587 					if (items.FindMessage(name, &item) != B_OK)
588 						break;
589 
590 					BString code;
591 					if (item.FindString("code", &code) != B_OK) {
592 						HDERROR("corrupt user rating at index %" B_PRIi32,
593 							index);
594 						continue;
595 					}
596 
597 					BString user;
598 					BMessage userInfo;
599 					if (item.FindMessage("user", &userInfo) != B_OK
600 							|| userInfo.FindString("nickname", &user) != B_OK) {
601 						HDERROR("ignored user rating [%s] without a user "
602 							"nickname", code.String());
603 						continue;
604 					}
605 
606 					// Extract basic info, all items are optional
607 					BString languageCode;
608 					BString comment;
609 					double rating;
610 					item.FindString("naturalLanguageCode", &languageCode);
611 					item.FindString("comment", &comment);
612 					if (item.FindDouble("rating", &rating) != B_OK)
613 						rating = -1;
614 					if (comment.Length() == 0 && rating == -1) {
615 						HDERROR("rating [%s] has no comment or rating so will"
616 							" be ignored", code.String());
617 						continue;
618 					}
619 
620 					// For which version of the package was the rating?
621 					BString major = "?";
622 					BString minor = "?";
623 					BString micro = "";
624 					double revision = -1;
625 					BString architectureCode = "";
626 					BMessage version;
627 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
628 						version.FindString("major", &major);
629 						version.FindString("minor", &minor);
630 						version.FindString("micro", &micro);
631 						version.FindDouble("revision", &revision);
632 						version.FindString("architectureCode",
633 							&architectureCode);
634 					}
635 					BString versionString = major;
636 					versionString << ".";
637 					versionString << minor;
638 					if (!micro.IsEmpty()) {
639 						versionString << ".";
640 						versionString << micro;
641 					}
642 					if (revision > 0) {
643 						versionString << "-";
644 						versionString << (int) revision;
645 					}
646 
647 					if (!architectureCode.IsEmpty()) {
648 						versionString << " " << STR_MDASH << " ";
649 						versionString << architectureCode;
650 					}
651 
652 					double createTimestamp;
653 					item.FindDouble("createTimestamp", &createTimestamp);
654 
655 					// Add the rating to the PackageInfo
656 					UserRatingRef userRating(new UserRating(
657 						UserInfo(user), rating,
658 						comment, languageCode, versionString,
659 						(uint64) createTimestamp), true);
660 					package->AddUserRating(userRating);
661 					HDDEBUG("rating [%s] retrieved from server", code.String());
662 				}
663 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
664 						index - 1, packageName.String());
665 			} else {
666 				BString message;
667 				message.SetToFormat("failure to retrieve user ratings for [%s]",
668 					packageName.String());
669 				_MaybeLogJsonRpcError(info, message.String());
670 			}
671 		} else
672 			HDERROR("unable to retrieve user ratings");
673 	}
674 
675 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
676 		std::vector<ScreenshotInfoRef> screenshotInfos;
677 		{
678 			BAutolock locker(&fLock);
679 			for (int32 i = 0; i < package->CountScreenshotInfos(); i++)
680 				screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i));
681 			package->ClearScreenshots();
682 		}
683 		std::vector<ScreenshotInfoRef>::iterator it;
684 		for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) {
685 			const ScreenshotInfoRef& info = *it;
686 			_PopulatePackageScreenshot(package, info, 320, false);
687 		}
688 	}
689 }
690 
691 
692 void
693 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
694 {
695 	BMessage info;
696 	BString packageName;
697 
698 	{
699 		BAutolock locker(&fLock);
700 		packageName = package->Name();
701 	}
702 
703 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
704 
705 	if (status == B_OK) {
706 		// Parse message
707 		BMessage result;
708 		BString content;
709 		if (info.FindMessage("result", &result) == B_OK) {
710 			if (result.FindString("content", &content) == B_OK
711 				&& 0 != content.Length()) {
712 				BAutolock locker(&fLock);
713 				package->SetChangelog(content);
714 				HDDEBUG("changelog populated for [%s]", packageName.String());
715 			} else
716 				HDDEBUG("no changelog present for [%s]", packageName.String());
717 		} else
718 			_MaybeLogJsonRpcError(info, "populate package changelog");
719 	} else {
720 		HDERROR("unable to obtain the changelog for the package [%s]",
721 			packageName.String());
722 	}
723 }
724 
725 
726 static void
727 model_remove_key_for_user(const BString& nickname)
728 {
729 	if (nickname.IsEmpty())
730 		return;
731 	BKeyStore keyStore;
732 	BPasswordKey key;
733 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
734 		<< nickname;
735 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
736 			passwordIdentifier, key);
737 
738 	switch (result) {
739 		case B_OK:
740 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
741 			if (result != B_OK) {
742 				HDERROR("error occurred when removing password for nickname "
743 					"[%s] : %s", nickname.String(), strerror(result));
744 			}
745 			break;
746 		case B_ENTRY_NOT_FOUND:
747 			return;
748 		default:
749 			HDERROR("error occurred when finding password for nickname "
750 				"[%s] : %s", nickname.String(), strerror(result));
751 			break;
752 	}
753 }
754 
755 
756 void
757 Model::SetNickname(BString nickname)
758 {
759 	BString password;
760 	BString existingNickname = Nickname();
761 
762 	// this happens when the user is logging out.  Best to remove the password
763 	// stored for the existing user since it is no longer required.
764 
765 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
766 		model_remove_key_for_user(existingNickname);
767 
768 	if (nickname.Length() > 0) {
769 		BPasswordKey key;
770 		BKeyStore keyStore;
771 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
772 			<< nickname;
773 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
774 				passwordIdentifier, key) == B_OK) {
775 			password = key.Password();
776 		}
777 		if (password.IsEmpty())
778 			nickname = "";
779 	}
780 
781 	SetAuthorization(nickname, password, false);
782 }
783 
784 
785 const BString&
786 Model::Nickname() const
787 {
788 	return fWebAppInterface.Nickname();
789 }
790 
791 
792 void
793 Model::SetAuthorization(const BString& nickname, const BString& passwordClear,
794 	bool storePassword)
795 {
796 	BString existingNickname = Nickname();
797 
798 	if (storePassword) {
799 		// no point continuing to store the password for the previous user.
800 
801 		if (!existingNickname.IsEmpty())
802 			model_remove_key_for_user(existingNickname);
803 
804 		// adding a key that is already there does not seem to override the
805 		// existing key so the old key needs to be removed first.
806 
807 		if (!nickname.IsEmpty())
808 			model_remove_key_for_user(nickname);
809 
810 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
811 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
812 				<< nickname;
813 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
814 			BKeyStore keyStore;
815 			keyStore.AddKeyring(kHaikuDepotKeyring);
816 			keyStore.AddKey(kHaikuDepotKeyring, key);
817 		}
818 	}
819 
820 	BAutolock locker(&fLock);
821 	fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear));
822 
823 	if (nickname != existingNickname)
824 		_NotifyAuthorizationChanged();
825 }
826 
827 
828 /*! When bulk repository data comes down from the server, it will
829     arrive as a json.gz payload.  This is stored locally as a cache
830     and this method will provide the on-disk storage location for
831     this file.
832 */
833 
834 status_t
835 Model::DumpExportRepositoryDataPath(BPath& path)
836 {
837 	BString leaf;
838 	leaf.SetToFormat("repository-all_%s.json.gz",
839 		Language()->PreferredLanguage()->Code());
840 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
841 }
842 
843 
844 /*! When the system downloads reference data (eg; categories) from the server
845     then the downloaded data is stored and cached at the path defined by this
846     method.
847 */
848 
849 status_t
850 Model::DumpExportReferenceDataPath(BPath& path)
851 {
852 	BString leaf;
853 	leaf.SetToFormat("reference-all_%s.json.gz",
854 		Language()->PreferredLanguage()->Code());
855 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
856 }
857 
858 
859 status_t
860 Model::IconTarPath(BPath& path) const
861 {
862 	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
863 }
864 
865 
866 status_t
867 Model::DumpExportPkgDataPath(BPath& path,
868 	const BString& repositorySourceCode)
869 {
870 	BString leaf;
871 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
872 		Language()->PreferredLanguage()->Code());
873 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
874 }
875 
876 
877 void
878 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
879 	const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly)
880 {
881 	// See if there is a cached screenshot
882 	BFile screenshotFile;
883 	BPath screenshotCachePath;
884 
885 	status_t result = StorageUtils::LocalWorkingDirectoryPath(
886 		"Screenshots", screenshotCachePath);
887 
888 	if (result != B_OK) {
889 		HDERROR("unable to get the screenshot dir - unable to proceed");
890 		return;
891 	}
892 
893 	bool fileExists = false;
894 	BString screenshotName(info->Code());
895 	screenshotName << "@" << scaledWidth;
896 	screenshotName << ".png";
897 	time_t modifiedTime;
898 	if (screenshotCachePath.Append(screenshotName) == B_OK) {
899 		// Try opening the file in read-only mode, which will fail if its
900 		// not a file or does not exist.
901 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
902 			B_READ_ONLY) == B_OK;
903 		if (fileExists)
904 			screenshotFile.GetModificationTime(&modifiedTime);
905 	}
906 
907 	if (fileExists) {
908 		time_t now;
909 		time(&now);
910 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
911 			// Cache file is recent enough, just use it and return.
912 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
913 				true);
914 			BAutolock locker(&fLock);
915 			package->AddScreenshot(bitmapRef);
916 			return;
917 		}
918 	}
919 
920 	if (fromCacheOnly)
921 		return;
922 
923 	// Retrieve screenshot from web-app
924 	BMallocIO buffer;
925 
926 	int32 scaledHeight = scaledWidth * info->Height() / info->Width();
927 
928 	status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(),
929 		scaledWidth, scaledHeight, &buffer);
930 	if (status == B_OK) {
931 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
932 		BAutolock locker(&fLock);
933 		package->AddScreenshot(bitmapRef);
934 		locker.Unlock();
935 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
936 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
937 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
938 		}
939 	} else {
940 		HDERROR("Failed to retrieve screenshot for code '%s' "
941 			"at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(),
942 			scaledWidth, scaledHeight);
943 	}
944 }
945 
946 
947 // #pragma mark - listener notification methods
948 
949 
950 void
951 Model::_NotifyAuthorizationChanged()
952 {
953 	std::vector<ModelListenerRef>::const_iterator it;
954 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
955 		const ModelListenerRef& listener = *it;
956 		if (listener.IsSet())
957 			listener->AuthorizationChanged();
958 	}
959 }
960 
961 
962 void
963 Model::_NotifyCategoryListChanged()
964 {
965 	std::vector<ModelListenerRef>::const_iterator it;
966 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
967 		const ModelListenerRef& listener = *it;
968 		if (listener.IsSet())
969 			listener->CategoryListChanged();
970 	}
971 }
972 
973 
974 void
975 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
976 	const char *sourceDescription) const
977 {
978 	BMessage error;
979 	BString errorMessage;
980 	double errorCode;
981 
982 	if (responsePayload.FindMessage("error", &error) == B_OK
983 		&& error.FindString("message", &errorMessage) == B_OK
984 		&& error.FindDouble("code", &errorCode) == B_OK) {
985 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
986 			errorMessage.String(), errorCode);
987 	} else
988 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
989 }
990 
991 
992 // #pragma mark - Rating Stabilities
993 
994 
995 int32
996 Model::CountRatingStabilities() const
997 {
998 	return fRatingStabilities.size();
999 }
1000 
1001 
1002 RatingStabilityRef
1003 Model::RatingStabilityByCode(BString& code) const
1004 {
1005 	std::vector<RatingStabilityRef>::const_iterator it;
1006 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
1007 			it++) {
1008 		RatingStabilityRef aRatingStability = *it;
1009 		if (aRatingStability->Code() == code)
1010 			return aRatingStability;
1011 	}
1012 	return RatingStabilityRef();
1013 }
1014 
1015 
1016 RatingStabilityRef
1017 Model::RatingStabilityAtIndex(int32 index) const
1018 {
1019 	return fRatingStabilities[index];
1020 }
1021 
1022 
1023 void
1024 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
1025 {
1026 	std::vector<RatingStabilityRef>::const_iterator it;
1027 	for (it = values.begin(); it != values.end(); it++)
1028 		_AddRatingStability(*it);
1029 }
1030 
1031 
1032 void
1033 Model::_AddRatingStability(const RatingStabilityRef& value)
1034 {
1035 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
1036 		= std::lower_bound(
1037 			fRatingStabilities.begin(),
1038 			fRatingStabilities.end(),
1039 			value,
1040 			&IsRatingStabilityBefore);
1041 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
1042 		fRatingStabilities.begin()
1043 			+ (itInsertionPtConst - fRatingStabilities.begin());
1044 
1045 	if (itInsertionPt != fRatingStabilities.end()
1046 		&& (*itInsertionPt)->Code() == value->Code()) {
1047 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
1048 			// replace the one with the same code.
1049 	}
1050 
1051 	fRatingStabilities.insert(itInsertionPt, value);
1052 }
1053 
1054 
1055 // #pragma mark - Categories
1056 
1057 
1058 int32
1059 Model::CountCategories() const
1060 {
1061 	return fCategories.size();
1062 }
1063 
1064 
1065 CategoryRef
1066 Model::CategoryByCode(BString& code) const
1067 {
1068 	std::vector<CategoryRef>::const_iterator it;
1069 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
1070 		CategoryRef aCategory = *it;
1071 		if (aCategory->Code() == code)
1072 			return aCategory;
1073 	}
1074 	return CategoryRef();
1075 }
1076 
1077 
1078 CategoryRef
1079 Model::CategoryAtIndex(int32 index) const
1080 {
1081 	return fCategories[index];
1082 }
1083 
1084 
1085 void
1086 Model::AddCategories(std::vector<CategoryRef>& values)
1087 {
1088 	std::vector<CategoryRef>::iterator it;
1089 	for (it = values.begin(); it != values.end(); it++)
1090 		_AddCategory(*it);
1091 	_NotifyCategoryListChanged();
1092 }
1093 
1094 /*! This will insert the category in order.
1095  */
1096 
1097 void
1098 Model::_AddCategory(const CategoryRef& category)
1099 {
1100 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
1101 		= std::lower_bound(
1102 			fCategories.begin(),
1103 			fCategories.end(),
1104 			category,
1105 			&IsPackageCategoryBefore);
1106 	std::vector<CategoryRef>::iterator itInsertionPt =
1107 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
1108 
1109 	if (itInsertionPt != fCategories.end()
1110 		&& (*itInsertionPt)->Code() == category->Code()) {
1111 		itInsertionPt = fCategories.erase(itInsertionPt);
1112 			// replace the one with the same code.
1113 	}
1114 
1115 	fCategories.insert(itInsertionPt, category);
1116 }
1117