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