xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision caed67a8cba83913b9c21ac2b06ebc6bd1cb3111)
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 	fDepots(),
55 	fCategories(),
56 	fPackageListViewMode(PROMINENT),
57 	fCanShareAnonymousUsageData(false)
58 {
59 	fPackageFilterModel = new PackageFilterModel();
60 	fPackageScreenshotRepository = new PackageScreenshotRepository(
61 		PackageScreenshotRepositoryListenerRef(this),
62 		&fWebAppInterface);
63 }
64 
65 
66 Model::~Model()
67 {
68 	delete fPackageFilterModel;
69 	delete fPackageScreenshotRepository;
70 }
71 
72 
73 LanguageModel*
74 Model::Language()
75 {
76 	return &fLanguageModel;
77 }
78 
79 
80 PackageFilterModel*
81 Model::PackageFilter()
82 {
83 	return fPackageFilterModel;
84 }
85 
86 
87 PackageIconRepository&
88 Model::GetPackageIconRepository()
89 {
90 	return fPackageIconRepository;
91 }
92 
93 
94 status_t
95 Model::InitPackageIconRepository()
96 {
97 	BPath tarPath;
98 	status_t result = IconTarPath(tarPath);
99 	if (result == B_OK)
100 		result = fPackageIconRepository.Init(tarPath);
101 	return result;
102 }
103 
104 
105 PackageScreenshotRepository*
106 Model::GetPackageScreenshotRepository()
107 {
108 	return fPackageScreenshotRepository;
109 }
110 
111 
112 void
113 Model::AddListener(const ModelListenerRef& listener)
114 {
115 	fListeners.push_back(listener);
116 }
117 
118 
119 // TODO; part of a wider change; cope with the package being in more than one
120 // depot
121 PackageInfoRef
122 Model::PackageForName(const BString& name)
123 {
124 	std::vector<DepotInfoRef>::iterator it;
125 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
126 		DepotInfoRef depotInfoRef = *it;
127 		PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name);
128 		if (packageInfoRef.Get() != NULL)
129 			return packageInfoRef;
130 	}
131 	return PackageInfoRef();
132 }
133 
134 
135 void
136 Model::MergeOrAddDepot(const DepotInfoRef& depot)
137 {
138 	BString depotName = depot->Name();
139 	for(uint32 i = 0; i < fDepots.size(); i++) {
140 		if (fDepots[i]->Name() == depotName) {
141 			DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true);
142 			ersatzDepot->SyncPackagesFromDepot(depot);
143 			fDepots[i] = ersatzDepot;
144 			return;
145 		}
146 	}
147 	fDepots.push_back(depot);
148 }
149 
150 
151 bool
152 Model::HasDepot(const BString& name) const
153 {
154 	return NULL != DepotForName(name).Get();
155 }
156 
157 
158 const DepotInfoRef
159 Model::DepotForName(const BString& name) const
160 {
161 	std::vector<DepotInfoRef>::const_iterator it;
162 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
163 		DepotInfoRef aDepot = *it;
164 		if (aDepot->Name() == name)
165 			return aDepot;
166 	}
167 	return DepotInfoRef();
168 }
169 
170 
171 int32
172 Model::CountDepots() const
173 {
174 	return fDepots.size();
175 }
176 
177 
178 DepotInfoRef
179 Model::DepotAtIndex(int32 index) const
180 {
181 	return fDepots[index];
182 }
183 
184 
185 bool
186 Model::HasAnyProminentPackages()
187 {
188 	std::vector<DepotInfoRef>::iterator it;
189 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
190 		DepotInfoRef aDepot = *it;
191 		if (aDepot->HasAnyProminentPackages())
192 			return true;
193 	}
194 	return false;
195 }
196 
197 
198 void
199 Model::Clear()
200 {
201 	GetPackageIconRepository().Clear();
202 	fDepots.clear();
203 	fPopulatedPackageNames.MakeEmpty();
204 }
205 
206 
207 void
208 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state)
209 {
210 	for (int32 i = 0; i < packageNames.CountStrings(); i++) {
211 		BString packageName = packageNames.StringAt(i);
212 		PackageInfoRef packageInfo = PackageForName(packageName);
213 
214 		if (packageInfo.IsSet()) {
215 			packageInfo->SetState(state);
216 			HDINFO("did update package [%s] with state [%s]",
217 				packageName.String(), package_state_to_string(state));
218 		}
219 		else {
220 			HDINFO("was unable to find package [%s] so was not possible to set"
221 				" the state to [%s]", packageName.String(),
222 				package_state_to_string(state));
223 		}
224 	}
225 }
226 
227 
228 void
229 Model::SetPackageListViewMode(package_list_view_mode mode)
230 {
231 	fPackageListViewMode = mode;
232 }
233 
234 
235 void
236 Model::SetCanShareAnonymousUsageData(bool value)
237 {
238 	fCanShareAnonymousUsageData = value;
239 }
240 
241 
242 // #pragma mark - information retrieval
243 
244 /*!	It may transpire that the package has no corresponding record on the
245 	server side because the repository is not represented in the server.
246 	In such a case, there is little point in communicating with the server
247 	only to hear back that the package does not exist.
248 */
249 
250 bool
251 Model::CanPopulatePackage(const PackageInfoRef& package)
252 {
253 	const BString& depotName = package->DepotName();
254 
255 	if (depotName.IsEmpty())
256 		return false;
257 
258 	const DepotInfoRef& depot = DepotForName(depotName);
259 
260 	if (depot.Get() == NULL)
261 		return false;
262 
263 	return !depot->WebAppRepositoryCode().IsEmpty();
264 }
265 
266 
267 /*! Initially only superficial data is loaded from the server into the data
268     model of the packages.  When the package is viewed, additional data needs
269     to be populated including ratings.  This method takes care of that.
270 */
271 
272 void
273 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
274 {
275 	HDTRACE("will populate package for [%s]", package->Name().String());
276 
277 	if (!CanPopulatePackage(package)) {
278 		HDINFO("unable to populate package [%s]", package->Name().String());
279 		return;
280 	}
281 
282 	// TODO: There should probably also be a way to "unpopulate" the
283 	// package information. Maybe a cache of populated packages, so that
284 	// packages loose their extra information after a certain amount of
285 	// time when they have not been accessed/displayed in the UI. Otherwise
286 	// HaikuDepot will consume more and more resources in the packages.
287 	{
288 		BAutolock locker(&fLock);
289 		bool alreadyPopulated = fPopulatedPackageNames.HasString(
290 			package->Name());
291 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
292 			return;
293 		if (!alreadyPopulated)
294 			fPopulatedPackageNames.Add(package->Name());
295 	}
296 
297 	if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) {
298 		_PopulatePackageChangelog(package);
299 	}
300 
301 	if ((flags & POPULATE_USER_RATINGS) != 0) {
302 		// Retrieve info from web-app
303 		BMessage info;
304 
305 		BString packageName;
306 		BString webAppRepositoryCode;
307 		BString webAppRepositorySourceCode;
308 
309 		{
310 			BAutolock locker(&fLock);
311 			packageName = package->Name();
312 			const DepotInfo* depot = DepotForName(package->DepotName());
313 
314 			if (depot != NULL) {
315 				webAppRepositoryCode = depot->WebAppRepositoryCode();
316 				webAppRepositorySourceCode
317 					= depot->WebAppRepositorySourceCode();
318 			}
319 		}
320 
321 		status_t status = fWebAppInterface
322 			.RetrieveUserRatingsForPackageForDisplay(packageName,
323 				webAppRepositoryCode, webAppRepositorySourceCode, 0,
324 				PACKAGE_INFO_MAX_USER_RATINGS, info);
325 		if (status == B_OK) {
326 			// Parse message
327 			BMessage result;
328 			BMessage items;
329 			if (info.FindMessage("result", &result) == B_OK
330 				&& result.FindMessage("items", &items) == B_OK) {
331 
332 				BAutolock locker(&fLock);
333 				package->ClearUserRatings();
334 
335 				int32 index = 0;
336 				while (true) {
337 					BString name;
338 					name << index++;
339 
340 					BMessage item;
341 					if (items.FindMessage(name, &item) != B_OK)
342 						break;
343 
344 					BString code;
345 					if (item.FindString("code", &code) != B_OK) {
346 						HDERROR("corrupt user rating at index %" B_PRIi32,
347 							index);
348 						continue;
349 					}
350 
351 					BString user;
352 					BMessage userInfo;
353 					if (item.FindMessage("user", &userInfo) != B_OK
354 							|| userInfo.FindString("nickname", &user) != B_OK) {
355 						HDERROR("ignored user rating [%s] without a user "
356 							"nickname", code.String());
357 						continue;
358 					}
359 
360 					// Extract basic info, all items are optional
361 					BString languageCode;
362 					BString comment;
363 					double rating;
364 					item.FindString("naturalLanguageCode", &languageCode);
365 					item.FindString("comment", &comment);
366 					if (item.FindDouble("rating", &rating) != B_OK)
367 						rating = -1;
368 					if (comment.Length() == 0 && rating == -1) {
369 						HDERROR("rating [%s] has no comment or rating so will"
370 							" be ignored", code.String());
371 						continue;
372 					}
373 
374 					// For which version of the package was the rating?
375 					BString major = "?";
376 					BString minor = "?";
377 					BString micro = "";
378 					double revision = -1;
379 					BString architectureCode = "";
380 					BMessage version;
381 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
382 						version.FindString("major", &major);
383 						version.FindString("minor", &minor);
384 						version.FindString("micro", &micro);
385 						version.FindDouble("revision", &revision);
386 						version.FindString("architectureCode",
387 							&architectureCode);
388 					}
389 					BString versionString = major;
390 					versionString << ".";
391 					versionString << minor;
392 					if (!micro.IsEmpty()) {
393 						versionString << ".";
394 						versionString << micro;
395 					}
396 					if (revision > 0) {
397 						versionString << "-";
398 						versionString << (int) revision;
399 					}
400 
401 					if (!architectureCode.IsEmpty()) {
402 						versionString << " " << STR_MDASH << " ";
403 						versionString << architectureCode;
404 					}
405 
406 					double createTimestamp;
407 					item.FindDouble("createTimestamp", &createTimestamp);
408 
409 					// Add the rating to the PackageInfo
410 					UserRatingRef userRating(new UserRating(
411 						UserInfo(user), rating,
412 						comment,
413 						languageCode,
414 							// note that language identifiers are "code" in HDS and "id" in Haiku
415 						versionString,
416 						(uint64) createTimestamp), true);
417 					package->AddUserRating(userRating);
418 					HDDEBUG("rating [%s] retrieved from server", code.String());
419 				}
420 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
421 						index - 1, packageName.String());
422 			} else {
423 				BString message;
424 				message.SetToFormat("failure to retrieve user ratings for [%s]",
425 					packageName.String());
426 				_MaybeLogJsonRpcError(info, message.String());
427 			}
428 		} else
429 			HDERROR("unable to retrieve user ratings");
430 	}
431 }
432 
433 
434 void
435 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
436 {
437 	BMessage info;
438 	BString packageName;
439 
440 	{
441 		BAutolock locker(&fLock);
442 		packageName = package->Name();
443 	}
444 
445 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
446 
447 	if (status == B_OK) {
448 		// Parse message
449 		BMessage result;
450 		BString content;
451 		if (info.FindMessage("result", &result) == B_OK) {
452 			if (result.FindString("content", &content) == B_OK
453 				&& 0 != content.Length()) {
454 				BAutolock locker(&fLock);
455 				package->SetChangelog(content);
456 				HDDEBUG("changelog populated for [%s]", packageName.String());
457 			} else
458 				HDDEBUG("no changelog present for [%s]", packageName.String());
459 		} else
460 			_MaybeLogJsonRpcError(info, "populate package changelog");
461 	} else {
462 		HDERROR("unable to obtain the changelog for the package [%s]",
463 			packageName.String());
464 	}
465 }
466 
467 
468 static void
469 model_remove_key_for_user(const BString& nickname)
470 {
471 	if (nickname.IsEmpty())
472 		return;
473 	BKeyStore keyStore;
474 	BPasswordKey key;
475 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
476 		<< nickname;
477 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
478 			passwordIdentifier, key);
479 
480 	switch (result) {
481 		case B_OK:
482 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
483 			if (result != B_OK) {
484 				HDERROR("error occurred when removing password for nickname "
485 					"[%s] : %s", nickname.String(), strerror(result));
486 			}
487 			break;
488 		case B_ENTRY_NOT_FOUND:
489 			return;
490 		default:
491 			HDERROR("error occurred when finding password for nickname "
492 				"[%s] : %s", nickname.String(), strerror(result));
493 			break;
494 	}
495 }
496 
497 
498 void
499 Model::SetNickname(BString nickname)
500 {
501 	BString password;
502 	BString existingNickname = Nickname();
503 
504 	// this happens when the user is logging out.  Best to remove the password
505 	// stored for the existing user since it is no longer required.
506 
507 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
508 		model_remove_key_for_user(existingNickname);
509 
510 	if (nickname.Length() > 0) {
511 		BPasswordKey key;
512 		BKeyStore keyStore;
513 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
514 			<< nickname;
515 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
516 				passwordIdentifier, key) == B_OK) {
517 			password = key.Password();
518 		}
519 		if (password.IsEmpty())
520 			nickname = "";
521 	}
522 
523 	SetCredentials(nickname, password, false);
524 }
525 
526 
527 const BString&
528 Model::Nickname()
529 {
530 	return fWebAppInterface.Nickname();
531 }
532 
533 
534 void
535 Model::SetCredentials(const BString& nickname, const BString& passwordClear,
536 	bool storePassword)
537 {
538 	BString existingNickname = Nickname();
539 
540 	if (storePassword) {
541 		// no point continuing to store the password for the previous user.
542 
543 		if (!existingNickname.IsEmpty())
544 			model_remove_key_for_user(existingNickname);
545 
546 		// adding a key that is already there does not seem to override the
547 		// existing key so the old key needs to be removed first.
548 
549 		if (!nickname.IsEmpty())
550 			model_remove_key_for_user(nickname);
551 
552 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
553 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
554 				<< nickname;
555 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
556 			BKeyStore keyStore;
557 			keyStore.AddKeyring(kHaikuDepotKeyring);
558 			keyStore.AddKey(kHaikuDepotKeyring, key);
559 		}
560 	}
561 
562 	BAutolock locker(&fLock);
563 	fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear));
564 
565 	if (nickname != existingNickname)
566 		_NotifyAuthorizationChanged();
567 }
568 
569 
570 /*! When bulk repository data comes down from the server, it will
571     arrive as a json.gz payload.  This is stored locally as a cache
572     and this method will provide the on-disk storage location for
573     this file.
574 */
575 
576 status_t
577 Model::DumpExportRepositoryDataPath(BPath& path)
578 {
579 	BString leaf;
580 	leaf.SetToFormat("repository-all_%s.json.gz",
581 		Language()->PreferredLanguage()->ID());
582 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
583 }
584 
585 
586 /*! When the system downloads reference data (eg; categories) from the server
587     then the downloaded data is stored and cached at the path defined by this
588     method.
589 */
590 
591 status_t
592 Model::DumpExportReferenceDataPath(BPath& path)
593 {
594 	BString leaf;
595 	leaf.SetToFormat("reference-all_%s.json.gz",
596 		Language()->PreferredLanguage()->ID());
597 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
598 }
599 
600 
601 status_t
602 Model::IconTarPath(BPath& path) const
603 {
604 	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
605 }
606 
607 
608 status_t
609 Model::DumpExportPkgDataPath(BPath& path,
610 	const BString& repositorySourceCode)
611 {
612 	BString leaf;
613 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
614 		Language()->PreferredLanguage()->ID());
615 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
616 }
617 
618 
619 // #pragma mark - listener notification methods
620 
621 
622 void
623 Model::_NotifyAuthorizationChanged()
624 {
625 	std::vector<ModelListenerRef>::const_iterator it;
626 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
627 		const ModelListenerRef& listener = *it;
628 		if (listener.IsSet())
629 			listener->AuthorizationChanged();
630 	}
631 }
632 
633 
634 void
635 Model::_NotifyCategoryListChanged()
636 {
637 	std::vector<ModelListenerRef>::const_iterator it;
638 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
639 		const ModelListenerRef& listener = *it;
640 		if (listener.IsSet())
641 			listener->CategoryListChanged();
642 	}
643 }
644 
645 
646 void
647 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
648 	const char *sourceDescription) const
649 {
650 	BMessage error;
651 	BString errorMessage;
652 	double errorCode;
653 
654 	if (responsePayload.FindMessage("error", &error) == B_OK
655 		&& error.FindString("message", &errorMessage) == B_OK
656 		&& error.FindDouble("code", &errorCode) == B_OK) {
657 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
658 			errorMessage.String(), errorCode);
659 	} else
660 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
661 }
662 
663 
664 // #pragma mark - Rating Stabilities
665 
666 
667 int32
668 Model::CountRatingStabilities() const
669 {
670 	return fRatingStabilities.size();
671 }
672 
673 
674 RatingStabilityRef
675 Model::RatingStabilityByCode(BString& code) const
676 {
677 	std::vector<RatingStabilityRef>::const_iterator it;
678 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
679 			it++) {
680 		RatingStabilityRef aRatingStability = *it;
681 		if (aRatingStability->Code() == code)
682 			return aRatingStability;
683 	}
684 	return RatingStabilityRef();
685 }
686 
687 
688 RatingStabilityRef
689 Model::RatingStabilityAtIndex(int32 index) const
690 {
691 	return fRatingStabilities[index];
692 }
693 
694 
695 void
696 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
697 {
698 	std::vector<RatingStabilityRef>::const_iterator it;
699 	for (it = values.begin(); it != values.end(); it++)
700 		_AddRatingStability(*it);
701 }
702 
703 
704 void
705 Model::_AddRatingStability(const RatingStabilityRef& value)
706 {
707 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
708 		= std::lower_bound(
709 			fRatingStabilities.begin(),
710 			fRatingStabilities.end(),
711 			value,
712 			&IsRatingStabilityBefore);
713 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
714 		fRatingStabilities.begin()
715 			+ (itInsertionPtConst - fRatingStabilities.begin());
716 
717 	if (itInsertionPt != fRatingStabilities.end()
718 		&& (*itInsertionPt)->Code() == value->Code()) {
719 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
720 			// replace the one with the same code.
721 	}
722 
723 	fRatingStabilities.insert(itInsertionPt, value);
724 }
725 
726 
727 // #pragma mark - Categories
728 
729 
730 int32
731 Model::CountCategories() const
732 {
733 	return fCategories.size();
734 }
735 
736 
737 CategoryRef
738 Model::CategoryByCode(BString& code) const
739 {
740 	std::vector<CategoryRef>::const_iterator it;
741 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
742 		CategoryRef aCategory = *it;
743 		if (aCategory->Code() == code)
744 			return aCategory;
745 	}
746 	return CategoryRef();
747 }
748 
749 
750 CategoryRef
751 Model::CategoryAtIndex(int32 index) const
752 {
753 	return fCategories[index];
754 }
755 
756 
757 void
758 Model::AddCategories(std::vector<CategoryRef>& values)
759 {
760 	std::vector<CategoryRef>::iterator it;
761 	for (it = values.begin(); it != values.end(); it++)
762 		_AddCategory(*it);
763 	_NotifyCategoryListChanged();
764 }
765 
766 /*! This will insert the category in order.
767  */
768 
769 void
770 Model::_AddCategory(const CategoryRef& category)
771 {
772 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
773 		= std::lower_bound(
774 			fCategories.begin(),
775 			fCategories.end(),
776 			category,
777 			&IsPackageCategoryBefore);
778 	std::vector<CategoryRef>::iterator itInsertionPt =
779 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
780 
781 	if (itInsertionPt != fCategories.end()
782 		&& (*itInsertionPt)->Code() == category->Code()) {
783 		itInsertionPt = fCategories.erase(itInsertionPt);
784 			// replace the one with the same code.
785 	}
786 
787 	fCategories.insert(itInsertionPt, category);
788 }
789 
790 
791 void
792 Model::ScreenshotCached(const ScreenshotCoordinate& coord)
793 {
794 	std::vector<ModelListenerRef>::const_iterator it;
795 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
796 		const ModelListenerRef& listener = *it;
797 		if (listener.IsSet())
798 			listener->ScreenshotCached(coord);
799 	}
800 }
801