xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision f7c507c3a6fbf3a44c59500543926a9088724968)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2018, 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 
357 	fShowFeaturedPackages(true),
358 	fShowAvailablePackages(true),
359 	fShowInstalledPackages(true),
360 	fShowSourcePackages(false),
361 	fShowDevelopPackages(false)
362 {
363 	_UpdateIsFeaturedFilter();
364 
365 	// Don't forget to add new categories to this list:
366 	fCategories.Add(fCategoryGames);
367 	fCategories.Add(fCategoryBusiness);
368 	fCategories.Add(fCategoryAudio);
369 	fCategories.Add(fCategoryVideo);
370 	fCategories.Add(fCategoryGraphics);
371 	fCategories.Add(fCategoryEducation);
372 	fCategories.Add(fCategoryProductivity);
373 	fCategories.Add(fCategorySystemAndUtilities);
374 	fCategories.Add(fCategoryInternetAndNetwork);
375 	fCategories.Add(fCategoryDevelopment);
376 	fCategories.Add(fCategoryScienceAndMathematics);
377 	// TODO: The server will eventually support an API to
378 	// get the defined categories and their translated names.
379 	// This should then be used instead of hard-coded
380 	// categories and translations in the app.
381 
382 	fPreferredLanguage = "en";
383 	BLocaleRoster* localeRoster = BLocaleRoster::Default();
384 	if (localeRoster != NULL) {
385 		BMessage preferredLanguages;
386 		if (localeRoster->GetPreferredLanguages(&preferredLanguages) == B_OK) {
387 			BString language;
388 			if (preferredLanguages.FindString("language", 0, &language) == B_OK)
389 				language.CopyInto(fPreferredLanguage, 0, 2);
390 		}
391 	}
392 
393 	// TODO: Fetch this from the web-app.
394 	fSupportedLanguages.Add("en");
395 	fSupportedLanguages.Add("de");
396 	fSupportedLanguages.Add("fr");
397 	fSupportedLanguages.Add("ja");
398 	fSupportedLanguages.Add("es");
399 	fSupportedLanguages.Add("zh");
400 	fSupportedLanguages.Add("pt");
401 	fSupportedLanguages.Add("ru");
402 
403 	if (!fSupportedLanguages.Contains(fPreferredLanguage)) {
404 		// Force the preferred language to one of the currently supported
405 		// ones, until the web application supports all ISO language codes.
406 		printf("User preferred language '%s' not currently supported, "
407 			"defaulting to 'en'.", fPreferredLanguage.String());
408 		fPreferredLanguage = "en";
409 	}
410 	fWebAppInterface.SetPreferredLanguage(fPreferredLanguage);
411 }
412 
413 
414 Model::~Model()
415 {
416 }
417 
418 
419 bool
420 Model::AddListener(const ModelListenerRef& listener)
421 {
422 	return fListeners.Add(listener);
423 }
424 
425 
426 PackageList
427 Model::CreatePackageList() const
428 {
429 	// Iterate all packages from all depots.
430 	// If configured, restrict depot, filter by search terms, status, name ...
431 	PackageList resultList;
432 
433 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
434 		const DepotInfo& depot = fDepots.ItemAtFast(i);
435 
436 		if (fDepotFilter.Length() > 0 && fDepotFilter != depot.Name())
437 			continue;
438 
439 		const PackageList& packages = depot.Packages();
440 
441 		for (int32 j = 0; j < packages.CountItems(); j++) {
442 			const PackageInfoRef& package = packages.ItemAtFast(j);
443 			if (MatchesFilter(package))
444 				resultList.Add(package);
445 		}
446 	}
447 
448 	return resultList;
449 }
450 
451 
452 bool
453 Model::MatchesFilter(const PackageInfoRef& package) const
454 {
455 	return fCategoryFilter->AcceptsPackage(package)
456 			&& fSearchTermsFilter->AcceptsPackage(package)
457 			&& fIsFeaturedFilter->AcceptsPackage(package)
458 			&& (fShowAvailablePackages || package->State() != NONE)
459 			&& (fShowInstalledPackages || package->State() != ACTIVATED)
460 			&& (fShowSourcePackages || !is_source_package(package))
461 			&& (fShowDevelopPackages || !is_develop_package(package));
462 }
463 
464 
465 bool
466 Model::AddDepot(const DepotInfo& depot)
467 {
468 	return fDepots.Add(depot);
469 }
470 
471 
472 bool
473 Model::HasDepot(const BString& name) const
474 {
475 	return NULL != DepotForName(name);
476 }
477 
478 
479 const DepotInfo*
480 Model::DepotForName(const BString& name) const
481 {
482 	for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) {
483 		if (fDepots.ItemAtFast(i).Name() == name)
484 			return &fDepots.ItemAtFast(i);
485 	}
486 	return NULL;
487 }
488 
489 
490 bool
491 Model::SyncDepot(const DepotInfo& depot)
492 {
493 	for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) {
494 		const DepotInfo& existingDepot = fDepots.ItemAtFast(i);
495 		if (existingDepot.Name() == depot.Name()) {
496 			DepotInfo mergedDepot(existingDepot);
497 			mergedDepot.SyncPackages(depot.Packages());
498 			fDepots.Replace(i, mergedDepot);
499 			return true;
500 		}
501 	}
502 	return false;
503 }
504 
505 
506 void
507 Model::Clear()
508 {
509 	fDepots.Clear();
510 }
511 
512 
513 void
514 Model::SetPackageState(const PackageInfoRef& package, PackageState state)
515 {
516 	switch (state) {
517 		default:
518 		case NONE:
519 			fInstalledPackages.Remove(package);
520 			fActivatedPackages.Remove(package);
521 			fUninstalledPackages.Remove(package);
522 			break;
523 		case INSTALLED:
524 			if (!fInstalledPackages.Contains(package))
525 				fInstalledPackages.Add(package);
526 			fActivatedPackages.Remove(package);
527 			fUninstalledPackages.Remove(package);
528 			break;
529 		case ACTIVATED:
530 			if (!fInstalledPackages.Contains(package))
531 				fInstalledPackages.Add(package);
532 			if (!fActivatedPackages.Contains(package))
533 				fActivatedPackages.Add(package);
534 			fUninstalledPackages.Remove(package);
535 			break;
536 		case UNINSTALLED:
537 			fInstalledPackages.Remove(package);
538 			fActivatedPackages.Remove(package);
539 			if (!fUninstalledPackages.Contains(package))
540 				fUninstalledPackages.Add(package);
541 			break;
542 	}
543 
544 	package->SetState(state);
545 }
546 
547 
548 // #pragma mark - filters
549 
550 
551 void
552 Model::SetCategory(const BString& category)
553 {
554 	PackageFilter* filter;
555 
556 	if (category.Length() == 0)
557 		filter = new AnyFilter();
558 	else
559 		filter = new CategoryFilter(category);
560 
561 	fCategoryFilter.SetTo(filter, true);
562 }
563 
564 
565 BString
566 Model::Category() const
567 {
568 	CategoryFilter* filter
569 		= dynamic_cast<CategoryFilter*>(fCategoryFilter.Get());
570 	if (filter == NULL)
571 		return "";
572 	return filter->Category();
573 }
574 
575 
576 void
577 Model::SetDepot(const BString& depot)
578 {
579 	fDepotFilter = depot;
580 }
581 
582 
583 BString
584 Model::Depot() const
585 {
586 	return fDepotFilter;
587 }
588 
589 
590 void
591 Model::SetSearchTerms(const BString& searchTerms)
592 {
593 	PackageFilter* filter;
594 
595 	if (searchTerms.Length() == 0)
596 		filter = new AnyFilter();
597 	else
598 		filter = new SearchTermsFilter(searchTerms);
599 
600 	fSearchTermsFilter.SetTo(filter, true);
601 	_UpdateIsFeaturedFilter();
602 }
603 
604 
605 BString
606 Model::SearchTerms() const
607 {
608 	SearchTermsFilter* filter
609 		= dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get());
610 	if (filter == NULL)
611 		return "";
612 	return filter->SearchTerms();
613 }
614 
615 
616 void
617 Model::SetShowFeaturedPackages(bool show)
618 {
619 	fShowFeaturedPackages = show;
620 	_UpdateIsFeaturedFilter();
621 }
622 
623 
624 void
625 Model::SetShowAvailablePackages(bool show)
626 {
627 	fShowAvailablePackages = show;
628 }
629 
630 
631 void
632 Model::SetShowInstalledPackages(bool show)
633 {
634 	fShowInstalledPackages = show;
635 }
636 
637 
638 void
639 Model::SetShowSourcePackages(bool show)
640 {
641 	fShowSourcePackages = show;
642 }
643 
644 
645 void
646 Model::SetShowDevelopPackages(bool show)
647 {
648 	fShowDevelopPackages = show;
649 }
650 
651 
652 // #pragma mark - information retrieval
653 
654 
655 /*! Initially only superficial data is loaded from the server into the data
656     model of the packages.  When the package is viewed, additional data needs
657     to be populated including ratings.  This method takes care of that.
658 */
659 
660 void
661 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
662 {
663 	// TODO: There should probably also be a way to "unpopulate" the
664 	// package information. Maybe a cache of populated packages, so that
665 	// packages loose their extra information after a certain amount of
666 	// time when they have not been accessed/displayed in the UI. Otherwise
667 	// HaikuDepot will consume more and more resources in the packages.
668 	// Especially screen-shots will be a problem eventually.
669 	{
670 		BAutolock locker(&fLock);
671 		bool alreadyPopulated = fPopulatedPackages.Contains(package);
672 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
673 			return;
674 		if (!alreadyPopulated)
675 			fPopulatedPackages.Add(package);
676 	}
677 
678 	if ((flags & POPULATE_CHANGELOG) != 0) {
679 		_PopulatePackageChangelog(package);
680 	}
681 
682 	if ((flags & POPULATE_USER_RATINGS) != 0) {
683 		// Retrieve info from web-app
684 		BMessage info;
685 
686 		BString packageName;
687 		BString architecture;
688 		{
689 			BAutolock locker(&fLock);
690 			packageName = package->Name();
691 			architecture = package->Architecture();
692 		}
693 
694 		status_t status = fWebAppInterface.RetrieveUserRatings(packageName,
695 			architecture, 0, 50, info);
696 		if (status == B_OK) {
697 			// Parse message
698 			BMessage result;
699 			BMessage items;
700 			if (info.FindMessage("result", &result) == B_OK
701 				&& result.FindMessage("items", &items) == B_OK) {
702 
703 				BAutolock locker(&fLock);
704 				package->ClearUserRatings();
705 
706 				int32 index = 0;
707 				while (true) {
708 					BString name;
709 					name << index++;
710 
711 					BMessage item;
712 					if (items.FindMessage(name, &item) != B_OK)
713 						break;
714 
715 					BString code;
716 					if (item.FindString("code", &code) != B_OK) {
717 						printf("corrupt user rating at index %" B_PRIi32 "\n",
718 							index);
719 						continue;
720 					}
721 
722 					BString user;
723 					BMessage userInfo;
724 					if (item.FindMessage("user", &userInfo) != B_OK
725 						|| userInfo.FindString("nickname", &user) != B_OK) {
726 						printf("ignored user rating [%s] without a user "
727 							"nickname\n", code.String());
728 						continue;
729 					}
730 
731 					// Extract basic info, all items are optional
732 					BString languageCode;
733 					BString comment;
734 					double rating;
735 					item.FindString("naturalLanguageCode", &languageCode);
736 					item.FindString("comment", &comment);
737 					if (item.FindDouble("rating", &rating) != B_OK)
738 						rating = -1;
739 					if (comment.Length() == 0 && rating == -1) {
740 						printf("rating [%s] has no comment or rating so will be"
741 							"ignored\n", code.String());
742 						continue;
743 					}
744 
745 					// For which version of the package was the rating?
746 					BString major = "?";
747 					BString minor = "?";
748 					BString micro = "";
749 					double revision = -1;
750 					BMessage version;
751 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
752 						version.FindString("major", &major);
753 						version.FindString("minor", &minor);
754 						version.FindString("micro", &micro);
755 						version.FindDouble("revision", &revision);
756 					}
757 					BString versionString = major;
758 					versionString << ".";
759 					versionString << minor;
760 					if (micro.Length() > 0) {
761 						versionString << ".";
762 						versionString << micro;
763 					}
764 					if (revision > 0) {
765 						versionString << "-";
766 						versionString << (int) revision;
767 					}
768 
769 					BDateTime createTimestamp;
770 					double createTimestampMillisF;
771 					if (item.FindDouble("createTimestamp",
772 						&createTimestampMillisF) == B_OK) {
773 						double createTimestampSecsF =
774 							createTimestampMillisF / 1000.0;
775 						time_t createTimestampSecs =
776 							(time_t) createTimestampSecsF;
777 						createTimestamp.SetTime_t(createTimestampSecs);
778 					}
779 
780 					// Add the rating to the PackageInfo
781 					UserRating userRating = UserRating(UserInfo(user), rating,
782 						comment, languageCode, versionString, 0, 0,
783 						createTimestamp);
784 					package->AddUserRating(userRating);
785 
786 					if (Logger::IsDebugEnabled()) {
787 						printf("rating [%s] retrieved from server\n",
788 							code.String());
789 					}
790 				}
791 
792 				if (Logger::IsDebugEnabled()) {
793 					printf("did retrieve %" B_PRIi32 " user ratings for [%s]\n",
794 						index - 1, packageName.String());
795 				}
796 			} else {
797 				_MaybeLogJsonRpcError(info, "retrieve user ratings");
798 			}
799 		} else {
800 			printf("unable to retrieve user ratings\n");
801 		}
802 	}
803 
804 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
805 		ScreenshotInfoList screenshotInfos;
806 		{
807 			BAutolock locker(&fLock);
808 			screenshotInfos = package->ScreenshotInfos();
809 			package->ClearScreenshots();
810 		}
811 		for (int i = 0; i < screenshotInfos.CountItems(); i++) {
812 			const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i);
813 			_PopulatePackageScreenshot(package, info, 320, false);
814 		}
815 	}
816 }
817 
818 
819 void
820 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
821 {
822 	BMessage info;
823 	BString packageName;
824 
825 	{
826 		BAutolock locker(&fLock);
827 		packageName = package->Name();
828 	}
829 
830 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
831 
832 	if (status == B_OK) {
833 		// Parse message
834 		BMessage result;
835 		BString content;
836 		if (info.FindMessage("result", &result) == B_OK) {
837 			if (result.FindString("content", &content) == B_OK
838 				&& 0 != content.Length()) {
839 				BAutolock locker(&fLock);
840 				package->SetChangelog(content);
841 				if (Logger::IsDebugEnabled()) {
842 					fprintf(stdout, "changelog populated for [%s]\n",
843 						packageName.String());
844 				}
845 			} else {
846 				if (Logger::IsDebugEnabled()) {
847 					fprintf(stdout, "no changelog present for [%s]\n",
848 						packageName.String());
849 				}
850 			}
851 		} else {
852 			_MaybeLogJsonRpcError(info, "populate package changelog");
853 		}
854 	} else {
855 		fprintf(stdout, "unable to obtain the changelog for the package"
856 			" [%s]\n", packageName.String());
857 	}
858 }
859 
860 
861 void
862 Model::SetUsername(BString username)
863 {
864 	BString password;
865 	if (username.Length() > 0) {
866 		BPasswordKey key;
867 		BKeyStore keyStore;
868 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, username,
869 				key) == B_OK) {
870 			password = key.Password();
871 		} else {
872 			username = "";
873 		}
874 	}
875 	SetAuthorization(username, password, false);
876 }
877 
878 
879 const BString&
880 Model::Username() const
881 {
882 	return fWebAppInterface.Username();
883 }
884 
885 
886 void
887 Model::SetAuthorization(const BString& username, const BString& password,
888 	bool storePassword)
889 {
890 	if (storePassword && username.Length() > 0 && password.Length() > 0) {
891 		BPasswordKey key(password, B_KEY_PURPOSE_WEB, username);
892 		BKeyStore keyStore;
893 		keyStore.AddKeyring(kHaikuDepotKeyring);
894 		keyStore.AddKey(kHaikuDepotKeyring, key);
895 	}
896 
897 	BAutolock locker(&fLock);
898 	fWebAppInterface.SetAuthorization(username, password);
899 
900 	_NotifyAuthorizationChanged();
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 	BPath repoDataPath;
914 
915 	if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK
916 		&& repoDataPath.Append("HaikuDepot") == B_OK
917 		&& create_directory(repoDataPath.Path(), 0777) == B_OK
918 		&& repoDataPath.Append("repository-all_en.json.gz") == B_OK) {
919 		path.SetTo(repoDataPath.Path());
920 		return B_OK;
921 	}
922 
923 	path.Unset();
924 	fprintf(stdout, "unable to find the user cache file for repositories'"
925 		" data");
926 	return B_ERROR;
927 }
928 
929 
930 status_t
931 Model::IconStoragePath(BPath& path) const
932 {
933 	BPath iconStoragePath;
934 
935 	if (find_directory(B_USER_CACHE_DIRECTORY, &iconStoragePath) == B_OK
936 		&& iconStoragePath.Append("HaikuDepot") == B_OK
937 		&& iconStoragePath.Append("__allicons") == B_OK
938 		&& create_directory(iconStoragePath.Path(), 0777) == B_OK) {
939 		path.SetTo(iconStoragePath.Path());
940 		return B_OK;
941 	}
942 
943 	path.Unset();
944 	fprintf(stdout, "unable to find the user cache directory for icons");
945 	return B_ERROR;
946 }
947 
948 
949 status_t
950 Model::DumpExportPkgDataPath(BPath& path,
951 	const BString& repositorySourceCode) const
952 {
953 	BPath repoDataPath;
954 	BString leafName;
955 
956 	leafName.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
957 		fPreferredLanguage.String());
958 
959 	if (find_directory(B_USER_CACHE_DIRECTORY, &repoDataPath) == B_OK
960 		&& repoDataPath.Append("HaikuDepot") == B_OK
961 		&& create_directory(repoDataPath.Path(), 0777) == B_OK
962 		&& repoDataPath.Append(leafName.String()) == B_OK) {
963 		path.SetTo(repoDataPath.Path());
964 		return B_OK;
965 	}
966 
967 	path.Unset();
968 	fprintf(stdout, "unable to find the user cache file for pkgs' data");
969 	return B_ERROR;
970 }
971 
972 
973 void
974 Model::_UpdateIsFeaturedFilter()
975 {
976 	if (fShowFeaturedPackages && SearchTerms().IsEmpty())
977 		fIsFeaturedFilter = PackageFilterRef(new IsFeaturedFilter(), true);
978 	else
979 		fIsFeaturedFilter = PackageFilterRef(new AnyFilter(), true);
980 }
981 
982 
983 void
984 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
985 	const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly)
986 {
987 	// See if there is a cached screenshot
988 	BFile screenshotFile;
989 	BPath screenshotCachePath;
990 	bool fileExists = false;
991 	BString screenshotName(info.Code());
992 	screenshotName << "@" << scaledWidth;
993 	screenshotName << ".png";
994 	time_t modifiedTime;
995 	if (find_directory(B_USER_CACHE_DIRECTORY, &screenshotCachePath) == B_OK
996 		&& screenshotCachePath.Append("HaikuDepot/Screenshots") == B_OK
997 		&& create_directory(screenshotCachePath.Path(), 0777) == B_OK
998 		&& screenshotCachePath.Append(screenshotName) == B_OK) {
999 		// Try opening the file in read-only mode, which will fail if its
1000 		// not a file or does not exist.
1001 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
1002 			B_READ_ONLY) == B_OK;
1003 		if (fileExists)
1004 			screenshotFile.GetModificationTime(&modifiedTime);
1005 	}
1006 
1007 	if (fileExists) {
1008 		time_t now;
1009 		time(&now);
1010 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
1011 			// Cache file is recent enough, just use it and return.
1012 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
1013 				true);
1014 			BAutolock locker(&fLock);
1015 			package->AddScreenshot(bitmapRef);
1016 			return;
1017 		}
1018 	}
1019 
1020 	if (fromCacheOnly)
1021 		return;
1022 
1023 	// Retrieve screenshot from web-app
1024 	BMallocIO buffer;
1025 
1026 	int32 scaledHeight = scaledWidth * info.Height() / info.Width();
1027 
1028 	status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(),
1029 		scaledWidth, scaledHeight, &buffer);
1030 	if (status == B_OK) {
1031 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
1032 		BAutolock locker(&fLock);
1033 		package->AddScreenshot(bitmapRef);
1034 		locker.Unlock();
1035 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
1036 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
1037 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
1038 		}
1039 	} else {
1040 		fprintf(stderr, "Failed to retrieve screenshot for code '%s' "
1041 			"at %" B_PRIi32 "x%" B_PRIi32 ".\n", info.Code().String(),
1042 			scaledWidth, scaledHeight);
1043 	}
1044 }
1045 
1046 
1047 // #pragma mark - listener notification methods
1048 
1049 
1050 void
1051 Model::_NotifyAuthorizationChanged()
1052 {
1053 	for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) {
1054 		const ModelListenerRef& listener = fListeners.ItemAtFast(i);
1055 		if (listener.Get() != NULL)
1056 			listener->AuthorizationChanged();
1057 	}
1058 }
1059 
1060 
1061 void
1062 Model::ForAllDepots(void (*func)(const DepotInfo& depot, void* context),
1063 	void* context)
1064 {
1065 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
1066 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1067 		func(depotInfo, context);
1068 	}
1069 }
1070 
1071 
1072 /*! This method will find the stored 'DepotInfo' that correlates to the
1073     supplied 'url' or 'baseUrl' and will invoke the mapper function in
1074     order to get a replacement for the 'DepotInfo'.  The two URLs are
1075     different.  The 'url' is a unique identifier for the repository that
1076     holds across mirrors.  The 'baseUrl' is the URL stem that was used
1077     to access the repository data in the first place.  The 'baseUrl' is
1078     a legacy construct that exists from a time where the identifying
1079     'url' was not being relayed properly.
1080 */
1081 
1082 void
1083 Model::ReplaceDepotByUrl(
1084 	const BString& URL,
1085 	const BString& baseURL,
1086 		// deprecated
1087 	DepotMapper* depotMapper, void* context)
1088 {
1089 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
1090 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1091 
1092 		if (RepositoryUrlUtils::EqualsOnUrlOrBaseUrl(URL, depotInfo.URL(),
1093 			baseURL, depotInfo.BaseURL())) {
1094 			BAutolock locker(&fLock);
1095 			fDepots.Replace(i, depotMapper->MapDepot(depotInfo, context));
1096 		}
1097 	}
1098 }
1099 
1100 
1101 void
1102 Model::ForAllPackages(PackageConsumer* packageConsumer, void* context)
1103 {
1104 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
1105 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1106 		PackageList packages = depotInfo.Packages();
1107 		for(int32 j = 0; j < packages.CountItems(); j++) {
1108 			const PackageInfoRef& packageInfoRef = packages.ItemAtFast(j);
1109 
1110 			if (packageInfoRef != NULL) {
1111 				BAutolock locker(&fLock);
1112 				if (!packageConsumer->ConsumePackage(packageInfoRef, context))
1113 					return;
1114 			}
1115 		}
1116 	}
1117 }
1118 
1119 
1120 void
1121 Model::ForPackageByNameInDepot(const BString& depotName,
1122 	const BString& packageName, PackageConsumer* packageConsumer, void* context)
1123 {
1124 	int32 depotCount = fDepots.CountItems();
1125 
1126 	for (int32 i = 0; i < depotCount; i++) {
1127 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1128 
1129 		if (depotInfo.Name() == depotName) {
1130 			int32 packageIndex = depotInfo.PackageIndexByName(packageName);
1131 
1132 			if (-1 != packageIndex) {
1133 				PackageList packages = depotInfo.Packages();
1134 				const PackageInfoRef& packageInfoRef =
1135 					packages.ItemAtFast(packageIndex);
1136 
1137 				BAutolock locker(&fLock);
1138 				packageConsumer->ConsumePackage(packageInfoRef,
1139 					context);
1140 			}
1141 
1142 			return;
1143 		}
1144 	}
1145 }
1146 
1147 
1148 void
1149 Model::LogDepotsWithNoWebAppRepositoryCode() const
1150 {
1151 	int32 i;
1152 
1153 	for (i = 0; i < fDepots.CountItems(); i++) {
1154 		const DepotInfo& depot = fDepots.ItemAt(i);
1155 
1156 		if (depot.WebAppRepositoryCode().Length() == 0) {
1157 			printf("depot [%s]", depot.Name().String());
1158 
1159 			if (depot.BaseURL().Length() > 0)
1160 				printf(" (%s)", depot.BaseURL().String());
1161 
1162 			printf(" correlates with no repository in the haiku"
1163 				"depot server system\n");
1164 		}
1165 	}
1166 }
1167 
1168 
1169 void
1170 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
1171 	const char *sourceDescription) const
1172 {
1173 	BMessage error;
1174 	BString errorMessage;
1175 	double errorCode;
1176 
1177 	if (responsePayload.FindMessage("error", &error) == B_OK
1178 		&& error.FindString("message", &errorMessage) == B_OK
1179 		&& error.FindDouble("code", &errorCode) == B_OK) {
1180 		printf("[%s] --> error : [%s] (%f)\n", sourceDescription,
1181 			errorMessage.String(), errorCode);
1182 
1183 	} else {
1184 		printf("[%s] --> an undefined error has occurred\n", sourceDescription);
1185 	}
1186 }
1187