%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/nextcloud.varak.net/apps_old/apps/support/lib/Service/
Upload File :
Create Path :
Current File : //www/varak.net/nextcloud.varak.net/apps_old/apps/support/lib/Service/SubscriptionService.php

<?php

declare(strict_types=1);
/**
 * @copyright Copyright (c) 2018 Morris Jobke <hey@morrisjobke.de>
 *
 * @author Morris Jobke <hey@morrisjobke.de>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace OCA\Support\Service;

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use OC\User\Backend;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Notification\IManager;
use Psr\Log\LoggerInterface;

class SubscriptionService {
	public const ERROR_FAILED_RETRY = 1;
	public const ERROR_FAILED_INVALID = 2;
	public const ERROR_NO_INTERNET_CONNECTION = 3;
	public const ERROR_INVALID_SUBSCRIPTION_KEY = 4;

	public const THRESHOLD_MEDIUM = 500;
	public const THRESHOLD_LARGE = 1000;

	private int $userCount = -1;
	private int $activeUserCount = -1;

	private ?array $subscriptionInfoCache = null;

	public function __construct(
		protected readonly IConfig $config,
		protected readonly IClientService $clientService,
		protected readonly LoggerInterface $log,
		protected readonly IUserManager $userManager,
		protected readonly IManager $notifications,
		protected readonly IURLGenerator $urlGenerator,
		protected readonly IGroupManager $groupManager,
		protected readonly IMailer $mailer,
		protected readonly IFactory $l10nFactory,
		protected readonly ICacheFactory $cacheFactory,
		protected readonly IAppConfig $appConfig
	) {
	}

	public function setSubscriptionKey(string $subscriptionKey): void {
		if (!preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)) {
			$this->config->setAppValue('support', 'last_error', self::ERROR_INVALID_SUBSCRIPTION_KEY);
			return;
		}

		$this->config->setAppValue('support', 'potential_subscription_key', $subscriptionKey);
		$this->config->deleteAppValue('support', 'last_error');

		$this->renewSubscriptionInfo(true);
	}

	public function getUserCount(): int {
		if ($this->userCount > 0) {
			return $this->userCount;
		}

		$userCount = 0;
		$backends = $this->userManager->getBackends();
		foreach ($backends as $backend) {
			if ($backend->implementsActions(Backend::COUNT_USERS)) {
				try {
					$backendUsers = $backend->countUsers();
				} catch (\Exception $e) {
					$backendUsers = false;

					$this->log->error($e->getMessage(), ['exception' => $e]);
				}
				if ($backendUsers !== false) {
					$userCount += $backendUsers;
				} else {
					// TODO what if the user count can't be determined?
					$this->log->warning('Can not determine user count for ' . get_class($backend), ['app' => 'support']);
				}
			}
		}

		$disabledUsers = $this->config->getUsersForUserValue('core', 'enabled', 'false');
		$disabledUsersCount = count($disabledUsers);
		$this->userCount = $userCount - $disabledUsersCount;

		if ($this->userCount < 0) {
			$this->userCount = 0;

			// TODO this should never happen
			$this->log->warning("Total user count was negative (users: $userCount, disabled: $disabledUsersCount)", ['app' => 'support']);
		}

		return $this->userCount;
	}

	public function getActiveUserCount(): int {
		if ($this->activeUserCount > 0) {
			return $this->activeUserCount;
		}

		$this->activeUserCount = $this->userManager->countSeenUsers();

		return $this->activeUserCount;
	}

	public function renewSubscriptionInfo(bool $fast): void {
		$hasInternetConnection = $this->config->getSystemValue('has_internet_connection', true);

		if (!$hasInternetConnection) {
			$this->config->setAppValue('support', 'last_error', self::ERROR_NO_INTERNET_CONNECTION);
			return;
		}

		$subscriptionKey = $this->config->getAppValue('support', 'potential_subscription_key', '');

		if (!preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)) {
			// fallback to normal subscription key
			$subscriptionKey = $this->config->getAppValue('support', 'subscription_key', '');
			if (!preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)) {
				return;
			}
		}

		$backendURL = $this->config->getSystemValue('support.backend', 'https://cloud.nextcloud.com/');
		$backendURL = rtrim($backendURL, '/') . '/apps/zammad_organisation_management/api/query/subscription/' . $subscriptionKey;
		try {
			$userCount = $this->getUserCount();
			$activeUserCount = $this->userManager->countSeenUsers();

			$httpClient = $this->clientService->newClient();
			$response = $httpClient->post(
				$backendURL,
				[
					'body' => [
						'instanceId' => $this->config->getSystemValue('instanceid', ''),
						'userCount' => $userCount,
						'activeUserCount' => $activeUserCount,
						'apps' => $this->getAppsDetails(),
						'version' => implode('.', \OCP\Util::getVersion()),
					],
					'timeout' => $fast ? 10 : 30,
					'connect_timeout' => $fast ? 3 : 30,
				]
			);

			$body = json_decode($response->getBody(), true);

			if ($response->getStatusCode() === 200 && is_array($body)) {
				$this->log->info('Subscription info successfully fetched');
				$this->config->setAppValue('support', 'subscription_key', $subscriptionKey);
				$this->config->setAppValue('support', 'last_check', time());
				$this->config->setAppValue('support', 'last_response', json_encode($body));
				$this->config->deleteAppValue('support', 'last_error');

				$currentUpdaterServer = $this->config->getSystemValue('updater.server.url', 'https://updates.nextcloud.com/updater_server/');
				$newUpdaterServer = 'https://updates.nextcloud.com/customers/' . $subscriptionKey . '/';

				/**
				 * only overwrite the updater server if:
				 * 	- it is the default one or another /.customers/ one
				 *  - there is a valid subscription
				 *  - there is a subscription key set
				 *  - the subscription key is halfway sane
				 */
				if (
					(
						$currentUpdaterServer === 'https://updates.nextcloud.com/updater_server/' ||
						substr($currentUpdaterServer, 0, 40) === 'https://updates.nextcloud.com/customers/'
					) &&
					$subscriptionKey !== '' &&
					preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)
				) {
					$this->config->setSystemValue('updater.server.url', $newUpdaterServer);
				}

				// remove all pending notifications
				$notification = $this->notifications->createNotification();
				$notification->setApp('support')
					->setSubject('subscription_info');
				$this->notifications->markProcessed($notification);

				// hide push fair use warning
				$cacheNotifications = $this->cacheFactory->createDistributed('notifications');
				$cacheNotifications->remove('push_fair_use');

				return;
			}

			$this->log->info('Renewal of subscription info returned invalid data. URL: ' . $backendURL . ' Status: ' . $response->getStatusCode() . ' Body: ' . $response->getBody());
			$error = self::ERROR_FAILED_RETRY;
		} catch (ConnectException $e) {
			$this->log->info('Renew of subscription info failed due to connect exception - retrying later. URL: ' . $backendURL, ['app' => 'support', 'exception' => $e]);
			$error = self::ERROR_FAILED_RETRY;
		} catch (RequestException $e) {
			$response = $e->getResponse();

			if ($response !== null && $response->getStatusCode() === 403) {
				$this->log->info('Subscription key invalid');
				$this->config->deleteAppValue('support', 'potential_subscription_key');
				$error = self::ERROR_FAILED_INVALID;
			} else {
				$this->log->info('Renew of subscription info failed. URL: ' . $backendURL, ['app' => 'support', 'exception' => $e]);
				$error = self::ERROR_FAILED_RETRY;
			}
		} catch (\Exception $e) {
			$this->log->info('Renew of subscription info failed. URL: ' . $backendURL, ['app' => 'support', 'exception' => $e]);
			$error = self::ERROR_FAILED_RETRY;
		}

		$this->config->setAppValue('support', 'last_error', $error);
	}

	public function getSubscriptionInfo(): array {
		if ($this->subscriptionInfoCache !== null) {
			return $this->subscriptionInfoCache;
		}

		$userCount = $this->getUserCount();
		$activeUserCount = $this->getActiveUserCount();

		$instanceSize = 'small';

		if ($userCount > SubscriptionService::THRESHOLD_MEDIUM) {
			if ($userCount > SubscriptionService::THRESHOLD_LARGE) {
				$instanceSize = 'large';
			} else {
				$instanceSize = 'medium';
			}
		}

		$subscriptionInfo = $this->getMinimalSubscriptionInfo();

		$now = new \DateTime();
		$subscriptionEndDate = new \DateTime($subscriptionInfo['endDate'] ?? 'now');
		if ($now > $subscriptionEndDate) {
			$years = 0;
			$months = 0;
			$days = 0;
		} else {
			$diff = $now->diff($subscriptionEndDate);
			$years = (int)$diff->format('%y');
			$months = $years * 12 + (int)$diff->format('%m');
			$days = $months * 30 + (int)$diff->format('%d');
		}

		$hasSubscription = $subscriptionInfo !== null;
		$isInvalidSubscription = ($years + $months + $days) <= 0;
		$allowedUsersCount = $subscriptionInfo['amountOfUsers'] ?? 0;
		$onlyCountActiveUsers = $subscriptionInfo['onlyCountActiveUsers'] ?? false;
		if ($allowedUsersCount === -1) {
			$isOverLimit = false;
		} elseif ($onlyCountActiveUsers) {
			$isOverLimit = $allowedUsersCount < $activeUserCount;
		} else {
			$isOverLimit = $allowedUsersCount < $userCount;
		}

		$this->subscriptionInfoCache = [
			$instanceSize,
			$hasSubscription,
			$isInvalidSubscription,
			$isOverLimit,
			$subscriptionInfo
		];

		return $this->subscriptionInfoCache;
	}

	public function getMinimalSubscriptionInfo(): ?array {
		$lastResponse = $this->config->getAppValue('support', 'last_response', '');
		return json_decode($lastResponse, true);
	}

	public function checkSubscription() {
		$hasInternetConnection = $this->config->getSystemValue('has_internet_connection', true);

		if (!$hasInternetConnection) {
			return;
		}

		[
			$instanceSize,
			$hasSubscription,
			$isInvalidSubscription,
			$isOverLimit,
			$subscriptionInfo
		] = $this->getSubscriptionInfo();

		if ($hasSubscription && $isInvalidSubscription) {
			$this->handleExpired(
				$subscriptionInfo['accountManagerInfo']['name'] ?? '',
				$subscriptionInfo['accountManagerInfo']['email'] ?? '',
				$subscriptionInfo['accountManagerInfo']['phone'] ?? '');
		} elseif ($hasSubscription && $isOverLimit) {
			$this->handleOverLimit(
				$subscriptionInfo['accountManagerInfo']['name'] ?? '',
				$subscriptionInfo['accountManagerInfo']['email'] ?? '',
				$subscriptionInfo['accountManagerInfo']['phone'] ?? '');
		} elseif (!$hasSubscription && $instanceSize === 'large') {
			$this->handleNoSubscription($instanceSize);
		}
	}

	private function handleNoSubscription(string $instanceSize): void {
		$currentTime = time();
		$installTime = (int)$this->config->getAppValue('core', 'installedat', $currentTime);

		// skip if installed within the last 30 days
		if (($installTime + 30 * 24 * 3600) > $currentTime) {
			return;
		}

		$lastNotificationTime = (int)$this->config->getAppValue('support', 'last_notification', 0);

		// skip if last notification was within the last 30 days
		if (($lastNotificationTime + 30 * 24 * 3600) > $currentTime) {
			return;
		}

		$updateLastNotificationTime = false;

		$adminGroup = $this->groupManager->get('admin');
		$adminUsers = $adminGroup->getUsers();

		foreach ($adminUsers as $adminUser) {
			$notification = $this->notifications->createNotification();
			$notification->setApp('support')
				->setObject('subscription', $instanceSize)
				->setSubject('subscription_info')
				->setUser($adminUser->getUID());

			$count = $this->notifications->getCount($notification);

			// skip if the user already has a notification
			if ($count > 0) {
				continue;
			}

			$notification->setDateTime(new \DateTime());
			$notification->setLink($this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']));
			$this->notifications->notify($notification);

			$updateLastNotificationTime = true;
		}

		foreach ($adminUsers as $adminUser) {
			$emailAddress = $adminUser->getEMailAddress();
			if ($emailAddress === null || $emailAddress === '') {
				continue;
			}

			$this->sendNoSubscriptionEmail($adminUser);

			$updateLastNotificationTime = true;
		}

		if ($updateLastNotificationTime) {
			$this->config->setAppValue('support', 'last_notification', $currentTime);
		}
	}

	private function handleOverLimit(string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
		$currentTime = time();

		$lastNotificationTime = (int)$this->config->getAppValue('support', 'last_over_limit_notification', 0);

		// skip if last notification was within the last 5 days
		if (($lastNotificationTime + 5 * 24 * 3600) > $currentTime) {
			return;
		}

		$updateLastNotificationTime = false;

		$adminGroup = $this->groupManager->get('admin');
		$adminUsers = $adminGroup->getUsers();

		foreach ($adminUsers as $adminUser) {
			$notification = $this->notifications->createNotification();
			$notification->setApp('support')
				->setObject('subscription', 'over_limit')
				->setSubject('subscription_over_limit')
				->setUser($adminUser->getUID());

			$count = $this->notifications->getCount($notification);

			// skip if the user already has a notification
			if ($count > 0) {
				continue;
			}

			$notification->setDateTime(new \DateTime());
			$notification->setLink($this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']));
			$this->notifications->notify($notification);

			$updateLastNotificationTime = true;
		}

		foreach ($adminUsers as $adminUser) {
			$emailAddress = $adminUser->getEMailAddress();
			if ($emailAddress === null || $emailAddress === '') {
				continue;
			}

			$this->sendOverLimitEmail(
				$adminUser,
				$accountManager,
				$accountManagerEmail,
				$accountManagerPhone
			);

			$updateLastNotificationTime = true;
		}

		if ($updateLastNotificationTime) {
			$this->config->setAppValue('support', 'last_over_limit_notification', $currentTime);
		}
	}

	private function handleExpired(string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
		$currentTime = time();

		$lastNotificationTime = (int)$this->config->getAppValue('support', 'last_expired_notification', 0);

		// skip if last notification was within the last 5 days
		if (($lastNotificationTime + 5 * 24 * 3600) > $currentTime) {
			return;
		}

		$updateLastNotificationTime = false;

		$adminGroup = $this->groupManager->get('admin');
		$adminUsers = $adminGroup->getUsers();

		foreach ($adminUsers as $adminUser) {
			$notification = $this->notifications->createNotification();
			$notification->setApp('support')
				->setObject('subscription', 'expired')
				->setSubject('subscription_expired')
				->setUser($adminUser->getUID());

			$count = $this->notifications->getCount($notification);

			// skip if the user already has a notification
			if ($count > 0) {
				continue;
			}

			$notification->setDateTime(new \DateTime());
			$notification->setLink($this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']));
			$this->notifications->notify($notification);

			$updateLastNotificationTime = true;
		}

		foreach ($adminUsers as $adminUser) {
			$emailAddress = $adminUser->getEMailAddress();
			if ($emailAddress === null || $emailAddress === '') {
				continue;
			}

			$this->sendExpiredEmail(
				$adminUser,
				$accountManager,
				$accountManagerEmail,
				$accountManagerPhone
			);

			$updateLastNotificationTime = true;
		}

		if ($updateLastNotificationTime) {
			$this->config->setAppValue('support', 'last_expired_notification', $currentTime);
		}
	}

	private function sendNoSubscriptionEmail(IUser $user): void {
		// TODO what about enforced language?
		$language = $this->config->getUserValue($user->getUID(), 'core', 'lang', 'en');
		$l = $this->l10nFactory->get('support', $language);

		$link = $this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']);

		$message = $this->mailer->createMessage();

		$emailTemplate = $this->mailer->createEMailTemplate('support.SubscriptionNotification', [
			'displayName' => $user->getDisplayName(),
		]);

		$emailTemplate->setSubject($l->t('Your server has no Nextcloud Subscription'));
		$emailTemplate->addHeader();
		$emailTemplate->addHeading($l->t('Your Nextcloud server is not backed by a Nextcloud Enterprise Subscription.'));
		$text = $l->t('A Nextcloud Enterprise Subscription means the original developers behind your self-hosted cloud server are 100%% dedicated to your success: the security, scalability, performance and functionality of your service!');

		$listItem1 = $l->t('If your server setup breaks and employees can\'t work anymore, you don\'t have to rely on searching online forums for a solution. You have direct access to our experienced engineers!');
		$listItem2 = $l->t('You have a contract with the vendor providing early security information, mitigations, patches and updates.');
		$listItem3 = $l->t('If you need to stay longer on your current version without disruptions, you don\'t have to run software without security updates.');
		$listItem4 = $l->t('You have the best expertise at hand to deal with performance and scalability issues.');
		$listItem5 = $l->t('You have access to the right documentation and expertise to quickly answer compliance questions or deliver on GDPR, HIPAA and other regulation requirements.');

		$text2 = $l->t('We can also provide Outlook integration, Online Office, scalable integrated audio-video and chat communication and other features only available in a limited form for free or develop further integrations and capabilities to your needs.');
		$text3 = $l->t('A subscription helps you get the most out of Nextcloud!');

		$emailTemplate->addBodyText(
			htmlspecialchars($text),
			$text
		);

		$emailTemplate->addBodyListItem(htmlspecialchars($listItem1), '', '', $listItem1);
		$emailTemplate->addBodyListItem(htmlspecialchars($listItem2), '', '', $listItem2);
		$emailTemplate->addBodyListItem(htmlspecialchars($listItem3), '', '', $listItem3);
		$emailTemplate->addBodyListItem(htmlspecialchars($listItem4), '', '', $listItem4);
		$emailTemplate->addBodyListItem(htmlspecialchars($listItem5), '', '', $listItem5);

		$emailTemplate->addBodyText(
			htmlspecialchars($text2) . '<br><br>' .
			htmlspecialchars($text3),
			$text2 . "\n\n" .
			$text3
		);

		$emailTemplate->addBodyButton(
			$l->t('Learn more now'),
			$link
		);

		$generalLink = $this->urlGenerator->getAbsoluteURL('/');
		$noteText = $l->t('This mail was sent to all administrators by the support app on your Nextcloud instance at %1$s because you have over %2$s registered users.', [$generalLink, self::THRESHOLD_LARGE]);
		$emailTemplate->addBodyText($noteText);

		$emailTemplate->addFooter();
		$message->useTemplate($emailTemplate);

		$attachment = $this->mailer->createAttachmentFromPath(__DIR__ . '/../../resources/Why the Nextcloud Subscription.pdf');
		$message->attach($attachment);
		$message->setTo([$user->getEMailAddress()]);

		$this->mailer->send($message);
	}

	private function sendOverLimitEmail(IUser $user, string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
		// TODO what about enforced language?
		$language = $this->config->getUserValue($user->getUID(), 'core', 'lang', 'en');
		$l = $this->l10nFactory->get('support', $language);

		$link = $this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']);

		$message = $this->mailer->createMessage();

		$emailTemplate = $this->mailer->createEMailTemplate('support.SubscriptionNotification', [
			'displayName' => $user->getDisplayName(),
		]);

		$emailTemplate->setSubject($l->t('Your Nextcloud server Subscription is over limit'));
		$emailTemplate->addHeader();
		$emailTemplate->addHeading($l->t('Your Nextcloud server Subscription is over limit'));
		$text = $l->t('Dear admin,');
		$text2 = $l->t('Your Nextcloud Subscription doesn\'t cover the number of users who are currently active on this server. Please contact your Nextcloud account manager to get your subscription updated!');
		$text3 = $l->t('%1$s is your account manager and can be reached by email via %2$s or by phone via %3$s.', [$accountManager, $accountManagerEmail, $accountManagerPhone]);
		$text4 = $l->t('Thank you,');
		$text5 = $l->t('Your Nextcloud team');

		$emailTemplate->addBodyText(
			htmlspecialchars($text) . '<br><br>' .
			htmlspecialchars($text2) . '<br><br>' .
			htmlspecialchars($text3) . '<br><br>' .
			htmlspecialchars($text4) . '<br><br>' .
			htmlspecialchars($text5),
			$text . "\n\n" .
			$text2 . "\n\n" .
			$text3 . "\n\n" .
			$text4 . "\n\n" .
			$text5
		);

		$emailTemplate->addBodyButton(
			$l->t('Learn more now'),
			$link
		);

		$generalLink = $this->urlGenerator->getAbsoluteURL('/');
		$noteText = $l->t('This mail was sent to all administrators by the support app on your Nextcloud instance at %s because you have more users than your subscription covers.', [$generalLink]);
		$emailTemplate->addBodyText($noteText);

		$message->setTo([$user->getEMailAddress()]);

		$emailTemplate->addFooter();

		$message->useTemplate($emailTemplate);
		$this->mailer->send($message);
	}

	private function sendExpiredEmail(IUser $user, string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
		// TODO what about enforced language?
		$language = $this->config->getUserValue($user->getUID(), 'core', 'lang', 'en');
		$l = $this->l10nFactory->get('support', $language);

		$link = $this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'support']);

		$message = $this->mailer->createMessage();

		$emailTemplate = $this->mailer->createEMailTemplate('support.SubscriptionNotification', [
			'displayName' => $user->getDisplayName(),
		]);

		$emailTemplate->setSubject($l->t('Your Nextcloud server Subscription is expired'));
		$emailTemplate->addHeader();
		$emailTemplate->addHeading($l->t('Your Nextcloud server Subscription is expired!'));
		$text = $l->t('Dear admin,');
		$text2 = $l->t('Your Nextcloud Subscription has expired! Please contact your Nextcloud account manager to get your subscription updated!');
		$text3 = $l->t('%1$s is your account manager and can be reached by email via %2$s or by phone via %3$s.', [$accountManager, $accountManagerEmail, $accountManagerPhone]);
		$text4 = $l->t('Thank you,');
		$text5 = $l->t('Your Nextcloud team');

		$emailTemplate->addBodyText(
			htmlspecialchars($text) . '<br><br>' .
			htmlspecialchars($text2) . '<br><br>' .
			htmlspecialchars($text3) . '<br><br>' .
			htmlspecialchars($text4) . '<br><br>' .
			htmlspecialchars($text5),
			$text . "\n\n" .
			$text2 . "\n\n" .
			$text3 . "\n\n" .
			$text4 . "\n\n" .
			$text5
		);

		$emailTemplate->addBodyButton(
			$l->t('Learn more now'),
			$link
		);

		$generalLink = $this->urlGenerator->getAbsoluteURL('/');
		$noteText = $l->t('This mail was sent to all administrators by the support app on your Nextcloud instance at %s because your subscription expired.', [$generalLink]);
		$emailTemplate->addBodyText($noteText);

		$message->setTo([$user->getEMailAddress()]);

		$emailTemplate->addFooter();

		$message->useTemplate($emailTemplate);
		$this->mailer->send($message);
	}


	/**
	 * return details about installed apps
	 *
	 *  [
	 *    appId => [
	 *      'enabled' => string,
	 *      'version' => string
	 *    ]
	 * ]
	 *
	 * 'enabled' can be:
	 *     'disabled', if app is disabled
	 *     'enabled', if app is enabled
	 *     'group-limited', if app is limited to groups
	 *     'invalid', if stored value does not fit previous condition
	 *
	 * @return array<string, array<string, string>>
	 */
	private function getAppsDetails(): array {
		$enabled = $this->appConfig->searchValues('enabled');
		$installed = $this->appConfig->searchValues('installed_version');

		/** @var array<string, array<string, string>> $details */
		$details = [];
		foreach ($enabled as $appId => $enabledStatus) {
			$enabledFlag = 'invalid';
			try {
				$enabledFlag = match ($enabledStatus) {
					'no' => 'disabled',
					'yes' => 'enabled',
					default => (is_array(json_decode($enabledStatus, flags: JSON_THROW_ON_ERROR))) ? 'group-limited' : $enabledFlag
				};
			} catch (\JsonException) {
			}

			$details[$appId] = [
				'enabled' => $enabledFlag,
				'version' => $installed[$appId] ?? 'missing',
			];
		}

		return $details;
	}
}

Zerion Mini Shell 1.0