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