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