xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 344ded80d400028c8f561b4b876257b94c12db4a)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2024, Andrew Lindesay <apl@lindesay.co.nz>.
5  * All rights reserved. Distributed under the terms of the MIT License.
6  */
7 #include "Model.h"
8 
9 #include <algorithm>
10 #include <ctime>
11 #include <vector>
12 
13 #include <stdarg.h>
14 #include <time.h>
15 
16 #include <Autolock.h>
17 #include <Catalog.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 "LocaleUtils.h"
29 #include "Logger.h"
30 #include "PackageUtils.h"
31 #include "StorageUtils.h"
32 
33 
34 #undef B_TRANSLATION_CONTEXT
35 #define B_TRANSLATION_CONTEXT "Model"
36 
37 
38 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password."
39 	// this prefix is added before the nickname in the keystore
40 	// so that HDS username/password pairs can be identified.
41 
42 static const char* kHaikuDepotKeyring = "HaikuDepot";
43 
44 
45 ModelListener::~ModelListener()
46 {
47 }
48 
49 
50 // #pragma mark - Model
51 
52 
53 Model::Model()
54 	:
55 	fPreferredLanguage(LanguageRef(new Language(LANGUAGE_DEFAULT_ID, "English", true), true)),
56 	fDepots(),
57 	fCategories(),
58 	fPackageListViewMode(PROMINENT),
59 	fCanShareAnonymousUsageData(false)
60 {
61 
62 	// setup the language into a default state with a hard-coded language so
63 	// that there is a language at all.
64 	fLanguageRepository = new LanguageRepository();
65 	fLanguageRepository->AddLanguage(fPreferredLanguage);
66 
67 	fPackageFilterModel = new PackageFilterModel();
68 	fPackageScreenshotRepository = new PackageScreenshotRepository(
69 		PackageScreenshotRepositoryListenerRef(this),
70 		&fWebAppInterface);
71 }
72 
73 
74 Model::~Model()
75 {
76 	delete fPackageFilterModel;
77 	delete fPackageScreenshotRepository;
78 	delete fLanguageRepository;
79 }
80 
81 
82 LanguageRepository*
83 Model::Languages()
84 {
85 	return fLanguageRepository;
86 }
87 
88 
89 PackageFilterModel*
90 Model::PackageFilter()
91 {
92 	return fPackageFilterModel;
93 }
94 
95 
96 PackageIconRepository&
97 Model::GetPackageIconRepository()
98 {
99 	return fPackageIconRepository;
100 }
101 
102 
103 status_t
104 Model::InitPackageIconRepository()
105 {
106 	BPath tarPath;
107 	status_t result = StorageUtils::IconTarPath(tarPath);
108 	if (result == B_OK)
109 		result = fPackageIconRepository.Init(tarPath);
110 	return result;
111 }
112 
113 
114 PackageScreenshotRepository*
115 Model::GetPackageScreenshotRepository()
116 {
117 	return fPackageScreenshotRepository;
118 }
119 
120 
121 void
122 Model::AddListener(const ModelListenerRef& listener)
123 {
124 	fListeners.push_back(listener);
125 }
126 
127 
128 LanguageRef
129 Model::PreferredLanguage() const
130 {
131 	return fPreferredLanguage;
132 }
133 
134 
135 void
136 Model::SetPreferredLanguage(LanguageRef value)
137 {
138 	if (value.IsSet()) {
139 		if (fPreferredLanguage != value)
140 			fPreferredLanguage = value;
141 	}
142 }
143 
144 
145 // TODO; part of a wider change; cope with the package being in more than one
146 // depot
147 PackageInfoRef
148 Model::PackageForName(const BString& name)
149 {
150 	std::vector<DepotInfoRef>::iterator it;
151 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
152 		DepotInfoRef depotInfoRef = *it;
153 		PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name);
154 		if (packageInfoRef.Get() != NULL)
155 			return packageInfoRef;
156 	}
157 	return PackageInfoRef();
158 }
159 
160 
161 void
162 Model::MergeOrAddDepot(const DepotInfoRef& depot)
163 {
164 	BString depotName = depot->Name();
165 	for(uint32 i = 0; i < fDepots.size(); i++) {
166 		if (fDepots[i]->Name() == depotName) {
167 			DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true);
168 			ersatzDepot->SyncPackagesFromDepot(depot);
169 			fDepots[i] = ersatzDepot;
170 			return;
171 		}
172 	}
173 	fDepots.push_back(depot);
174 }
175 
176 
177 bool
178 Model::HasDepot(const BString& name) const
179 {
180 	return NULL != DepotForName(name).Get();
181 }
182 
183 
184 const DepotInfoRef
185 Model::DepotForName(const BString& name) const
186 {
187 	std::vector<DepotInfoRef>::const_iterator it;
188 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
189 		DepotInfoRef aDepot = *it;
190 		if (aDepot->Name() == name)
191 			return aDepot;
192 	}
193 	return DepotInfoRef();
194 }
195 
196 
197 int32
198 Model::CountDepots() const
199 {
200 	return fDepots.size();
201 }
202 
203 
204 DepotInfoRef
205 Model::DepotAtIndex(int32 index) const
206 {
207 	return fDepots[index];
208 }
209 
210 
211 bool
212 Model::HasAnyProminentPackages()
213 {
214 	std::vector<DepotInfoRef>::iterator it;
215 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
216 		DepotInfoRef aDepot = *it;
217 		if (aDepot->HasAnyProminentPackages())
218 			return true;
219 	}
220 	return false;
221 }
222 
223 
224 void
225 Model::Clear()
226 {
227 	GetPackageIconRepository().Clear();
228 	fDepots.clear();
229 	fPopulatedPackageNames.MakeEmpty();
230 }
231 
232 
233 void
234 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state)
235 {
236 	for (int32 i = 0; i < packageNames.CountStrings(); i++) {
237 		BString packageName = packageNames.StringAt(i);
238 		PackageInfoRef packageInfo = PackageForName(packageName);
239 
240 		if (packageInfo.IsSet()) {
241 			// TODO; make this immutable
242 
243 			PackageLocalInfoRef localInfo = PackageUtils::NewLocalInfo(packageInfo);
244 			localInfo->SetState(state);
245 			packageInfo->SetLocalInfo(localInfo);
246 
247 			HDINFO("did update package [%s] with state [%s]", packageName.String(),
248 				PackageUtils::StateToString(state));
249 		}
250 		else {
251 			HDINFO("was unable to find package [%s] so was not possible to set the state to [%s]",
252 				packageName.String(), PackageUtils::StateToString(state));
253 		}
254 	}
255 }
256 
257 
258 void
259 Model::SetPackageListViewMode(package_list_view_mode mode)
260 {
261 	fPackageListViewMode = mode;
262 }
263 
264 
265 void
266 Model::SetCanShareAnonymousUsageData(bool value)
267 {
268 	fCanShareAnonymousUsageData = value;
269 }
270 
271 
272 // #pragma mark - information retrieval
273 
274 /*!	It may transpire that the package has no corresponding record on the
275 	server side because the repository is not represented in the server.
276 	In such a case, there is little point in communicating with the server
277 	only to hear back that the package does not exist.
278 */
279 
280 bool
281 Model::CanPopulatePackage(const PackageInfoRef& package)
282 {
283 	const BString& depotName = package->DepotName();
284 
285 	if (depotName.IsEmpty())
286 		return false;
287 
288 	const DepotInfoRef& depot = DepotForName(depotName);
289 
290 	if (depot.Get() == NULL)
291 		return false;
292 
293 	return !depot->WebAppRepositoryCode().IsEmpty();
294 }
295 
296 
297 static void
298 model_remove_key_for_user(const BString& nickname)
299 {
300 	if (nickname.IsEmpty())
301 		return;
302 	BKeyStore keyStore;
303 	BPasswordKey key;
304 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
305 		<< nickname;
306 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
307 			passwordIdentifier, key);
308 
309 	switch (result) {
310 		case B_OK:
311 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
312 			if (result != B_OK) {
313 				HDERROR("error occurred when removing password for nickname "
314 					"[%s] : %s", nickname.String(), strerror(result));
315 			}
316 			break;
317 		case B_ENTRY_NOT_FOUND:
318 			return;
319 		default:
320 			HDERROR("error occurred when finding password for nickname "
321 				"[%s] : %s", nickname.String(), strerror(result));
322 			break;
323 	}
324 }
325 
326 
327 void
328 Model::SetNickname(BString nickname)
329 {
330 	BString password;
331 	BString existingNickname = Nickname();
332 
333 	// this happens when the user is logging out.  Best to remove the password
334 	// stored for the existing user since it is no longer required.
335 
336 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
337 		model_remove_key_for_user(existingNickname);
338 
339 	if (nickname.Length() > 0) {
340 		BPasswordKey key;
341 		BKeyStore keyStore;
342 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
343 			<< nickname;
344 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
345 				passwordIdentifier, key) == B_OK) {
346 			password = key.Password();
347 		}
348 		if (password.IsEmpty())
349 			nickname = "";
350 	}
351 
352 	SetCredentials(nickname, password, false);
353 }
354 
355 
356 const BString&
357 Model::Nickname()
358 {
359 	return fWebAppInterface.Nickname();
360 }
361 
362 
363 void
364 Model::SetCredentials(const BString& nickname, const BString& passwordClear,
365 	bool storePassword)
366 {
367 	BString existingNickname = Nickname();
368 
369 	if (storePassword) {
370 		// no point continuing to store the password for the previous user.
371 
372 		if (!existingNickname.IsEmpty())
373 			model_remove_key_for_user(existingNickname);
374 
375 		// adding a key that is already there does not seem to override the
376 		// existing key so the old key needs to be removed first.
377 
378 		if (!nickname.IsEmpty())
379 			model_remove_key_for_user(nickname);
380 
381 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
382 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
383 				<< nickname;
384 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
385 			BKeyStore keyStore;
386 			keyStore.AddKeyring(kHaikuDepotKeyring);
387 			keyStore.AddKey(kHaikuDepotKeyring, key);
388 		}
389 	}
390 
391 	BAutolock locker(&fLock);
392 	fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear));
393 
394 	if (nickname != existingNickname)
395 		_NotifyAuthorizationChanged();
396 }
397 
398 
399 // #pragma mark - listener notification methods
400 
401 
402 void
403 Model::_NotifyAuthorizationChanged()
404 {
405 	std::vector<ModelListenerRef>::const_iterator it;
406 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
407 		const ModelListenerRef& listener = *it;
408 		if (listener.IsSet())
409 			listener->AuthorizationChanged();
410 	}
411 }
412 
413 
414 void
415 Model::_NotifyCategoryListChanged()
416 {
417 	std::vector<ModelListenerRef>::const_iterator it;
418 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
419 		const ModelListenerRef& listener = *it;
420 		if (listener.IsSet())
421 			listener->CategoryListChanged();
422 	}
423 }
424 
425 
426 void
427 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
428 	const char *sourceDescription) const
429 {
430 	BMessage error;
431 	BString errorMessage;
432 	double errorCode;
433 
434 	if (responsePayload.FindMessage("error", &error) == B_OK
435 		&& error.FindString("message", &errorMessage) == B_OK
436 		&& error.FindDouble("code", &errorCode) == B_OK) {
437 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
438 			errorMessage.String(), errorCode);
439 	} else
440 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
441 }
442 
443 
444 // #pragma mark - Rating Stabilities
445 
446 
447 int32
448 Model::CountRatingStabilities() const
449 {
450 	return fRatingStabilities.size();
451 }
452 
453 
454 RatingStabilityRef
455 Model::RatingStabilityByCode(BString& code) const
456 {
457 	std::vector<RatingStabilityRef>::const_iterator it;
458 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
459 			it++) {
460 		RatingStabilityRef aRatingStability = *it;
461 		if (aRatingStability->Code() == code)
462 			return aRatingStability;
463 	}
464 	return RatingStabilityRef();
465 }
466 
467 
468 RatingStabilityRef
469 Model::RatingStabilityAtIndex(int32 index) const
470 {
471 	return fRatingStabilities[index];
472 }
473 
474 
475 void
476 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
477 {
478 	std::vector<RatingStabilityRef>::const_iterator it;
479 	for (it = values.begin(); it != values.end(); it++)
480 		_AddRatingStability(*it);
481 }
482 
483 
484 void
485 Model::_AddRatingStability(const RatingStabilityRef& value)
486 {
487 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
488 		= std::lower_bound(
489 			fRatingStabilities.begin(),
490 			fRatingStabilities.end(),
491 			value,
492 			&IsRatingStabilityBefore);
493 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
494 		fRatingStabilities.begin()
495 			+ (itInsertionPtConst - fRatingStabilities.begin());
496 
497 	if (itInsertionPt != fRatingStabilities.end()
498 		&& (*itInsertionPt)->Code() == value->Code()) {
499 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
500 			// replace the one with the same code.
501 	}
502 
503 	fRatingStabilities.insert(itInsertionPt, value);
504 }
505 
506 
507 // #pragma mark - Categories
508 
509 
510 int32
511 Model::CountCategories() const
512 {
513 	return fCategories.size();
514 }
515 
516 
517 CategoryRef
518 Model::CategoryByCode(BString& code) const
519 {
520 	std::vector<CategoryRef>::const_iterator it;
521 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
522 		CategoryRef aCategory = *it;
523 		if (aCategory->Code() == code)
524 			return aCategory;
525 	}
526 	return CategoryRef();
527 }
528 
529 
530 CategoryRef
531 Model::CategoryAtIndex(int32 index) const
532 {
533 	return fCategories[index];
534 }
535 
536 
537 void
538 Model::AddCategories(std::vector<CategoryRef>& values)
539 {
540 	std::vector<CategoryRef>::iterator it;
541 	for (it = values.begin(); it != values.end(); it++)
542 		_AddCategory(*it);
543 	_NotifyCategoryListChanged();
544 }
545 
546 /*! This will insert the category in order.
547  */
548 
549 void
550 Model::_AddCategory(const CategoryRef& category)
551 {
552 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
553 		= std::lower_bound(
554 			fCategories.begin(),
555 			fCategories.end(),
556 			category,
557 			&IsPackageCategoryBefore);
558 	std::vector<CategoryRef>::iterator itInsertionPt =
559 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
560 
561 	if (itInsertionPt != fCategories.end()
562 		&& (*itInsertionPt)->Code() == category->Code()) {
563 		itInsertionPt = fCategories.erase(itInsertionPt);
564 			// replace the one with the same code.
565 	}
566 
567 	fCategories.insert(itInsertionPt, category);
568 }
569 
570 
571 void
572 Model::ScreenshotCached(const ScreenshotCoordinate& coord)
573 {
574 	std::vector<ModelListenerRef>::const_iterator it;
575 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
576 		const ModelListenerRef& listener = *it;
577 		if (listener.IsSet())
578 			listener->ScreenshotCached(coord);
579 	}
580 }
581