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