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