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