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