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