xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 1705656eac83cd8b65c2ee895888cd529c62c04c)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2019, 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 <ctime>
11 #include <stdarg.h>
12 #include <stdio.h>
13 #include <time.h>
14 
15 #include <Autolock.h>
16 #include <Catalog.h>
17 #include <Directory.h>
18 #include <Entry.h>
19 #include <File.h>
20 #include <KeyStore.h>
21 #include <LocaleRoster.h>
22 #include <Message.h>
23 #include <Path.h>
24 
25 #include "Logger.h"
26 #include "StorageUtils.h"
27 #include "RepositoryUrlUtils.h"
28 
29 
30 #undef B_TRANSLATION_CONTEXT
31 #define B_TRANSLATION_CONTEXT "Model"
32 
33 
34 static const char* kHaikuDepotKeyring = "HaikuDepot";
35 
36 
37 PackageFilter::~PackageFilter()
38 {
39 }
40 
41 
42 ModelListener::~ModelListener()
43 {
44 }
45 
46 
47 // #pragma mark - PackageFilters
48 
49 
50 class AnyFilter : public PackageFilter {
51 public:
52 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
53 	{
54 		return true;
55 	}
56 };
57 
58 
59 class DepotFilter : public PackageFilter {
60 public:
61 	DepotFilter(const DepotInfo& depot)
62 		:
63 		fDepot(depot)
64 	{
65 	}
66 
67 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
68 	{
69 		// TODO: Maybe a PackageInfo ought to know the Depot it came from?
70 		// But right now the same package could theoretically be provided
71 		// from different depots and the filter would work correctly.
72 		// Also the PackageList could actually contain references to packages
73 		// instead of the packages as objects. The equal operator is quite
74 		// expensive as is.
75 		return fDepot.Packages().Contains(package);
76 	}
77 
78 	const BString& Depot() const
79 	{
80 		return fDepot.Name();
81 	}
82 
83 private:
84 	DepotInfo	fDepot;
85 };
86 
87 
88 class CategoryFilter : public PackageFilter {
89 public:
90 	CategoryFilter(const BString& category)
91 		:
92 		fCategory(category)
93 	{
94 	}
95 
96 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
97 	{
98 		if (package.Get() == NULL)
99 			return false;
100 
101 		const CategoryList& categories = package->Categories();
102 		for (int i = categories.CountItems() - 1; i >= 0; i--) {
103 			const CategoryRef& category = categories.ItemAtFast(i);
104 			if (category.Get() == NULL)
105 				continue;
106 			if (category->Name() == fCategory)
107 				return true;
108 		}
109 		return false;
110 	}
111 
112 	const BString& Category() const
113 	{
114 		return fCategory;
115 	}
116 
117 private:
118 	BString		fCategory;
119 };
120 
121 
122 class ContainedInFilter : public PackageFilter {
123 public:
124 	ContainedInFilter(const PackageList& packageList)
125 		:
126 		fPackageList(packageList)
127 	{
128 	}
129 
130 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
131 	{
132 		return fPackageList.Contains(package);
133 	}
134 
135 private:
136 	const PackageList&	fPackageList;
137 };
138 
139 
140 class ContainedInEitherFilter : public PackageFilter {
141 public:
142 	ContainedInEitherFilter(const PackageList& packageListA,
143 		const PackageList& packageListB)
144 		:
145 		fPackageListA(packageListA),
146 		fPackageListB(packageListB)
147 	{
148 	}
149 
150 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
151 	{
152 		return fPackageListA.Contains(package)
153 			|| fPackageListB.Contains(package);
154 	}
155 
156 private:
157 	const PackageList&	fPackageListA;
158 	const PackageList&	fPackageListB;
159 };
160 
161 
162 class NotContainedInFilter : public PackageFilter {
163 public:
164 	NotContainedInFilter(const PackageList* packageList, ...)
165 	{
166 		va_list args;
167 		va_start(args, packageList);
168 		while (true) {
169 			const PackageList* packageList = va_arg(args, const PackageList*);
170 			if (packageList == NULL)
171 				break;
172 			fPackageLists.Add(packageList);
173 		}
174 		va_end(args);
175 	}
176 
177 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
178 	{
179 		if (package.Get() == NULL)
180 			return false;
181 
182 		printf("TEST %s\n", package->Name().String());
183 
184 		for (int32 i = 0; i < fPackageLists.CountItems(); i++) {
185 			if (fPackageLists.ItemAtFast(i)->Contains(package)) {
186 				printf("  contained in %" B_PRId32 "\n", i);
187 				return false;
188 			}
189 		}
190 		return true;
191 	}
192 
193 private:
194 	List<const PackageList*, true>	fPackageLists;
195 };
196 
197 
198 class StateFilter : public PackageFilter {
199 public:
200 	StateFilter(PackageState state)
201 		:
202 		fState(state)
203 	{
204 	}
205 
206 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
207 	{
208 		return package->State() == NONE;
209 	}
210 
211 private:
212 	PackageState	fState;
213 };
214 
215 
216 class SearchTermsFilter : public PackageFilter {
217 public:
218 	SearchTermsFilter(const BString& searchTerms)
219 	{
220 		// Separate the string into terms at spaces
221 		int32 index = 0;
222 		while (index < searchTerms.Length()) {
223 			int32 nextSpace = searchTerms.FindFirst(" ", index);
224 			if (nextSpace < 0)
225 				nextSpace = searchTerms.Length();
226 			if (nextSpace > index) {
227 				BString term;
228 				searchTerms.CopyInto(term, index, nextSpace - index);
229 				term.ToLower();
230 				fSearchTerms.Add(term);
231 			}
232 			index = nextSpace + 1;
233 		}
234 	}
235 
236 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
237 	{
238 		if (package.Get() == NULL)
239 			return false;
240 		// Every search term must be found in one of the package texts
241 		for (int32 i = fSearchTerms.CountItems() - 1; i >= 0; i--) {
242 			const BString& term = fSearchTerms.ItemAtFast(i);
243 			if (!_TextContains(package->Name(), term)
244 				&& !_TextContains(package->Title(), term)
245 				&& !_TextContains(package->Publisher().Name(), term)
246 				&& !_TextContains(package->ShortDescription(), term)
247 				&& !_TextContains(package->FullDescription(), term)) {
248 				return false;
249 			}
250 		}
251 		return true;
252 	}
253 
254 	BString SearchTerms() const
255 	{
256 		BString searchTerms;
257 		for (int32 i = 0; i < fSearchTerms.CountItems(); i++) {
258 			const BString& term = fSearchTerms.ItemAtFast(i);
259 			if (term.IsEmpty())
260 				continue;
261 			if (!searchTerms.IsEmpty())
262 				searchTerms.Append(" ");
263 			searchTerms.Append(term);
264 		}
265 		return searchTerms;
266 	}
267 
268 private:
269 	bool _TextContains(BString text, const BString& string) const
270 	{
271 		text.ToLower();
272 		int32 index = text.FindFirst(string);
273 		return index >= 0;
274 	}
275 
276 private:
277 	StringList fSearchTerms;
278 };
279 
280 
281 class IsFeaturedFilter : public PackageFilter {
282 public:
283 	IsFeaturedFilter()
284 	{
285 	}
286 
287 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
288 	{
289 		return package.Get() != NULL && package->IsProminent();
290 	}
291 };
292 
293 
294 static inline bool
295 is_source_package(const PackageInfoRef& package)
296 {
297 	const BString& packageName = package->Name();
298 	return packageName.EndsWith("_source");
299 }
300 
301 
302 static inline bool
303 is_develop_package(const PackageInfoRef& package)
304 {
305 	const BString& packageName = package->Name();
306 	return packageName.EndsWith("_devel")
307 		|| packageName.EndsWith("_debuginfo");
308 }
309 
310 
311 // #pragma mark - Model
312 
313 
314 Model::Model()
315 	:
316 	fDepots(),
317 
318 	fCategoryAudio(new PackageCategory(
319 		BitmapRef(),
320 		B_TRANSLATE("Audio"), "audio"), true),
321 	fCategoryBusiness(new PackageCategory(
322 		BitmapRef(),
323 		B_TRANSLATE("Business"), "business"), true),
324 	fCategoryDevelopment(new PackageCategory(
325 		BitmapRef(),
326 		B_TRANSLATE("Development"), "development"), true),
327 	fCategoryEducation(new PackageCategory(
328 		BitmapRef(),
329 		B_TRANSLATE("Education"), "education"), true),
330 	fCategoryGames(new PackageCategory(
331 		BitmapRef(),
332 		B_TRANSLATE("Games"), "games"), true),
333 	fCategoryGraphics(new PackageCategory(
334 		BitmapRef(),
335 		B_TRANSLATE("Graphics"), "graphics"), true),
336 	fCategoryInternetAndNetwork(new PackageCategory(
337 		BitmapRef(),
338 		B_TRANSLATE("Internet & Network"), "internetandnetwork"), true),
339 	fCategoryProductivity(new PackageCategory(
340 		BitmapRef(),
341 		B_TRANSLATE("Productivity"), "productivity"), true),
342 	fCategoryScienceAndMathematics(new PackageCategory(
343 		BitmapRef(),
344 		B_TRANSLATE("Science & Mathematics"), "scienceandmathematics"), true),
345 	fCategorySystemAndUtilities(new PackageCategory(
346 		BitmapRef(),
347 		B_TRANSLATE("System & Utilities"), "systemandutilities"), true),
348 	fCategoryVideo(new PackageCategory(
349 		BitmapRef(),
350 		B_TRANSLATE("Video"), "video"), true),
351 
352 	fCategoryFilter(PackageFilterRef(new AnyFilter(), true)),
353 	fDepotFilter(""),
354 	fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)),
355 	fIsFeaturedFilter(),
356 	fShowFeaturedPackages(true),
357 	fShowAvailablePackages(true),
358 	fShowInstalledPackages(true),
359 	fShowSourcePackages(false),
360 	fShowDevelopPackages(false)
361 {
362 	_UpdateIsFeaturedFilter();
363 
364 	// Don't forget to add new categories to this list:
365 	fCategories.Add(fCategoryGames);
366 	fCategories.Add(fCategoryBusiness);
367 	fCategories.Add(fCategoryAudio);
368 	fCategories.Add(fCategoryVideo);
369 	fCategories.Add(fCategoryGraphics);
370 	fCategories.Add(fCategoryEducation);
371 	fCategories.Add(fCategoryProductivity);
372 	fCategories.Add(fCategorySystemAndUtilities);
373 	fCategories.Add(fCategoryInternetAndNetwork);
374 	fCategories.Add(fCategoryDevelopment);
375 	fCategories.Add(fCategoryScienceAndMathematics);
376 	// TODO: The server will eventually support an API to
377 	// get the defined categories and their translated names.
378 	// This should then be used instead of hard-coded
379 	// categories and translations in the app.
380 }
381 
382 
383 Model::~Model()
384 {
385 }
386 
387 
388 LanguageModel&
389 Model::Language()
390 {
391 	return fLanguageModel;
392 }
393 
394 
395 bool
396 Model::AddListener(const ModelListenerRef& listener)
397 {
398 	return fListeners.Add(listener);
399 }
400 
401 
402 PackageList
403 Model::CreatePackageList() const
404 {
405 	// Iterate all packages from all depots.
406 	// If configured, restrict depot, filter by search terms, status, name ...
407 	PackageList resultList;
408 
409 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
410 		const DepotInfo& depot = fDepots.ItemAtFast(i);
411 
412 		if (fDepotFilter.Length() > 0 && fDepotFilter != depot.Name())
413 			continue;
414 
415 		const PackageList& packages = depot.Packages();
416 
417 		for (int32 j = 0; j < packages.CountItems(); j++) {
418 			const PackageInfoRef& package = packages.ItemAtFast(j);
419 			if (MatchesFilter(package))
420 				resultList.Add(package);
421 		}
422 	}
423 
424 	return resultList;
425 }
426 
427 
428 bool
429 Model::MatchesFilter(const PackageInfoRef& package) const
430 {
431 	return fCategoryFilter->AcceptsPackage(package)
432 			&& fSearchTermsFilter->AcceptsPackage(package)
433 			&& fIsFeaturedFilter->AcceptsPackage(package)
434 			&& (fShowAvailablePackages || package->State() != NONE)
435 			&& (fShowInstalledPackages || package->State() != ACTIVATED)
436 			&& (fShowSourcePackages || !is_source_package(package))
437 			&& (fShowDevelopPackages || !is_develop_package(package));
438 }
439 
440 
441 bool
442 Model::AddDepot(const DepotInfo& depot)
443 {
444 	return fDepots.Add(depot);
445 }
446 
447 
448 bool
449 Model::HasDepot(const BString& name) const
450 {
451 	return NULL != DepotForName(name);
452 }
453 
454 
455 const DepotInfo*
456 Model::DepotForName(const BString& name) const
457 {
458 	for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) {
459 		if (fDepots.ItemAtFast(i).Name() == name)
460 			return &fDepots.ItemAtFast(i);
461 	}
462 	return NULL;
463 }
464 
465 
466 bool
467 Model::SyncDepot(const DepotInfo& depot)
468 {
469 	for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) {
470 		const DepotInfo& existingDepot = fDepots.ItemAtFast(i);
471 		if (existingDepot.Name() == depot.Name()) {
472 			DepotInfo mergedDepot(existingDepot);
473 			mergedDepot.SyncPackages(depot.Packages());
474 			fDepots.Replace(i, mergedDepot);
475 			return true;
476 		}
477 	}
478 	return false;
479 }
480 
481 
482 void
483 Model::Clear()
484 {
485 	fDepots.Clear();
486 }
487 
488 
489 void
490 Model::SetPackageState(const PackageInfoRef& package, PackageState state)
491 {
492 	switch (state) {
493 		default:
494 		case NONE:
495 			fInstalledPackages.Remove(package);
496 			fActivatedPackages.Remove(package);
497 			fUninstalledPackages.Remove(package);
498 			break;
499 		case INSTALLED:
500 			if (!fInstalledPackages.Contains(package))
501 				fInstalledPackages.Add(package);
502 			fActivatedPackages.Remove(package);
503 			fUninstalledPackages.Remove(package);
504 			break;
505 		case ACTIVATED:
506 			if (!fInstalledPackages.Contains(package))
507 				fInstalledPackages.Add(package);
508 			if (!fActivatedPackages.Contains(package))
509 				fActivatedPackages.Add(package);
510 			fUninstalledPackages.Remove(package);
511 			break;
512 		case UNINSTALLED:
513 			fInstalledPackages.Remove(package);
514 			fActivatedPackages.Remove(package);
515 			if (!fUninstalledPackages.Contains(package))
516 				fUninstalledPackages.Add(package);
517 			break;
518 	}
519 
520 	package->SetState(state);
521 }
522 
523 
524 // #pragma mark - filters
525 
526 
527 void
528 Model::SetCategory(const BString& category)
529 {
530 	PackageFilter* filter;
531 
532 	if (category.Length() == 0)
533 		filter = new AnyFilter();
534 	else
535 		filter = new CategoryFilter(category);
536 
537 	fCategoryFilter.SetTo(filter, true);
538 }
539 
540 
541 BString
542 Model::Category() const
543 {
544 	CategoryFilter* filter
545 		= dynamic_cast<CategoryFilter*>(fCategoryFilter.Get());
546 	if (filter == NULL)
547 		return "";
548 	return filter->Category();
549 }
550 
551 
552 void
553 Model::SetDepot(const BString& depot)
554 {
555 	fDepotFilter = depot;
556 }
557 
558 
559 BString
560 Model::Depot() const
561 {
562 	return fDepotFilter;
563 }
564 
565 
566 void
567 Model::SetSearchTerms(const BString& searchTerms)
568 {
569 	PackageFilter* filter;
570 
571 	if (searchTerms.Length() == 0)
572 		filter = new AnyFilter();
573 	else
574 		filter = new SearchTermsFilter(searchTerms);
575 
576 	fSearchTermsFilter.SetTo(filter, true);
577 	_UpdateIsFeaturedFilter();
578 }
579 
580 
581 BString
582 Model::SearchTerms() const
583 {
584 	SearchTermsFilter* filter
585 		= dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get());
586 	if (filter == NULL)
587 		return "";
588 	return filter->SearchTerms();
589 }
590 
591 
592 void
593 Model::SetShowFeaturedPackages(bool show)
594 {
595 	fShowFeaturedPackages = show;
596 	_UpdateIsFeaturedFilter();
597 }
598 
599 
600 void
601 Model::SetShowAvailablePackages(bool show)
602 {
603 	fShowAvailablePackages = show;
604 }
605 
606 
607 void
608 Model::SetShowInstalledPackages(bool show)
609 {
610 	fShowInstalledPackages = show;
611 }
612 
613 
614 void
615 Model::SetShowSourcePackages(bool show)
616 {
617 	fShowSourcePackages = show;
618 }
619 
620 
621 void
622 Model::SetShowDevelopPackages(bool show)
623 {
624 	fShowDevelopPackages = show;
625 }
626 
627 
628 // #pragma mark - information retrieval
629 
630 
631 /*! Initially only superficial data is loaded from the server into the data
632     model of the packages.  When the package is viewed, additional data needs
633     to be populated including ratings.  This method takes care of that.
634 */
635 
636 void
637 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
638 {
639 	// TODO: There should probably also be a way to "unpopulate" the
640 	// package information. Maybe a cache of populated packages, so that
641 	// packages loose their extra information after a certain amount of
642 	// time when they have not been accessed/displayed in the UI. Otherwise
643 	// HaikuDepot will consume more and more resources in the packages.
644 	// Especially screen-shots will be a problem eventually.
645 	{
646 		BAutolock locker(&fLock);
647 		bool alreadyPopulated = fPopulatedPackages.Contains(package);
648 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
649 			return;
650 		if (!alreadyPopulated)
651 			fPopulatedPackages.Add(package);
652 	}
653 
654 	if ((flags & POPULATE_CHANGELOG) != 0) {
655 		_PopulatePackageChangelog(package);
656 	}
657 
658 	if ((flags & POPULATE_USER_RATINGS) != 0) {
659 		// Retrieve info from web-app
660 		BMessage info;
661 
662 		BString packageName;
663 		BString architecture;
664 		{
665 			BAutolock locker(&fLock);
666 			packageName = package->Name();
667 			architecture = package->Architecture();
668 		}
669 
670 		status_t status = fWebAppInterface.RetrieveUserRatings(packageName,
671 			architecture, 0, 50, info);
672 		if (status == B_OK) {
673 			// Parse message
674 			BMessage result;
675 			BMessage items;
676 			if (info.FindMessage("result", &result) == B_OK
677 				&& result.FindMessage("items", &items) == B_OK) {
678 
679 				BAutolock locker(&fLock);
680 				package->ClearUserRatings();
681 
682 				int32 index = 0;
683 				while (true) {
684 					BString name;
685 					name << index++;
686 
687 					BMessage item;
688 					if (items.FindMessage(name, &item) != B_OK)
689 						break;
690 
691 					BString code;
692 					if (item.FindString("code", &code) != B_OK) {
693 						printf("corrupt user rating at index %" B_PRIi32 "\n",
694 							index);
695 						continue;
696 					}
697 
698 					BString user;
699 					BMessage userInfo;
700 					if (item.FindMessage("user", &userInfo) != B_OK
701 						|| userInfo.FindString("nickname", &user) != B_OK) {
702 						printf("ignored user rating [%s] without a user "
703 							"nickname\n", code.String());
704 						continue;
705 					}
706 
707 					// Extract basic info, all items are optional
708 					BString languageCode;
709 					BString comment;
710 					double rating;
711 					item.FindString("naturalLanguageCode", &languageCode);
712 					item.FindString("comment", &comment);
713 					if (item.FindDouble("rating", &rating) != B_OK)
714 						rating = -1;
715 					if (comment.Length() == 0 && rating == -1) {
716 						printf("rating [%s] has no comment or rating so will be"
717 							"ignored\n", code.String());
718 						continue;
719 					}
720 
721 					// For which version of the package was the rating?
722 					BString major = "?";
723 					BString minor = "?";
724 					BString micro = "";
725 					double revision = -1;
726 					BMessage version;
727 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
728 						version.FindString("major", &major);
729 						version.FindString("minor", &minor);
730 						version.FindString("micro", &micro);
731 						version.FindDouble("revision", &revision);
732 					}
733 					BString versionString = major;
734 					versionString << ".";
735 					versionString << minor;
736 					if (micro.Length() > 0) {
737 						versionString << ".";
738 						versionString << micro;
739 					}
740 					if (revision > 0) {
741 						versionString << "-";
742 						versionString << (int) revision;
743 					}
744 
745 					BDateTime createTimestamp;
746 					double createTimestampMillisF;
747 					if (item.FindDouble("createTimestamp",
748 						&createTimestampMillisF) == B_OK) {
749 						double createTimestampSecsF =
750 							createTimestampMillisF / 1000.0;
751 						time_t createTimestampSecs =
752 							(time_t) createTimestampSecsF;
753 						createTimestamp.SetTime_t(createTimestampSecs);
754 					}
755 
756 					// Add the rating to the PackageInfo
757 					UserRating userRating = UserRating(UserInfo(user), rating,
758 						comment, languageCode, versionString, 0, 0,
759 						createTimestamp);
760 					package->AddUserRating(userRating);
761 
762 					if (Logger::IsDebugEnabled()) {
763 						printf("rating [%s] retrieved from server\n",
764 							code.String());
765 					}
766 				}
767 
768 				if (Logger::IsDebugEnabled()) {
769 					printf("did retrieve %" B_PRIi32 " user ratings for [%s]\n",
770 						index - 1, packageName.String());
771 				}
772 			} else {
773 				_MaybeLogJsonRpcError(info, "retrieve user ratings");
774 			}
775 		} else {
776 			printf("unable to retrieve user ratings\n");
777 		}
778 	}
779 
780 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
781 		ScreenshotInfoList screenshotInfos;
782 		{
783 			BAutolock locker(&fLock);
784 			screenshotInfos = package->ScreenshotInfos();
785 			package->ClearScreenshots();
786 		}
787 		for (int i = 0; i < screenshotInfos.CountItems(); i++) {
788 			const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i);
789 			_PopulatePackageScreenshot(package, info, 320, false);
790 		}
791 	}
792 }
793 
794 
795 void
796 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
797 {
798 	BMessage info;
799 	BString packageName;
800 
801 	{
802 		BAutolock locker(&fLock);
803 		packageName = package->Name();
804 	}
805 
806 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
807 
808 	if (status == B_OK) {
809 		// Parse message
810 		BMessage result;
811 		BString content;
812 		if (info.FindMessage("result", &result) == B_OK) {
813 			if (result.FindString("content", &content) == B_OK
814 				&& 0 != content.Length()) {
815 				BAutolock locker(&fLock);
816 				package->SetChangelog(content);
817 				if (Logger::IsDebugEnabled()) {
818 					fprintf(stdout, "changelog populated for [%s]\n",
819 						packageName.String());
820 				}
821 			} else {
822 				if (Logger::IsDebugEnabled()) {
823 					fprintf(stdout, "no changelog present for [%s]\n",
824 						packageName.String());
825 				}
826 			}
827 		} else {
828 			_MaybeLogJsonRpcError(info, "populate package changelog");
829 		}
830 	} else {
831 		fprintf(stdout, "unable to obtain the changelog for the package"
832 			" [%s]\n", packageName.String());
833 	}
834 }
835 
836 
837 void
838 Model::SetUsername(BString username)
839 {
840 	BString password;
841 	if (username.Length() > 0) {
842 		BPasswordKey key;
843 		BKeyStore keyStore;
844 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, username,
845 				key) == B_OK) {
846 			password = key.Password();
847 		} else {
848 			username = "";
849 		}
850 	}
851 	SetAuthorization(username, password, false);
852 }
853 
854 
855 const BString&
856 Model::Username() const
857 {
858 	return fWebAppInterface.Username();
859 }
860 
861 
862 void
863 Model::SetAuthorization(const BString& username, const BString& password,
864 	bool storePassword)
865 {
866 	if (storePassword && username.Length() > 0 && password.Length() > 0) {
867 		BPasswordKey key(password, B_KEY_PURPOSE_WEB, username);
868 		BKeyStore keyStore;
869 		keyStore.AddKeyring(kHaikuDepotKeyring);
870 		keyStore.AddKey(kHaikuDepotKeyring, key);
871 	}
872 
873 	BAutolock locker(&fLock);
874 	fWebAppInterface.SetAuthorization(username, password);
875 
876 	_NotifyAuthorizationChanged();
877 }
878 
879 
880 status_t
881 Model::_LocalDataPath(const BString leaf, BPath& path) const
882 {
883 	BString leafAssembled(leaf);
884 	leafAssembled.ReplaceAll("%languageCode%",
885 		LanguageModel().PreferredLanguage().Code());
886 
887 	BPath repoDataPath;
888 
889 	if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK
890 		&& repoDataPath.Append("HaikuDepot") == B_OK
891 		&& create_directory(repoDataPath.Path(), 0777) == B_OK
892 		&& repoDataPath.Append(leafAssembled) == B_OK) {
893 		path.SetTo(repoDataPath.Path());
894 		return B_OK;
895 	}
896 
897 	path.Unset();
898 	fprintf(stdout, "unable to find the user cache file for [%s] data",
899 		leaf.String());
900 	return B_ERROR;
901 }
902 
903 
904 /*! When bulk repository data comes down from the server, it will
905     arrive as a json.gz payload.  This is stored locally as a cache
906     and this method will provide the on-disk storage location for
907     this file.
908 */
909 
910 status_t
911 Model::DumpExportRepositoryDataPath(BPath& path) const
912 {
913 	return _LocalDataPath("repository-all_%languageCode%.json.gz", path);
914 }
915 
916 
917 /*! When the system downloads reference data (eg; categories) from the server
918     then the downloaded data is stored and cached at the path defined by this
919     method.
920 */
921 
922 status_t
923 Model::DumpExportReferenceDataPath(BPath& path) const
924 {
925 	return _LocalDataPath("reference-all_%languageCode%.json.gz", path);
926 }
927 
928 
929 status_t
930 Model::IconStoragePath(BPath& path) const
931 {
932 	BPath iconStoragePath;
933 
934 	if (find_directory(B_USER_CACHE_DIRECTORY, &iconStoragePath) == B_OK
935 		&& iconStoragePath.Append("HaikuDepot") == B_OK
936 		&& iconStoragePath.Append("__allicons") == B_OK
937 		&& create_directory(iconStoragePath.Path(), 0777) == B_OK) {
938 		path.SetTo(iconStoragePath.Path());
939 		return B_OK;
940 	}
941 
942 	path.Unset();
943 	fprintf(stdout, "unable to find the user cache directory for icons");
944 	return B_ERROR;
945 }
946 
947 
948 status_t
949 Model::DumpExportPkgDataPath(BPath& path,
950 	const BString& repositorySourceCode) const
951 {
952 	BPath repoDataPath;
953 	BString leafName;
954 
955 	leafName.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
956 		LanguageModel().PreferredLanguage().Code());
957 
958 	if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK
959 		&& repoDataPath.Append("HaikuDepot") == B_OK
960 		&& create_directory(repoDataPath.Path(), 0777) == B_OK
961 		&& repoDataPath.Append(leafName.String()) == B_OK) {
962 		path.SetTo(repoDataPath.Path());
963 		return B_OK;
964 	}
965 
966 	path.Unset();
967 	fprintf(stdout, "unable to find the user cache file for pkgs' data");
968 	return B_ERROR;
969 }
970 
971 
972 void
973 Model::_UpdateIsFeaturedFilter()
974 {
975 	if (fShowFeaturedPackages && SearchTerms().IsEmpty())
976 		fIsFeaturedFilter = PackageFilterRef(new IsFeaturedFilter(), true);
977 	else
978 		fIsFeaturedFilter = PackageFilterRef(new AnyFilter(), true);
979 }
980 
981 
982 void
983 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
984 	const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly)
985 {
986 	// See if there is a cached screenshot
987 	BFile screenshotFile;
988 	BPath screenshotCachePath;
989 	bool fileExists = false;
990 	BString screenshotName(info.Code());
991 	screenshotName << "@" << scaledWidth;
992 	screenshotName << ".png";
993 	time_t modifiedTime;
994 	if (find_directory(B_USER_CACHE_DIRECTORY, &screenshotCachePath) == B_OK
995 		&& screenshotCachePath.Append("HaikuDepot/Screenshots") == B_OK
996 		&& create_directory(screenshotCachePath.Path(), 0777) == B_OK
997 		&& screenshotCachePath.Append(screenshotName) == B_OK) {
998 		// Try opening the file in read-only mode, which will fail if its
999 		// not a file or does not exist.
1000 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
1001 			B_READ_ONLY) == B_OK;
1002 		if (fileExists)
1003 			screenshotFile.GetModificationTime(&modifiedTime);
1004 	}
1005 
1006 	if (fileExists) {
1007 		time_t now;
1008 		time(&now);
1009 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
1010 			// Cache file is recent enough, just use it and return.
1011 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
1012 				true);
1013 			BAutolock locker(&fLock);
1014 			package->AddScreenshot(bitmapRef);
1015 			return;
1016 		}
1017 	}
1018 
1019 	if (fromCacheOnly)
1020 		return;
1021 
1022 	// Retrieve screenshot from web-app
1023 	BMallocIO buffer;
1024 
1025 	int32 scaledHeight = scaledWidth * info.Height() / info.Width();
1026 
1027 	status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(),
1028 		scaledWidth, scaledHeight, &buffer);
1029 	if (status == B_OK) {
1030 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
1031 		BAutolock locker(&fLock);
1032 		package->AddScreenshot(bitmapRef);
1033 		locker.Unlock();
1034 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
1035 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
1036 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
1037 		}
1038 	} else {
1039 		fprintf(stderr, "Failed to retrieve screenshot for code '%s' "
1040 			"at %" B_PRIi32 "x%" B_PRIi32 ".\n", info.Code().String(),
1041 			scaledWidth, scaledHeight);
1042 	}
1043 }
1044 
1045 
1046 // #pragma mark - listener notification methods
1047 
1048 
1049 void
1050 Model::_NotifyAuthorizationChanged()
1051 {
1052 	for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) {
1053 		const ModelListenerRef& listener = fListeners.ItemAtFast(i);
1054 		if (listener.Get() != NULL)
1055 			listener->AuthorizationChanged();
1056 	}
1057 }
1058 
1059 
1060 /*! This method will find the stored 'DepotInfo' that correlates to the
1061     supplied 'url' and will invoke the mapper function in order to get a
1062     replacement for the 'DepotInfo'.  The 'url' is a unique identifier
1063     for the repository that holds across mirrors.
1064 */
1065 
1066 void
1067 Model::ReplaceDepotByUrl(const BString& URL, DepotMapper* depotMapper,
1068 	void* context)
1069 {
1070 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
1071 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1072 
1073 		if (RepositoryUrlUtils::EqualsNormalized(URL, depotInfo.URL())) {
1074 			BAutolock locker(&fLock);
1075 			fDepots.Replace(i, depotMapper->MapDepot(depotInfo, context));
1076 		}
1077 	}
1078 }
1079 
1080 
1081 void
1082 Model::LogDepotsWithNoWebAppRepositoryCode() const
1083 {
1084 	int32 i;
1085 
1086 	for (i = 0; i < fDepots.CountItems(); i++) {
1087 		const DepotInfo& depot = fDepots.ItemAt(i);
1088 
1089 		if (depot.WebAppRepositoryCode().Length() == 0) {
1090 			printf("depot [%s]", depot.Name().String());
1091 
1092 			if (depot.URL().Length() > 0)
1093 				printf(" (%s)", depot.URL().String());
1094 
1095 			printf(" correlates with no repository in the haiku"
1096 				"depot server system\n");
1097 		}
1098 	}
1099 }
1100 
1101 
1102 void
1103 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
1104 	const char *sourceDescription) const
1105 {
1106 	BMessage error;
1107 	BString errorMessage;
1108 	double errorCode;
1109 
1110 	if (responsePayload.FindMessage("error", &error) == B_OK
1111 		&& error.FindString("message", &errorMessage) == B_OK
1112 		&& error.FindDouble("code", &errorCode) == B_OK) {
1113 		printf("[%s] --> error : [%s] (%f)\n", sourceDescription,
1114 			errorMessage.String(), errorCode);
1115 
1116 	} else {
1117 		printf("[%s] --> an undefined error has occurred\n", sourceDescription);
1118 	}
1119 }
1120