xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 5889cb5e7e8e7bfea6072ddfe881f55d364a0cf0)
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 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
727 		ScreenshotInfoList screenshotInfos;
728 		{
729 			BAutolock locker(&fLock);
730 			screenshotInfos = package->ScreenshotInfos();
731 			package->ClearScreenshots();
732 		}
733 		for (int i = 0; i < screenshotInfos.CountItems(); i++) {
734 			const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i);
735 			_PopulatePackageScreenshot(package, info, 320, false);
736 		}
737 	}
738 }
739 
740 
741 void
742 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
743 {
744 	BMessage info;
745 	BString packageName;
746 
747 	{
748 		BAutolock locker(&fLock);
749 		packageName = package->Name();
750 	}
751 
752 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
753 
754 	if (status == B_OK) {
755 		// Parse message
756 		BMessage result;
757 		BString content;
758 		if (info.FindMessage("result", &result) == B_OK) {
759 			if (result.FindString("content", &content) == B_OK
760 				&& 0 != content.Length()) {
761 				BAutolock locker(&fLock);
762 				package->SetChangelog(content);
763 				HDDEBUG("changelog populated for [%s]", packageName.String());
764 			} else
765 				HDDEBUG("no changelog present for [%s]", packageName.String());
766 		} else
767 			_MaybeLogJsonRpcError(info, "populate package changelog");
768 	} else {
769 		HDERROR("unable to obtain the changelog for the package [%s]",
770 			packageName.String());
771 	}
772 }
773 
774 
775 static void
776 model_remove_key_for_user(const BString& nickname)
777 {
778 	if (nickname.IsEmpty())
779 		return;
780 	BKeyStore keyStore;
781 	BPasswordKey key;
782 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
783 		<< nickname;
784 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
785 			passwordIdentifier, key);
786 
787 	switch (result) {
788 		case B_OK:
789 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
790 			if (result != B_OK) {
791 				HDERROR("error occurred when removing password for nickname "
792 					"[%s] : %s", nickname.String(), strerror(result));
793 			}
794 			break;
795 		case B_ENTRY_NOT_FOUND:
796 			return;
797 		default:
798 			HDERROR("error occurred when finding password for nickname "
799 				"[%s] : %s", nickname.String(), strerror(result));
800 			break;
801 	}
802 }
803 
804 
805 void
806 Model::SetNickname(BString nickname)
807 {
808 	BString password;
809 	BString existingNickname = Nickname();
810 
811 	// this happens when the user is logging out.  Best to remove the password
812 	// stored for the existing user since it is no longer required.
813 
814 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
815 		model_remove_key_for_user(existingNickname);
816 
817 	if (nickname.Length() > 0) {
818 		BPasswordKey key;
819 		BKeyStore keyStore;
820 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
821 			<< nickname;
822 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
823 				passwordIdentifier, key) == B_OK) {
824 			password = key.Password();
825 		}
826 		if (password.IsEmpty())
827 			nickname = "";
828 	}
829 
830 	SetAuthorization(nickname, password, false);
831 }
832 
833 
834 const BString&
835 Model::Nickname() const
836 {
837 	return fWebAppInterface.Nickname();
838 }
839 
840 
841 void
842 Model::SetAuthorization(const BString& nickname, const BString& passwordClear,
843 	bool storePassword)
844 {
845 	BString existingNickname = Nickname();
846 
847 	if (storePassword) {
848 		// no point continuing to store the password for the previous user.
849 
850 		if (!existingNickname.IsEmpty())
851 			model_remove_key_for_user(existingNickname);
852 
853 		// adding a key that is already there does not seem to override the
854 		// existing key so the old key needs to be removed first.
855 
856 		if (!nickname.IsEmpty())
857 			model_remove_key_for_user(nickname);
858 
859 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
860 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
861 				<< nickname;
862 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
863 			BKeyStore keyStore;
864 			keyStore.AddKeyring(kHaikuDepotKeyring);
865 			keyStore.AddKey(kHaikuDepotKeyring, key);
866 		}
867 	}
868 
869 	BAutolock locker(&fLock);
870 	fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear));
871 
872 	if (nickname != existingNickname)
873 		_NotifyAuthorizationChanged();
874 }
875 
876 
877 /*! When bulk repository data comes down from the server, it will
878     arrive as a json.gz payload.  This is stored locally as a cache
879     and this method will provide the on-disk storage location for
880     this file.
881 */
882 
883 status_t
884 Model::DumpExportRepositoryDataPath(BPath& path)
885 {
886 	BString leaf;
887 	leaf.SetToFormat("repository-all_%s.json.gz",
888 		Language()->PreferredLanguage()->Code());
889 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
890 }
891 
892 
893 /*! When the system downloads reference data (eg; categories) from the server
894     then the downloaded data is stored and cached at the path defined by this
895     method.
896 */
897 
898 status_t
899 Model::DumpExportReferenceDataPath(BPath& path)
900 {
901 	BString leaf;
902 	leaf.SetToFormat("reference-all_%s.json.gz",
903 		Language()->PreferredLanguage()->Code());
904 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
905 }
906 
907 
908 status_t
909 Model::IconStoragePath(BPath& path) const
910 {
911 	return StorageUtils::LocalWorkingDirectoryPath("__allicons", path);
912 }
913 
914 
915 status_t
916 Model::DumpExportPkgDataPath(BPath& path,
917 	const BString& repositorySourceCode)
918 {
919 	BString leaf;
920 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
921 		Language()->PreferredLanguage()->Code());
922 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
923 }
924 
925 
926 void
927 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
928 	const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly)
929 {
930 	// See if there is a cached screenshot
931 	BFile screenshotFile;
932 	BPath screenshotCachePath;
933 
934 	status_t result = StorageUtils::LocalWorkingDirectoryPath(
935 		"Screenshots", screenshotCachePath);
936 
937 	if (result != B_OK) {
938 		HDERROR("unable to get the screenshot dir - unable to proceed");
939 		return;
940 	}
941 
942 	bool fileExists = false;
943 	BString screenshotName(info.Code());
944 	screenshotName << "@" << scaledWidth;
945 	screenshotName << ".png";
946 	time_t modifiedTime;
947 	if (screenshotCachePath.Append(screenshotName) == B_OK) {
948 		// Try opening the file in read-only mode, which will fail if its
949 		// not a file or does not exist.
950 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
951 			B_READ_ONLY) == B_OK;
952 		if (fileExists)
953 			screenshotFile.GetModificationTime(&modifiedTime);
954 	}
955 
956 	if (fileExists) {
957 		time_t now;
958 		time(&now);
959 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
960 			// Cache file is recent enough, just use it and return.
961 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
962 				true);
963 			BAutolock locker(&fLock);
964 			package->AddScreenshot(bitmapRef);
965 			return;
966 		}
967 	}
968 
969 	if (fromCacheOnly)
970 		return;
971 
972 	// Retrieve screenshot from web-app
973 	BMallocIO buffer;
974 
975 	int32 scaledHeight = scaledWidth * info.Height() / info.Width();
976 
977 	status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(),
978 		scaledWidth, scaledHeight, &buffer);
979 	if (status == B_OK) {
980 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
981 		BAutolock locker(&fLock);
982 		package->AddScreenshot(bitmapRef);
983 		locker.Unlock();
984 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
985 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
986 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
987 		}
988 	} else {
989 		HDERROR("Failed to retrieve screenshot for code '%s' "
990 			"at %" B_PRIi32 "x%" B_PRIi32 ".", info.Code().String(),
991 			scaledWidth, scaledHeight);
992 	}
993 }
994 
995 
996 // #pragma mark - listener notification methods
997 
998 
999 void
1000 Model::_NotifyAuthorizationChanged()
1001 {
1002 	for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) {
1003 		const ModelListenerRef& listener = fListeners.ItemAtFast(i);
1004 		if (listener.Get() != NULL)
1005 			listener->AuthorizationChanged();
1006 	}
1007 }
1008 
1009 
1010 void
1011 Model::_NotifyCategoryListChanged()
1012 {
1013 	for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) {
1014 		const ModelListenerRef& listener = fListeners.ItemAtFast(i);
1015 		if (listener.Get() != NULL)
1016 			listener->CategoryListChanged();
1017 	}
1018 }
1019 
1020 
1021 
1022 /*! This method will find the stored 'DepotInfo' that correlates to the
1023     supplied 'identifier' and will invoke the mapper function in order
1024     to get a replacement for the 'DepotInfo'.  The 'identifier' holds
1025     across mirrors.
1026 */
1027 
1028 void
1029 Model::ReplaceDepotByIdentifier(const BString& identifier,
1030 	DepotMapper* depotMapper, void* context)
1031 {
1032 	for (int32 i = 0; i < fDepots.CountItems(); i++) {
1033 		DepotInfo depotInfo = fDepots.ItemAtFast(i);
1034 
1035 		if (identifier == depotInfo.URL()) {
1036 			BAutolock locker(&fLock);
1037 			fDepots.Replace(i, depotMapper->MapDepot(depotInfo, context));
1038 		}
1039 	}
1040 }
1041 
1042 
1043 void
1044 Model::LogDepotsWithNoWebAppRepositoryCode() const
1045 {
1046 	int32 i;
1047 
1048 	for (i = 0; i < fDepots.CountItems(); i++) {
1049 		const DepotInfo& depot = fDepots.ItemAt(i);
1050 
1051 		if (depot.WebAppRepositoryCode().Length() == 0) {
1052 			if (depot.URL().Length() > 0) {
1053 				HDINFO("depot [%s] (%s) correlates with no repository in the"
1054 					" the haiku depot server system", depot.Name().String(),
1055 					depot.URL().String());
1056 			}
1057 			else {
1058 				HDINFO("depot [%s] correlates with no repository in the"
1059 					" the haiku depot server system", depot.Name().String());
1060 			}
1061 		}
1062 	}
1063 }
1064 
1065 
1066 void
1067 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
1068 	const char *sourceDescription) const
1069 {
1070 	BMessage error;
1071 	BString errorMessage;
1072 	double errorCode;
1073 
1074 	if (responsePayload.FindMessage("error", &error) == B_OK
1075 		&& error.FindString("message", &errorMessage) == B_OK
1076 		&& error.FindDouble("code", &errorCode) == B_OK) {
1077 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
1078 			errorMessage.String(), errorCode);
1079 	} else
1080 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
1081 }
1082 
1083 
1084 void
1085 Model::AddCategories(const CategoryList& categories)
1086 {
1087 	int32 i;
1088 	for (i = 0; i < categories.CountItems(); i++)
1089 		_AddCategory(categories.ItemAt(i));
1090 	_NotifyCategoryListChanged();
1091 }
1092 
1093 
1094 void
1095 Model::_AddCategory(const CategoryRef& category)
1096 {
1097 	int32 i;
1098 	for (i = 0; i < fCategories.CountItems(); i++) {
1099 		if (fCategories.ItemAt(i)->Code() == category->Code()) {
1100 			fCategories.Replace(i, category);
1101 			return;
1102 		}
1103 	}
1104 
1105 	fCategories.Add(category);
1106 }
1107