xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 1deede7388b04dbeec5af85cae7164735ea9e70d)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2021, 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 		{
554 			BAutolock locker(&fLock);
555 			packageName = package->Name();
556 			const DepotInfo* depot = DepotForName(package->DepotName());
557 
558 			if (depot != NULL)
559 				webAppRepositoryCode = depot->WebAppRepositoryCode();
560 		}
561 
562 		status_t status = fWebAppInterface
563 			.RetreiveUserRatingsForPackageForDisplay(packageName,
564 				webAppRepositoryCode, 0, PACKAGE_INFO_MAX_USER_RATINGS,
565 				info);
566 		if (status == B_OK) {
567 			// Parse message
568 			BMessage result;
569 			BMessage items;
570 			if (info.FindMessage("result", &result) == B_OK
571 				&& result.FindMessage("items", &items) == B_OK) {
572 
573 				BAutolock locker(&fLock);
574 				package->ClearUserRatings();
575 
576 				int32 index = 0;
577 				while (true) {
578 					BString name;
579 					name << index++;
580 
581 					BMessage item;
582 					if (items.FindMessage(name, &item) != B_OK)
583 						break;
584 
585 					BString code;
586 					if (item.FindString("code", &code) != B_OK) {
587 						HDERROR("corrupt user rating at index %" B_PRIi32,
588 							index);
589 						continue;
590 					}
591 
592 					BString user;
593 					BMessage userInfo;
594 					if (item.FindMessage("user", &userInfo) != B_OK
595 							|| userInfo.FindString("nickname", &user) != B_OK) {
596 						HDERROR("ignored user rating [%s] without a user "
597 							"nickname", code.String());
598 						continue;
599 					}
600 
601 					// Extract basic info, all items are optional
602 					BString languageCode;
603 					BString comment;
604 					double rating;
605 					item.FindString("naturalLanguageCode", &languageCode);
606 					item.FindString("comment", &comment);
607 					if (item.FindDouble("rating", &rating) != B_OK)
608 						rating = -1;
609 					if (comment.Length() == 0 && rating == -1) {
610 						HDERROR("rating [%s] has no comment or rating so will"
611 							" be ignored", code.String());
612 						continue;
613 					}
614 
615 					// For which version of the package was the rating?
616 					BString major = "?";
617 					BString minor = "?";
618 					BString micro = "";
619 					double revision = -1;
620 					BString architectureCode = "";
621 					BMessage version;
622 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
623 						version.FindString("major", &major);
624 						version.FindString("minor", &minor);
625 						version.FindString("micro", &micro);
626 						version.FindDouble("revision", &revision);
627 						version.FindString("architectureCode",
628 							&architectureCode);
629 					}
630 					BString versionString = major;
631 					versionString << ".";
632 					versionString << minor;
633 					if (!micro.IsEmpty()) {
634 						versionString << ".";
635 						versionString << micro;
636 					}
637 					if (revision > 0) {
638 						versionString << "-";
639 						versionString << (int) revision;
640 					}
641 
642 					if (!architectureCode.IsEmpty()) {
643 						versionString << " " << STR_MDASH << " ";
644 						versionString << architectureCode;
645 					}
646 
647 					double createTimestamp;
648 					item.FindDouble("createTimestamp", &createTimestamp);
649 
650 					// Add the rating to the PackageInfo
651 					UserRatingRef userRating(new UserRating(
652 						UserInfo(user), rating,
653 						comment, languageCode, versionString,
654 						(uint64) createTimestamp), true);
655 					package->AddUserRating(userRating);
656 					HDDEBUG("rating [%s] retrieved from server", code.String());
657 				}
658 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
659 						index - 1, packageName.String());
660 			} else {
661 				BString message;
662 				message.SetToFormat("failure to retrieve user ratings for [%s]",
663 					packageName.String());
664 				_MaybeLogJsonRpcError(info, message.String());
665 			}
666 		} else
667 			HDERROR("unable to retrieve user ratings");
668 	}
669 
670 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
671 		std::vector<ScreenshotInfoRef> screenshotInfos;
672 		{
673 			BAutolock locker(&fLock);
674 			for (int32 i = 0; i < package->CountScreenshotInfos(); i++)
675 				screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i));
676 			package->ClearScreenshots();
677 		}
678 		std::vector<ScreenshotInfoRef>::iterator it;
679 		for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) {
680 			const ScreenshotInfoRef& info = *it;
681 			_PopulatePackageScreenshot(package, info, 320, false);
682 		}
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 	SetAuthorization(nickname, password, false);
777 }
778 
779 
780 const BString&
781 Model::Nickname() const
782 {
783 	return fWebAppInterface.Nickname();
784 }
785 
786 
787 void
788 Model::SetAuthorization(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.SetAuthorization(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 void
873 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
874 	const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly)
875 {
876 	// See if there is a cached screenshot
877 	BFile screenshotFile;
878 	BPath screenshotCachePath;
879 
880 	status_t result = StorageUtils::LocalWorkingDirectoryPath(
881 		"Screenshots", screenshotCachePath);
882 
883 	if (result != B_OK) {
884 		HDERROR("unable to get the screenshot dir - unable to proceed");
885 		return;
886 	}
887 
888 	bool fileExists = false;
889 	BString screenshotName(info->Code());
890 	screenshotName << "@" << scaledWidth;
891 	screenshotName << ".png";
892 	time_t modifiedTime;
893 	if (screenshotCachePath.Append(screenshotName) == B_OK) {
894 		// Try opening the file in read-only mode, which will fail if its
895 		// not a file or does not exist.
896 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
897 			B_READ_ONLY) == B_OK;
898 		if (fileExists)
899 			screenshotFile.GetModificationTime(&modifiedTime);
900 	}
901 
902 	if (fileExists) {
903 		time_t now;
904 		time(&now);
905 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
906 			// Cache file is recent enough, just use it and return.
907 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
908 				true);
909 			BAutolock locker(&fLock);
910 			package->AddScreenshot(bitmapRef);
911 			return;
912 		}
913 	}
914 
915 	if (fromCacheOnly)
916 		return;
917 
918 	// Retrieve screenshot from web-app
919 	BMallocIO buffer;
920 
921 	int32 scaledHeight = scaledWidth * info->Height() / info->Width();
922 
923 	status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(),
924 		scaledWidth, scaledHeight, &buffer);
925 	if (status == B_OK) {
926 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
927 		BAutolock locker(&fLock);
928 		package->AddScreenshot(bitmapRef);
929 		locker.Unlock();
930 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
931 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
932 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
933 		}
934 	} else {
935 		HDERROR("Failed to retrieve screenshot for code '%s' "
936 			"at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(),
937 			scaledWidth, scaledHeight);
938 	}
939 }
940 
941 
942 // #pragma mark - listener notification methods
943 
944 
945 void
946 Model::_NotifyAuthorizationChanged()
947 {
948 	std::vector<ModelListenerRef>::const_iterator it;
949 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
950 		const ModelListenerRef& listener = *it;
951 		if (listener.IsSet())
952 			listener->AuthorizationChanged();
953 	}
954 }
955 
956 
957 void
958 Model::_NotifyCategoryListChanged()
959 {
960 	std::vector<ModelListenerRef>::const_iterator it;
961 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
962 		const ModelListenerRef& listener = *it;
963 		if (listener.IsSet())
964 			listener->CategoryListChanged();
965 	}
966 }
967 
968 
969 void
970 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
971 	const char *sourceDescription) const
972 {
973 	BMessage error;
974 	BString errorMessage;
975 	double errorCode;
976 
977 	if (responsePayload.FindMessage("error", &error) == B_OK
978 		&& error.FindString("message", &errorMessage) == B_OK
979 		&& error.FindDouble("code", &errorCode) == B_OK) {
980 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
981 			errorMessage.String(), errorCode);
982 	} else
983 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
984 }
985 
986 
987 // #pragma mark - Rating Stabilities
988 
989 
990 int32
991 Model::CountRatingStabilities() const
992 {
993 	return fRatingStabilities.size();
994 }
995 
996 
997 RatingStabilityRef
998 Model::RatingStabilityByCode(BString& code) const
999 {
1000 	std::vector<RatingStabilityRef>::const_iterator it;
1001 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
1002 			it++) {
1003 		RatingStabilityRef aRatingStability = *it;
1004 		if (aRatingStability->Code() == code)
1005 			return aRatingStability;
1006 	}
1007 	return RatingStabilityRef();
1008 }
1009 
1010 
1011 RatingStabilityRef
1012 Model::RatingStabilityAtIndex(int32 index) const
1013 {
1014 	return fRatingStabilities[index];
1015 }
1016 
1017 
1018 void
1019 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
1020 {
1021 	std::vector<RatingStabilityRef>::const_iterator it;
1022 	for (it = values.begin(); it != values.end(); it++)
1023 		_AddRatingStability(*it);
1024 }
1025 
1026 
1027 void
1028 Model::_AddRatingStability(const RatingStabilityRef& value)
1029 {
1030 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
1031 		= std::lower_bound(
1032 			fRatingStabilities.begin(),
1033 			fRatingStabilities.end(),
1034 			value,
1035 			&IsRatingStabilityBefore);
1036 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
1037 		fRatingStabilities.begin()
1038 			+ (itInsertionPtConst - fRatingStabilities.begin());
1039 
1040 	if (itInsertionPt != fRatingStabilities.end()
1041 		&& (*itInsertionPt)->Code() == value->Code()) {
1042 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
1043 			// replace the one with the same code.
1044 	}
1045 
1046 	fRatingStabilities.insert(itInsertionPt, value);
1047 }
1048 
1049 
1050 // #pragma mark - Categories
1051 
1052 
1053 int32
1054 Model::CountCategories() const
1055 {
1056 	return fCategories.size();
1057 }
1058 
1059 
1060 CategoryRef
1061 Model::CategoryByCode(BString& code) const
1062 {
1063 	std::vector<CategoryRef>::const_iterator it;
1064 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
1065 		CategoryRef aCategory = *it;
1066 		if (aCategory->Code() == code)
1067 			return aCategory;
1068 	}
1069 	return CategoryRef();
1070 }
1071 
1072 
1073 CategoryRef
1074 Model::CategoryAtIndex(int32 index) const
1075 {
1076 	return fCategories[index];
1077 }
1078 
1079 
1080 void
1081 Model::AddCategories(std::vector<CategoryRef>& values)
1082 {
1083 	std::vector<CategoryRef>::iterator it;
1084 	for (it = values.begin(); it != values.end(); it++)
1085 		_AddCategory(*it);
1086 	_NotifyCategoryListChanged();
1087 }
1088 
1089 /*! This will insert the category in order.
1090  */
1091 
1092 void
1093 Model::_AddCategory(const CategoryRef& category)
1094 {
1095 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
1096 		= std::lower_bound(
1097 			fCategories.begin(),
1098 			fCategories.end(),
1099 			category,
1100 			&IsPackageCategoryBefore);
1101 	std::vector<CategoryRef>::iterator itInsertionPt =
1102 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
1103 
1104 	if (itInsertionPt != fCategories.end()
1105 		&& (*itInsertionPt)->Code() == category->Code()) {
1106 		itInsertionPt = fCategories.erase(itInsertionPt);
1107 			// replace the one with the same code.
1108 	}
1109 
1110 	fCategories.insert(itInsertionPt, category);
1111 }
1112