Обновление клиента
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\AppInfo;
|
||||
|
||||
use OCA\Support\Capabilities;
|
||||
use OCA\Support\Notification\Notifier;
|
||||
use OCA\Support\Settings\Admin;
|
||||
use OCA\Support\Settings\Section;
|
||||
use OCA\Support\Subscription\SubscriptionAdapter;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\IConfig;
|
||||
use OCP\Settings\IManager as ISettingsManager;
|
||||
use OCP\Support\Subscription\Exception\AlreadyRegisteredException;
|
||||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
public const APP_ID = 'support';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function register(IRegistrationContext $context): void {
|
||||
$context->registerCapability(Capabilities::class);
|
||||
$context->registerNotifierService(Notifier::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function boot(IBootContext $context): void {
|
||||
$container = $context->getAppContainer();
|
||||
|
||||
/* @var $registry IRegistry */
|
||||
$registry = $container->get(IRegistry::class);
|
||||
try {
|
||||
$registry->registerService(SubscriptionAdapter::class);
|
||||
if ($container->get(IConfig::class)->getAppValue('support', 'hide-app', 'no') !== 'yes') {
|
||||
$settingsManager = $container->get(ISettingsManager::class);
|
||||
$settingsManager->registerSetting('admin', Admin::class);
|
||||
$settingsManager->registerSection('admin', Section::class);
|
||||
}
|
||||
} catch (AlreadyRegisteredException $e) {
|
||||
$logger = $container->get(LoggerInterface::class);
|
||||
$logger->critical('Multiple subscription adapters are registered.', [
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\BackgroundJobs;
|
||||
|
||||
use OCA\Support\Service\SubscriptionService;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class CheckSubscription extends TimedJob {
|
||||
public function __construct(
|
||||
ITimeFactory $factory,
|
||||
private readonly IAppConfig $appConfig,
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
) {
|
||||
parent::__construct($factory);
|
||||
// Run every 5 minutes
|
||||
$this->setInterval(60 * 5);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function run($argument) {
|
||||
$lastCheck = $this->appConfig->getValueInt('support', 'last_check');
|
||||
// renew subscription info every 23h
|
||||
if (time() - $lastCheck > 23 * 60 * 60) {
|
||||
$this->subscriptionService->renewSubscriptionInfo(false);
|
||||
$this->subscriptionService->checkSubscription();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support;
|
||||
|
||||
use OCA\Support\Subscription\SubscriptionAdapter;
|
||||
use OCP\Capabilities\ICapability;
|
||||
use OCP\IConfig;
|
||||
|
||||
class Capabilities implements ICapability {
|
||||
public function __construct(
|
||||
protected readonly SubscriptionAdapter $adapter,
|
||||
protected readonly IConfig $config,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* support?: array{
|
||||
* hasValidSubscription: bool,
|
||||
* desktopEnterpriseChannel: string
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
#[\Override]
|
||||
public function getCapabilities(): array {
|
||||
if (!$this->adapter->hasValidSubscription()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'support' => [
|
||||
'hasValidSubscription' => true,
|
||||
'desktopEnterpriseChannel' => $this->config->getSystemValueString('desktopEnterpriseChannel', 'enterprise'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Support\Command;
|
||||
|
||||
use OCA\Support\DetailManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class SystemReport extends Command {
|
||||
public function __construct(
|
||||
protected readonly DetailManager $detailManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setName('support:report')
|
||||
->setDescription('Generate a system report')
|
||||
;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$output->writeln($this->detailManager->getRenderedDetails());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Controller;
|
||||
|
||||
use OCA\Support\DetailManager;
|
||||
use OCA\Support\Service\SubscriptionService;
|
||||
use OCA\Support\Settings\Admin;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Constants;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Security\Events\GenerateSecurePasswordEvent;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ApiController extends Controller {
|
||||
private Folder $userFolder;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
protected readonly IURLGenerator $urlGenerator,
|
||||
protected readonly SubscriptionService $subscriptionService,
|
||||
protected readonly DetailManager $detailManager,
|
||||
protected readonly IUserSession $userSession,
|
||||
protected readonly LoggerInterface $logger,
|
||||
protected readonly IL10N $l10n,
|
||||
protected readonly IManager $shareManager,
|
||||
protected readonly IEventDispatcher $eventDispatcher,
|
||||
protected readonly ISecureRandom $random,
|
||||
protected readonly ITimeFactory $timeFactory,
|
||||
protected readonly ?string $userId,
|
||||
readonly IRootFolder $rootFolder,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->userFolder = $rootFolder->getUserFolder($this->userId);
|
||||
}
|
||||
|
||||
#[AuthorizedAdminSetting(settings: Admin::class)]
|
||||
public function setSubscriptionKey(string $subscriptionKey): RedirectResponse {
|
||||
$this->subscriptionService->setSubscriptionKey(trim($subscriptionKey));
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'support'])));
|
||||
}
|
||||
|
||||
#[AuthorizedAdminSetting(settings: Admin::class)]
|
||||
public function generateSystemReport(): DataResponse {
|
||||
try {
|
||||
$directory = $this->userFolder->get('System information');
|
||||
} catch (NotFoundException $e) {
|
||||
try {
|
||||
$directory = $this->userFolder->newFolder('System information');
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->warning('Could not create folder "System information" to store generated report.', [
|
||||
'app' => 'support',
|
||||
'exception' => $e,
|
||||
]);
|
||||
$response = new DataResponse(['message' => $this->l10n->t('Could not create folder "System information" to store generated report.')]);
|
||||
$response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!($directory instanceof Folder)) {
|
||||
$this->logger->warning('Could not create folder "System information" to store generated report, a file exists with this name.', [
|
||||
'app' => 'support',
|
||||
]);
|
||||
$response = new DataResponse(['message' => $this->l10n->t('Could not create folder "System information" to store generated report, a file exists with this name.')]);
|
||||
$response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$date = $this->timeFactory->getDateTime()->format('Y-m-d');
|
||||
$filename = $date . '.md';
|
||||
$filename = $directory->getNonExistingName($filename);
|
||||
|
||||
try {
|
||||
$file = $directory->newFile($filename);
|
||||
$details = $this->detailManager->getRenderedDetails();
|
||||
$file->putContent($details);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Could not create file "' . $filename . '" to store generated report.', [
|
||||
'app' => 'support',
|
||||
'exception' => $e,
|
||||
]);
|
||||
$response = new DataResponse(['message' => $this->l10n->t('Could not create file "%s" to store generated report.', [ $filename ])]);
|
||||
$response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return $response;
|
||||
}
|
||||
|
||||
try {
|
||||
$passwordEvent = new GenerateSecurePasswordEvent();
|
||||
$this->eventDispatcher->dispatchTyped($passwordEvent);
|
||||
$password = $passwordEvent->getPassword() ?? $this->random->generate(20);
|
||||
$share = $this->shareManager->newShare();
|
||||
$share->setNode($file);
|
||||
$share->setPermissions(Constants::PERMISSION_READ);
|
||||
$share->setShareType(IShare::TYPE_LINK);
|
||||
$share->setSharedBy($this->userId);
|
||||
$share->setPassword($password);
|
||||
|
||||
if ($this->shareManager->shareApiLinkDefaultExpireDateEnforced()) {
|
||||
$expiry = $this->timeFactory->getDateTime();
|
||||
$expiry->add(new \DateInterval('P' . $this->shareManager->shareApiLinkDefaultExpireDays() . 'D'));
|
||||
} else {
|
||||
$expiry = $this->timeFactory->getDateTime();
|
||||
$expiry->add(new \DateInterval('P2W'));
|
||||
}
|
||||
|
||||
$share->setExpirationDate($expiry);
|
||||
|
||||
$share = $this->shareManager->createShare($share);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Could not share file "' . $filename . '".', [
|
||||
'app' => 'support',
|
||||
'exception' => $e,
|
||||
]);
|
||||
$response = new DataResponse(['message' => $this->l10n->t('Could not share file "%s". Nevertheless, you can find it in the folder "System information".', [$filename])]);
|
||||
$response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new DataResponse(
|
||||
[
|
||||
'link' => $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]),
|
||||
'password' => $password,
|
||||
],
|
||||
Http::STATUS_CREATED
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support;
|
||||
|
||||
class Detail implements IDetail {
|
||||
public function __construct(
|
||||
private readonly string $section,
|
||||
private readonly string $title,
|
||||
private readonly string $information,
|
||||
private readonly int $type,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getTitle(): string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getSection(): string {
|
||||
return $this->section;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getInformation(): string {
|
||||
return $this->information;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getType(): int {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support;
|
||||
|
||||
use OCA\Support\Sections\AppApiSection;
|
||||
use OCA\Support\Sections\LdapSection;
|
||||
use OCA\Support\Sections\PhpInfoSection;
|
||||
use OCA\Support\Sections\ServerSection;
|
||||
use OCA\Support\Sections\SetupChecksSection;
|
||||
use OCA\Support\Sections\TalkSection;
|
||||
|
||||
class DetailManager {
|
||||
private array $sections = [];
|
||||
|
||||
public function __construct(
|
||||
ServerSection $serverSection,
|
||||
SetupChecksSection $setupChecksSection,
|
||||
PhpInfoSection $phpInfoSection,
|
||||
TalkSection $talkSection,
|
||||
LdapSection $ldapSection,
|
||||
AppApiSection $appApiSection,
|
||||
) {
|
||||
// Register core details that are used in every report
|
||||
$this->addSection($serverSection);
|
||||
$this->addSection($setupChecksSection);
|
||||
$this->addSection($phpInfoSection);
|
||||
if ($talkSection->isTalkEnabled()) {
|
||||
$this->addSection($talkSection);
|
||||
}
|
||||
if ($ldapSection->isLdapEnabled()) {
|
||||
$this->addSection($ldapSection);
|
||||
}
|
||||
if ($appApiSection->isAppApiEnabled()) {
|
||||
$this->addSection($appApiSection);
|
||||
}
|
||||
}
|
||||
|
||||
public function createSection(string $identifier, string $title, int $order = 0): void {
|
||||
$section = new Section($identifier, $title, $order);
|
||||
$this->addSection($section);
|
||||
}
|
||||
|
||||
public function addSection(ISection $section): void {
|
||||
if (array_key_exists($section->getIdentifier(), $this->sections)) {
|
||||
/** @var ISection $existing */
|
||||
$existing = $this->sections[$section->getIdentifier()];
|
||||
foreach ($section->getDetails() as $detail) {
|
||||
$existing->addDetail($detail);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->sections[$section->getIdentifier()] = $section;
|
||||
}
|
||||
|
||||
public function removeSection(string $section): void {
|
||||
unset($this->sections[$section]);
|
||||
}
|
||||
|
||||
public function createDetail(string $sectionIdentifier, string $title, string $information, int $type = IDetail::TYPE_MULTI_LINE_PREFORMAT): void {
|
||||
$detail = new Detail($sectionIdentifier, $title, $information, $type);
|
||||
/** @var ISection $sectionObject */
|
||||
$sectionObject = $this->sections[$sectionIdentifier];
|
||||
$sectionObject->addDetail($detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ISection[]
|
||||
*/
|
||||
public function getSections(): array {
|
||||
return $this->sections;
|
||||
}
|
||||
|
||||
public function getRenderedDetails(): string {
|
||||
$result = '';
|
||||
/** @var ISection $section */
|
||||
foreach ($this->sections as $section) {
|
||||
$result .= $this->renderSectionHeader($section);
|
||||
/** @var IDetail $detail */
|
||||
foreach ($section->getDetails() as $detail) {
|
||||
$result .= $this->renderDetail($detail);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function renderSectionHeader(ISection $section): string {
|
||||
return '## ' . $section->getTitle() . "\n\n";
|
||||
}
|
||||
|
||||
private function renderDetail(IDetail $detail): string {
|
||||
switch ($detail->getType()) {
|
||||
case IDetail::TYPE_SINGLE_LINE:
|
||||
return '**' . $detail->getTitle() . ':** ' . $detail->getInformation() . "\n\n";
|
||||
case IDetail::TYPE_MULTI_LINE:
|
||||
return '**' . $detail->getTitle() . ":** \n\n" . $detail->getInformation() . "\n\n";
|
||||
case IDetail::TYPE_MULTI_LINE_PREFORMAT:
|
||||
return '**' . $detail->getTitle() . ":** \n\n``` \n" . $detail->getInformation() . "\n```\n\n";
|
||||
case IDetail::TYPE_COLLAPSIBLE:
|
||||
return '<details><summary>' . $detail->getTitle() . "</summary>\n\n" . $detail->getInformation() . "\n</details>\n\n";
|
||||
case IDetail::TYPE_COLLAPSIBLE_PREFORMAT:
|
||||
return '<details><summary>' . $detail->getTitle() . "</summary>\n\n```\n" . $detail->getInformation() . "\n```\n</details>\n\n";
|
||||
default:
|
||||
return '**' . $detail->getTitle() . ':** ' . $detail->getInformation() . "\n\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Support;
|
||||
|
||||
interface IDetail {
|
||||
public const TYPE_SINGLE_LINE = 0;
|
||||
public const TYPE_MULTI_LINE = 1;
|
||||
public const TYPE_MULTI_LINE_PREFORMAT = 2;
|
||||
public const TYPE_COLLAPSIBLE = 3;
|
||||
public const TYPE_COLLAPSIBLE_PREFORMAT = 4;
|
||||
|
||||
public function getTitle(): string;
|
||||
public function getSection(): string;
|
||||
public function getInformation(): string;
|
||||
public function getType(): int;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support;
|
||||
|
||||
/**
|
||||
* Interface ISection
|
||||
*
|
||||
* @package OCA\IssueTemplate
|
||||
*/
|
||||
interface ISection {
|
||||
public function getIdentifier(): string;
|
||||
public function getTitle(): string;
|
||||
public function addDetail(IDetail $details): void;
|
||||
|
||||
/**
|
||||
* @return IDetail[]
|
||||
*/
|
||||
public function getDetails(): array;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Notification;
|
||||
|
||||
use OCA\Support\AppInfo\Application;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Notification\IManager;
|
||||
use OCP\Notification\INotification;
|
||||
use OCP\Notification\INotifier;
|
||||
use OCP\Notification\UnknownNotificationException;
|
||||
|
||||
class Notifier implements INotifier {
|
||||
public function __construct(
|
||||
protected readonly IURLGenerator $url,
|
||||
protected readonly IConfig $config,
|
||||
protected readonly IManager $notificationManager,
|
||||
protected readonly IFactory $l10nFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getID(): string {
|
||||
return 'support';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return $this->l10nFactory->get(Application::APP_ID)->t('Subscription notifications');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param INotification $notification
|
||||
* @param string $languageCode The code of the language that should be used to prepare the notification
|
||||
* @return INotification
|
||||
* @throws UnknownNotificationException When the notification was not prepared by a notifier
|
||||
* @since 9.0.0
|
||||
*/
|
||||
#[\Override]
|
||||
public function prepare(INotification $notification, string $languageCode): INotification {
|
||||
if ($notification->getApp() !== 'support') {
|
||||
throw new UnknownNotificationException();
|
||||
}
|
||||
|
||||
$l = $this->l10nFactory->get('support', $languageCode);
|
||||
|
||||
switch ($notification->getSubject()) {
|
||||
case 'subscription_info':
|
||||
$notification->setParsedSubject($l->t('Nextcloud Subscription'))
|
||||
->setParsedMessage($l->t('Your server has no Nextcloud Subscription or your Subscription has expired.'));
|
||||
$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('support', 'notification.svg')));
|
||||
return $notification;
|
||||
|
||||
case 'subscription_over_limit':
|
||||
$notification->setParsedSubject($l->t('Nextcloud Subscription'))
|
||||
->setParsedMessage($l->t('Your Nextcloud server subscription does not cover your number of users.'));
|
||||
$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('support', 'notification.svg')));
|
||||
return $notification;
|
||||
|
||||
case 'subscription_expired':
|
||||
$notification->setParsedSubject($l->t('Nextcloud Subscription'))
|
||||
->setParsedMessage($l->t('Your Nextcloud Subscription has expired!'));
|
||||
$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('support', 'notification.svg')));
|
||||
return $notification;
|
||||
|
||||
default:
|
||||
// Unknown subject => Unknown notification => throw
|
||||
throw new UnknownNotificationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Repair;
|
||||
|
||||
use OCP\IAppConfig;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
|
||||
class MigrateLazyAppConfig implements IRepairStep {
|
||||
public function __construct(
|
||||
protected readonly IAppConfig $appConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return 'Migrate some config values to lazy loading';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function run(IOutput $output): void {
|
||||
$this->appConfig->updateLazy('support', 'last_response', true);
|
||||
|
||||
// Copy often used values to non-lazy (also done when fetching)
|
||||
$data = $this->appConfig->getValueArray('support', 'last_response');
|
||||
if (!empty($data)) {
|
||||
$this->appConfig->setValueString('support', 'end_date', $data['endDate'] ?? '');
|
||||
$this->appConfig->setValueBool('support', 'extended_support', $data['extendedSupport'] ?? false);
|
||||
}
|
||||
|
||||
// if more config values needs to be switched to lazy, just add them here
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Repair;
|
||||
|
||||
use OCP\IConfig;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
use OCP\Support\Subscription\IRegistry;
|
||||
|
||||
class SwitchUpdaterServer implements IRepairStep {
|
||||
public function __construct(
|
||||
protected readonly IConfig $config,
|
||||
protected readonly IRegistry $subscriptionRegistry,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return 'Switches from default updater server to the customer one if a valid subscription is available';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function run(IOutput $output): void {
|
||||
if ($this->config->getAppValue('support', 'SwitchUpdaterServerHasRun') === 'yes') {
|
||||
$output->info('Repair step already executed');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUpdaterServer = $this->config->getSystemValue('updater.server.url', 'https://updates.nextcloud.com/updater_server/');
|
||||
$subscriptionKey = $this->config->getAppValue('support', 'subscription_key', '');
|
||||
|
||||
/**
|
||||
* only overwrite the updater server if:
|
||||
* - it is the default 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/' &&
|
||||
$this->subscriptionRegistry->delegateHasValidSubscription() &&
|
||||
$subscriptionKey !== '' &&
|
||||
preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)
|
||||
) {
|
||||
$this->config->setSystemValue('updater.server.url', 'https://updates.nextcloud.com/customers/' . $subscriptionKey . '/');
|
||||
}
|
||||
|
||||
// if everything is done, no need to redo the repair during next upgrade
|
||||
$this->config->setAppValue('support', 'SwitchUpdaterServerHasRun', 'yes');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support;
|
||||
|
||||
class Section implements ISection {
|
||||
/** @var IDetail[] */
|
||||
private array $details = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $identifier,
|
||||
private readonly string $title,
|
||||
int $order = 0,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getIdentifier(): string {
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getTitle(): string {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function addDetail(IDetail $details): void {
|
||||
$this->details[] = $details;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
return $this->details;
|
||||
}
|
||||
|
||||
public function createDetail(string $title, string $information, int $type = IDetail::TYPE_SINGLE_LINE): IDetail {
|
||||
$detail = new Detail($this->getIdentifier(), $title, $information, $type);
|
||||
$this->addDetail($detail);
|
||||
return $detail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use OCA\AppAPI\Db\ExAppMapper;
|
||||
use OCA\AppAPI\Service\DaemonConfigService;
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\Server;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class AppApiSection extends Section {
|
||||
public function __construct(
|
||||
protected readonly IConfig $config,
|
||||
protected readonly IAppManager $appManager,
|
||||
protected readonly IClientService $clientService,
|
||||
) {
|
||||
parent::__construct('app_api', 'AppAPI');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
$this->createDetail('AppAPI configuration', $this->getAppApiInfo(), IDetail::TYPE_COLLAPSIBLE);
|
||||
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
public function isAppApiEnabled(): bool {
|
||||
return $this->appManager->isInstalled('app_api');
|
||||
}
|
||||
|
||||
private function getAppApiInfo(): string {
|
||||
$output = PHP_EOL;
|
||||
|
||||
try {
|
||||
$exAppMapper = Server::get(ExAppMapper::class);
|
||||
$exApps = $exAppMapper->findAll();
|
||||
$output .= PHP_EOL;
|
||||
$output .= '## ExApps' . PHP_EOL;
|
||||
if (!empty($exApps)) {
|
||||
foreach ($exApps as $exApp) {
|
||||
$enabled = $exApp->getEnabled() ? 'enabled' : 'disabled';
|
||||
$output .= ' * ' . $exApp->getAppid() . ' (' . $exApp->getName() . '): ' . $exApp->getVersion() . ' [' . $enabled . ']' . PHP_EOL;
|
||||
}
|
||||
} else {
|
||||
$output .= ' * no ExApps installed' . PHP_EOL;
|
||||
}
|
||||
|
||||
$daemonConfigService = Server::get(DaemonConfigService::class);
|
||||
$daemonConfigs = $daemonConfigService->getRegisteredDaemonConfigs();
|
||||
$output .= PHP_EOL;
|
||||
$output .= '## Deploy daemons' . PHP_EOL;
|
||||
foreach ($daemonConfigs as $daemon) {
|
||||
$deployConfig = $daemon->getDeployConfig();
|
||||
$deployConfig['haproxy_password'] = '***';
|
||||
$output .= ' * ' . $daemon->getName() . ' (' . $daemon->getDisplayName() . ')' . PHP_EOL;
|
||||
$output .= ' - Is HaRP: ' . (isset($deployConfig['harp']) ? 'yes' : 'no') . PHP_EOL;
|
||||
$output .= ' - Deployment method: ' . $daemon->getAcceptsDeployId() . PHP_EOL;
|
||||
$output .= ' - Protocol: ' . $daemon->getProtocol() . PHP_EOL;
|
||||
$output .= ' - Host: ' . $daemon->getHost() . PHP_EOL;
|
||||
$output .= ' - Deploy config: ' . json_encode($deployConfig, JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
}
|
||||
|
||||
$config = [
|
||||
'default_daemon_config' => $this->config->getAppValue('app_api', 'default_daemon_config'),
|
||||
'init_timeout' => $this->config->getAppValue('app_api', 'init_timeout', '40'),
|
||||
'container_restart_policy' => $this->config->getAppValue('app_api', 'container_restart_policy', 'unless-stopped'),
|
||||
];
|
||||
$output .= PHP_EOL;
|
||||
$output .= '## Config' . PHP_EOL;
|
||||
$output .= ' * Default daemon config (default_daemon_config): ' . $config['default_daemon_config'] . PHP_EOL;
|
||||
$output .= ' * Init timeout (init_timeout): ' . $config['init_timeout'] . PHP_EOL;
|
||||
$output .= ' * Container restart policy (container_restart_policy): ' . $config['container_restart_policy'] . PHP_EOL;
|
||||
|
||||
} catch (NotFoundExceptionInterface|ContainerExceptionInterface) {
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
use OCA\User_LDAP\Configuration;
|
||||
use OCA\User_LDAP\Helper;
|
||||
use OCA\User_LDAP\User_Proxy;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class LdapSection extends Section {
|
||||
public function __construct(
|
||||
protected readonly IUserManager $userManager,
|
||||
) {
|
||||
parent::__construct('ldap', 'LDAP');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
$this->createDetail('LDAP configuration', $this->getLDAPInfo(), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
public function isLDAPEnabled(): bool {
|
||||
$backends = $this->userManager->getBackends();
|
||||
|
||||
foreach ($backends as $backend) {
|
||||
if ($backend instanceof User_Proxy) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getLDAPInfo(): string {
|
||||
$helper = Server::get(Helper::class);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
|
||||
// copy of OCA\User_LDAP\Command\ShowConfig::renderConfigs
|
||||
$configIDs = $helper->getServerConfigurationPrefixes();
|
||||
foreach ($configIDs as $id) {
|
||||
$configHolder = new Configuration($id);
|
||||
$configuration = $configHolder->getConfiguration();
|
||||
ksort($configuration);
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['Configuration', $id]);
|
||||
$rows = [];
|
||||
foreach ($configuration as $key => $value) {
|
||||
if ($key === 'ldapAgentPassword') {
|
||||
$value = '***';
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$value = implode(';', $value);
|
||||
}
|
||||
$rows[] = [$key, $value];
|
||||
}
|
||||
$table->setRows($rows);
|
||||
$table->render();
|
||||
}
|
||||
|
||||
return $output->fetch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
|
||||
class PhpInfoSection extends Section {
|
||||
public function __construct(
|
||||
) {
|
||||
parent::__construct('phpinfo', 'Phpinfo');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
ob_start();
|
||||
phpinfo(INFO_CONFIGURATION | INFO_MODULES);
|
||||
$phpinfo = ob_get_clean();
|
||||
|
||||
if ($phpinfo === false) {
|
||||
$this->createDetail('error', 'Failed to retrieve phpinfo output.', IDetail::TYPE_SINGLE_LINE);
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
if (strpos($phpinfo, '<!DOCTYPE html>') === false) {
|
||||
// If phpinfo output is not HTML, we dump the raw output
|
||||
$this->createDetail('phpifo', $phpinfo, IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
$parsedInfo = array_values($this->parsePhpInfoFromHtml($phpinfo))[0] ?? [];
|
||||
foreach ($parsedInfo as $sectionName => $sectionData) {
|
||||
$this->createDetail($sectionName, json_encode($sectionData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
}
|
||||
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
private function parsePhpInfoFromHtml(string $phpInfoHtmlStr): array {
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML($phpInfoHtmlStr, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
libxml_clear_errors();
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
|
||||
$result = [];
|
||||
$currentSection = '';
|
||||
$currentSubsection = '';
|
||||
|
||||
$elements = $xpath->query('//h1 | //h2 | //table');
|
||||
|
||||
foreach ($elements as $el) {
|
||||
if ($el->nodeName === 'h1') {
|
||||
$currentSection = trim($el->textContent);
|
||||
$result[$currentSection] = [];
|
||||
} elseif ($el->nodeName === 'h2') {
|
||||
$currentSubsection = trim($el->textContent);
|
||||
if (!isset($result[$currentSection][$currentSubsection])) {
|
||||
$result[$currentSection][$currentSubsection] = [];
|
||||
}
|
||||
} elseif ($el->nodeName === 'table') {
|
||||
$rows = $el->getElementsByTagName('tr');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$cols = $row->getElementsByTagName('td');
|
||||
$ths = $row->getElementsByTagName('th');
|
||||
|
||||
if ($cols->length === 2) {
|
||||
// Single key => value
|
||||
$key = trim($cols->item(0)->textContent);
|
||||
$val = trim($cols->item(1)->textContent);
|
||||
$result[$currentSection][$currentSubsection][$key] = $val;
|
||||
} elseif ($cols->length === 3) {
|
||||
// Directive with local/master value
|
||||
$key = trim($cols->item(0)->textContent);
|
||||
$local = trim($cols->item(1)->textContent);
|
||||
$master = trim($cols->item(2)->textContent);
|
||||
$result[$currentSection][$currentSubsection][$key] = [
|
||||
'local' => $local,
|
||||
'master' => $master,
|
||||
];
|
||||
} elseif ($ths->length > 0) {
|
||||
// This is a header row; skip or save metadata
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use OC\IntegrityCheck\Checker;
|
||||
use OC\SystemConfig;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
use OCA\Support\Service\SubscriptionService;
|
||||
use OCA\Support\Subscription\SubscriptionAdapter;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
use OCP\ServerVersion;
|
||||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class ServerSection extends Section {
|
||||
public function __construct(
|
||||
protected readonly IConfig $config,
|
||||
protected readonly Checker $checker,
|
||||
protected readonly IAppManager $appManager,
|
||||
protected readonly IDBConnection $connection,
|
||||
protected readonly IUserManager $userManager,
|
||||
protected readonly LoggerInterface $logger,
|
||||
protected readonly SystemConfig $systemConfig,
|
||||
protected readonly IAppConfig $appConfig,
|
||||
protected readonly ServerVersion $serverVersion,
|
||||
protected readonly SubscriptionAdapter $adapter,
|
||||
protected readonly ITimeFactory $timeFactory,
|
||||
) {
|
||||
parent::__construct('server-detail', 'Server configuration detail');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
$this->createDetail('Operating system', $this->getOsVersion());
|
||||
$this->createDetail('Webserver', $this->getWebserver());
|
||||
$this->createDetail('Database', $this->getDatabaseInfo());
|
||||
$this->createDetail('PHP version', $this->getPhpVersion());
|
||||
$this->createDetail('Nextcloud version', $this->getNextcloudVersion());
|
||||
$this->createDetail('Updated from an older Nextcloud/ownCloud or fresh install', '');
|
||||
$this->createDetail('Where did you install Nextcloud from', $this->getInstallMethod());
|
||||
$this->createDetail('Signing status', $this->getIntegrityResults(), IDetail::TYPE_COLLAPSIBLE);
|
||||
$this->createDetail('List of activated apps', $this->renderAppList(), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
|
||||
$this->createDetail('Configuration (config/config.php)', print_r(json_encode($this->getConfig(), JSON_PRETTY_PRINT), true), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
$this->createDetail('Cron Configuration', $this->getCronConfig());
|
||||
|
||||
$externalStorageEnabled = $this->appManager->isEnabledForUser('files_external');
|
||||
$this->createDetail('External storages', $externalStorageEnabled ? 'yes' : 'files_external is disabled');
|
||||
if ($externalStorageEnabled) {
|
||||
$this->createDetail('External storage configuration', $this->getExternalStorageInfo(), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
}
|
||||
|
||||
$this->createDetail('Encryption', $this->getEncryptionInfo());
|
||||
$this->createDetail('User-backends', $this->getUserBackendInfo());
|
||||
$this->createDetail('Subscription', $this->getSubscriptionInfo());
|
||||
|
||||
$this->createDetail('Browser', $this->getBrowser());
|
||||
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
private function getWebserver(): string {
|
||||
return ($_SERVER['SERVER_SOFTWARE'] ?? 'Unknown') . ' (' . PHP_SAPI . ')';
|
||||
}
|
||||
|
||||
private function getNextcloudVersion(): string {
|
||||
return $this->serverVersion->getHumanVersion() . ' - ' . $this->config->getSystemValueString('version');
|
||||
}
|
||||
private function getOsVersion(): string {
|
||||
return function_exists('php_uname') ? php_uname('s') . ' ' . php_uname('r') . ' ' . php_uname('v') . ' ' . php_uname('m') : PHP_OS;
|
||||
}
|
||||
private function getPhpVersion(): string {
|
||||
return PHP_VERSION . "\n\nModules loaded: " . implode(', ', get_loaded_extensions());
|
||||
}
|
||||
|
||||
protected function getDatabaseInfo(): string {
|
||||
return $this->config->getSystemValueString('dbtype') . ' ' . $this->getDatabaseVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* original source from nextcloud/survey_client
|
||||
* @link https://github.com/nextcloud/survey_client/blob/master/lib/Categories/Database.php#L80-L107
|
||||
*
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @license AGPL-3.0
|
||||
*/
|
||||
private function getDatabaseVersion(): string {
|
||||
switch ($this->config->getSystemValueString('dbtype')) {
|
||||
case 'sqlite':
|
||||
case 'sqlite3':
|
||||
$sql = 'SELECT sqlite_version() AS version';
|
||||
break;
|
||||
case 'oci':
|
||||
$sql = 'SELECT VERSION FROM PRODUCT_COMPONENT_VERSION';
|
||||
break;
|
||||
case 'mysql':
|
||||
case 'pgsql':
|
||||
default:
|
||||
$sql = 'SELECT VERSION() AS version';
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$version = $result->fetchOne();
|
||||
$result->closeCursor();
|
||||
if ($version) {
|
||||
return $this->cleanVersion($version);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logger->debug('Unable to determine database version', [
|
||||
'exception' => $e
|
||||
]);
|
||||
}
|
||||
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to strip away additional information
|
||||
*
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* @param string $version E.g. `5.6.27-0ubuntu0.14.04.1`
|
||||
* @return string `5.6.27`
|
||||
*/
|
||||
protected function cleanVersion(string $version): string {
|
||||
$matches = [];
|
||||
preg_match('/^(\d+)(\.\d+)(\.\d+)/', $version, $matches);
|
||||
if (isset($matches[0])) {
|
||||
return $matches[0];
|
||||
}
|
||||
return $version;
|
||||
}
|
||||
|
||||
private function getCronConfig(): string {
|
||||
$mode = $this->appConfig->getValueString('core', 'backgroundjobs_mode', 'ajax');
|
||||
$last = $this->appConfig->getValueInt('core', 'lastcron', 0);
|
||||
|
||||
if ($last === 0) {
|
||||
$formattedLast = 'never';
|
||||
} else {
|
||||
$formattedLast = date('c', $last) . ' (' . (time() - $last) . ' seconds ago)';
|
||||
}
|
||||
|
||||
return PHP_EOL . PHP_EOL
|
||||
. 'Mode: ' . $mode . PHP_EOL
|
||||
. 'Last: ' . $formattedLast . PHP_EOL;
|
||||
}
|
||||
|
||||
private function getIntegrityResults(): string {
|
||||
if (!$this->checker->isCodeCheckEnforced()) {
|
||||
return 'Integrity checker has been disabled. Integrity cannot be verified.';
|
||||
}
|
||||
return print_r(json_encode($this->checker->getResults(), JSON_PRETTY_PRINT), true);
|
||||
}
|
||||
|
||||
private function getInstallMethod(): string {
|
||||
$base = \OC::$SERVERROOT;
|
||||
if (file_exists($base . '/.git')) {
|
||||
return 'git';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function renderAppList(): string {
|
||||
$apps = $this->getAppList();
|
||||
|
||||
$result = '';
|
||||
if ($apps['supported'] !== []) {
|
||||
$result .= "Supported:\n";
|
||||
foreach ($apps['supported'] as $name => $version) {
|
||||
$result .= ' - ' . $name . ': ' . $version . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$result .= "Enabled:\n";
|
||||
foreach ($apps['enabled'] as $name => $version) {
|
||||
$result .= ' - ' . $name . ': ' . $version . "\n";
|
||||
}
|
||||
|
||||
$result .= "Disabled:\n";
|
||||
foreach ($apps['disabled'] as $name => $version) {
|
||||
if ($version) {
|
||||
$result .= ' - ' . $name . ': ' . $version . "\n";
|
||||
} else {
|
||||
$result .= ' - ' . $name . "\n";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, string|bool>>
|
||||
*/
|
||||
private function getAppList(): array {
|
||||
$apps = $this->appManager->getAllAppsInAppsFolders();
|
||||
$alwaysEnabled = $this->appManager->getAlwaysEnabledApps();
|
||||
|
||||
$subscriptionRegistry = Server::get(IRegistry::class);
|
||||
$supportedAppsIDs = $subscriptionRegistry->delegateGetSupportedApps();
|
||||
|
||||
$supportedApps = $enabledApps = $disabledApps = [];
|
||||
$versions = $this->appManager->getAppInstalledVersions();
|
||||
|
||||
// sort enabled apps above disabled apps
|
||||
foreach ($apps as $app) {
|
||||
if (in_array($app, $alwaysEnabled)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->appManager->isEnabledForAnyone($app)) {
|
||||
if (in_array($app, $supportedAppsIDs)) {
|
||||
$supportedApps[] = $app;
|
||||
continue;
|
||||
}
|
||||
|
||||
// enabled but not ours
|
||||
$enabledApps[] = $app;
|
||||
continue;
|
||||
}
|
||||
|
||||
$disabledApps[] = $app;
|
||||
}
|
||||
|
||||
$apps = [
|
||||
'supported' => [],
|
||||
'enabled' => [],
|
||||
'disabled' => []
|
||||
];
|
||||
|
||||
sort($supportedApps);
|
||||
foreach ($supportedApps as $app) {
|
||||
$apps['supported'][$app] = $versions[$app] ?? true;
|
||||
}
|
||||
|
||||
sort($enabledApps);
|
||||
foreach ($enabledApps as $app) {
|
||||
$apps['enabled'][$app] = $versions[$app] ?? true;
|
||||
}
|
||||
|
||||
sort($disabledApps);
|
||||
foreach ($disabledApps as $app) {
|
||||
$apps['disabled'][$app] = $versions[$app] ?? false;
|
||||
}
|
||||
|
||||
return $apps;
|
||||
}
|
||||
|
||||
protected function getEncryptionInfo(): string {
|
||||
return $this->appConfig->getValueString('core', 'encryption_enabled', 'no');
|
||||
}
|
||||
|
||||
protected function getExternalStorageInfo(): string {
|
||||
$globalService = Server::get(GlobalStoragesService::class);
|
||||
$mounts = $globalService->getStorageForAllUsers();
|
||||
|
||||
// copy of OCA\Files_External\Command\ListCommand::listMounts
|
||||
if ($mounts === null || count($mounts) === 0) {
|
||||
return 'No mounts configured';
|
||||
}
|
||||
$headers = ['Mount ID', 'Mount Point', 'Storage', 'Authentication Type', 'Configuration', 'Options'];
|
||||
$headers[] = 'Applicable Users';
|
||||
$headers[] = 'Applicable Groups';
|
||||
$headers[] = 'Type';
|
||||
|
||||
$hideKeys = ['password', 'refresh_token', 'token', 'client_secret', 'public_key', 'private_key', 'key', 'secret'];
|
||||
/** @var StorageConfig $mount */
|
||||
foreach ($mounts as $mount) {
|
||||
$config = $mount->getBackendOptions();
|
||||
foreach ($config as $key => $value) {
|
||||
if (in_array($key, $hideKeys)) {
|
||||
$mount->setBackendOption($key, '***');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$defaultMountOptions = [
|
||||
'encrypt' => true,
|
||||
'previews' => true,
|
||||
'filesystem_check_changes' => 1,
|
||||
'enable_sharing' => false,
|
||||
'encoding_compatibility' => false,
|
||||
'readonly' => false,
|
||||
];
|
||||
$rows = array_map(function (StorageConfig $config) use ($defaultMountOptions) {
|
||||
$storageConfig = $config->getBackendOptions();
|
||||
$keys = array_keys($storageConfig);
|
||||
$values = array_values($storageConfig);
|
||||
$configStrings = array_map(function ($key, $value) {
|
||||
return $key . ': ' . json_encode($value);
|
||||
}, $keys, $values);
|
||||
$configString = implode(', ', $configStrings);
|
||||
$mountOptions = $config->getMountOptions();
|
||||
// hide defaults
|
||||
foreach ($mountOptions as $key => $value) {
|
||||
if (isset($defaultMountOptions[$key]) && ($value === $defaultMountOptions[$key])) {
|
||||
unset($mountOptions[$key]);
|
||||
}
|
||||
}
|
||||
$keys = array_keys($mountOptions);
|
||||
$values = array_values($mountOptions);
|
||||
$optionsStrings = array_map(function ($key, $value) {
|
||||
return $key . ': ' . json_encode($value);
|
||||
}, $keys, $values);
|
||||
$optionsString = implode(', ', $optionsStrings);
|
||||
$values = [
|
||||
$config->getId(),
|
||||
$config->getMountPoint(),
|
||||
$config->getBackend()->getText(),
|
||||
$config->getAuthMechanism()->getText(),
|
||||
$configString,
|
||||
$optionsString
|
||||
];
|
||||
$applicableUsers = implode(', ', $config->getApplicableUsers());
|
||||
$applicableGroups = implode(', ', $config->getApplicableGroups());
|
||||
if ($applicableUsers === '' && $applicableGroups === '') {
|
||||
$applicableUsers = 'All';
|
||||
}
|
||||
$values[] = $applicableUsers;
|
||||
$values[] = $applicableGroups;
|
||||
$values[] = $config->getType() === StorageConfig::MOUNT_TYPE_ADMIN ? 'Admin' : 'Personal';
|
||||
|
||||
return $values;
|
||||
}, $mounts);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$table = new Table($output);
|
||||
$table->setHeaders($headers);
|
||||
$table->setRows($rows);
|
||||
$table->render();
|
||||
|
||||
return $output->fetch();
|
||||
}
|
||||
|
||||
private function getConfig(): array {
|
||||
$keys = $this->systemConfig->getKeys();
|
||||
$configs = [];
|
||||
foreach ($keys as $key) {
|
||||
$value = $this->config->getFilteredSystemValue($key, serialize(null));
|
||||
if ($value !== 'N;') {
|
||||
$configs[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $configs;
|
||||
}
|
||||
|
||||
private function getBrowser(): string {
|
||||
return $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
|
||||
}
|
||||
|
||||
private function getUserBackendInfo(): string {
|
||||
$backends = $this->userManager->getBackends();
|
||||
|
||||
$output = PHP_EOL;
|
||||
foreach ($backends as $backend) {
|
||||
$output .= ' * ' . get_class($backend) . PHP_EOL;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getSubscriptionInfo(): string {
|
||||
$output = PHP_EOL;
|
||||
|
||||
if ($this->adapter->hasValidSubscription()) {
|
||||
$output .= ' * Instance has valid subscription key set' . PHP_EOL;
|
||||
} else {
|
||||
$output .= ' * No valid subscription key set' . PHP_EOL;
|
||||
}
|
||||
|
||||
$lastError = $this->appConfig->getValueInt('support', 'last_error');
|
||||
|
||||
if ($lastError > 0) {
|
||||
switch ($lastError) {
|
||||
case SubscriptionService::ERROR_FAILED_RETRY:
|
||||
$output .= ' * The subscription info could not properly fetched and will be retried' . PHP_EOL;
|
||||
break;
|
||||
case SubscriptionService::ERROR_FAILED_INVALID:
|
||||
$output .= ' * The subscription key was invalid' . PHP_EOL;
|
||||
break;
|
||||
case SubscriptionService::ERROR_NO_INTERNET_CONNECTION:
|
||||
$output .= ' * The subscription key could not be verified, because this server has no internet connection' . PHP_EOL;
|
||||
break;
|
||||
case SubscriptionService::ERROR_INVALID_SUBSCRIPTION_KEY:
|
||||
$output .= ' * The subscription key had an invalid format' . PHP_EOL;
|
||||
break;
|
||||
default:
|
||||
$output .= ' * An error occurred while fetching the subscription information' . PHP_EOL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->adapter->isHardUserLimitReached()) {
|
||||
$output .= ' * Reached user limit of subscription' . PHP_EOL;
|
||||
}
|
||||
|
||||
$rateLimitReached = $this->appConfig->getValueInt('notifications', 'rate_limit_reached');
|
||||
if ($rateLimitReached >= ($this->timeFactory->now()->getTimestamp() - 7 * 24 * 3600)) {
|
||||
$output .= ' * Fair-use push notification limit reached' . PHP_EOL;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
use OCP\RichObjectStrings\IRichTextFormatter;
|
||||
use OCP\SetupCheck\ISetupCheckManager;
|
||||
use OCP\SetupCheck\SetupResult;
|
||||
|
||||
class SetupChecksSection extends Section {
|
||||
public function __construct(
|
||||
private ISetupCheckManager $setupCheckManager,
|
||||
private IRichTextFormatter $richTextFormatter,
|
||||
) {
|
||||
parent::__construct('setupchecks', 'Setup checks');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
// FIXME Make sure to use the cached version once we have it
|
||||
$results = $this->setupCheckManager->runAll();
|
||||
foreach ($results as $category => $content) {
|
||||
if ($category === 'accounts') {
|
||||
/* Do not include accounts section in the report */
|
||||
continue;
|
||||
}
|
||||
$problems = '';
|
||||
foreach ($content as $class => $result) {
|
||||
if ($result->getSeverity() != SetupResult::SUCCESS) {
|
||||
$description = $result->getDescription();
|
||||
$descriptionParameters = $result->getDescriptionParameters();
|
||||
if ($description !== null && $descriptionParameters !== null) {
|
||||
$description = $this->richTextFormatter->richToParsed($description, $descriptionParameters);
|
||||
}
|
||||
$descriptionLines = explode("\n", $description);
|
||||
$problems .= ' * ' . $result->getName() . ': ' . implode("\n ", $descriptionLines) . "\n";
|
||||
}
|
||||
}
|
||||
if ($problems !== '') {
|
||||
$this->createDetail($category, $problems, IDetail::TYPE_COLLAPSIBLE);
|
||||
}
|
||||
}
|
||||
return parent::getDetails();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Sections;
|
||||
|
||||
use OCA\Support\IDetail;
|
||||
use OCA\Support\Section;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
|
||||
class TalkSection extends Section {
|
||||
public function __construct(
|
||||
protected readonly IConfig $config,
|
||||
protected readonly IAppManager $appManager,
|
||||
protected readonly IClientService $clientService,
|
||||
protected readonly IAppConfig $appConfig,
|
||||
) {
|
||||
parent::__construct('talk', 'Talk');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDetails(): array {
|
||||
$this->createDetail('Talk configuration', $this->getTalkInfo());
|
||||
$this->createDetail('Talk app configuration', $this->getTalkAppConfiguration(), IDetail::TYPE_COLLAPSIBLE_PREFORMAT);
|
||||
|
||||
return parent::getDetails();
|
||||
}
|
||||
|
||||
public function isTalkEnabled(): bool {
|
||||
return $this->appManager->isEnabledForUser('spreed');
|
||||
}
|
||||
|
||||
private function getTalkInfo(): string {
|
||||
$output = PHP_EOL;
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'stun_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
$output .= PHP_EOL;
|
||||
$output .= 'STUN servers' . PHP_EOL;
|
||||
if (empty($servers)) {
|
||||
$output .= ' * no custom server configured' . PHP_EOL;
|
||||
} else {
|
||||
foreach ($servers as $server) {
|
||||
$output .= ' * ' . $server . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'turn_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
$output .= PHP_EOL;
|
||||
$output .= 'TURN servers' . PHP_EOL;
|
||||
if (empty($servers)) {
|
||||
$output .= ' * no custom server configured' . PHP_EOL;
|
||||
} else {
|
||||
foreach ($servers as $server) {
|
||||
$output .= ' * ' . ($server['schemes'] ?? 'turn') . ':' . $server['server'] . ' - ' . $server['protocols'] . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'signaling_mode', 'default');
|
||||
$output .= PHP_EOL;
|
||||
$output .= 'Signaling servers (mode: ' . $config . '):' . PHP_EOL;
|
||||
|
||||
if ($this->config->getAppValue('spreed', 'sip_bridge_shared_secret') !== '') {
|
||||
$output .= ' * SIP dialin is enabled' . PHP_EOL;
|
||||
} else {
|
||||
$output .= ' * SIP dialin is disabled' . PHP_EOL;
|
||||
}
|
||||
if ($this->config->getAppValue('spreed', 'sip_dialout', 'no') !== 'no') {
|
||||
$output .= ' * SIP dialout is enabled' . PHP_EOL;
|
||||
} else {
|
||||
$output .= ' * SIP dialout is disabled' . PHP_EOL;
|
||||
}
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'signaling_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
if (empty($servers['servers'])) {
|
||||
$output .= ' * no custom server configured' . PHP_EOL;
|
||||
} else {
|
||||
foreach ($servers['servers'] as $server) {
|
||||
$output .= ' * ' . $server['server'] . ' - ' . $this->getTalkComponentVersion($server['server']) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$output .= PHP_EOL;
|
||||
$output .= 'Recording servers:' . PHP_EOL;
|
||||
if ($this->config->getAppValue('spreed', 'call_recording', 'yes') !== 'yes') {
|
||||
$output .= ' * Recording is disabled' . PHP_EOL;
|
||||
} else {
|
||||
$output .= ' * Recording is enabled' . PHP_EOL;
|
||||
}
|
||||
$output .= ' * Recording consent is set to "' . $this->config->getAppValue('spreed', 'recording_consent', 'default') . '"' . PHP_EOL;
|
||||
|
||||
$config = $this->config->getAppValue('spreed', 'recording_servers');
|
||||
$servers = json_decode($config, true);
|
||||
|
||||
if (empty($servers['servers'])) {
|
||||
$output .= ' * no recording server configured' . PHP_EOL;
|
||||
} else {
|
||||
foreach ($servers['servers'] as $server) {
|
||||
$output .= ' * ' . $server['server'] . ' - ' . $this->getTalkComponentVersion($server['server']) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getTalkAppConfiguration(): string {
|
||||
$spreedConfig = $this->appConfig->getAllValues('spreed', filtered: true);
|
||||
return json_encode($spreedConfig, JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
}
|
||||
|
||||
private function getTalkComponentVersion(string $url): string {
|
||||
$url = rtrim($url, '/');
|
||||
|
||||
if (strpos($url, 'wss://') === 0) {
|
||||
$url = 'https://' . substr($url, 6);
|
||||
}
|
||||
|
||||
if (strpos($url, 'ws://') === 0) {
|
||||
$url = 'http://' . substr($url, 5);
|
||||
}
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
try {
|
||||
$response = $client->get($url . '/api/v1/welcome', [
|
||||
'verify' => false,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$body = $response->getBody();
|
||||
|
||||
$data = json_decode($body, true);
|
||||
if (!is_array($data) || !isset($data['version'])) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return (string)$data['version'];
|
||||
} catch (\Exception $e) {
|
||||
return 'error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
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 OCP\ServerVersion;
|
||||
use OCP\User\Backend\ICountUsersBackend;
|
||||
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,
|
||||
protected readonly ServerVersion $serverVersion,
|
||||
) {
|
||||
}
|
||||
|
||||
public function setSubscriptionKey(string $subscriptionKey): void {
|
||||
if (!preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)) {
|
||||
$this->appConfig->setValueInt('support', 'last_error', self::ERROR_INVALID_SUBSCRIPTION_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->appConfig->setValueString('support', 'potential_subscription_key', $subscriptionKey);
|
||||
$this->appConfig->deleteKey('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)) {
|
||||
/** @var ICountUsersBackend $backend */
|
||||
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->appConfig->setValueInt('support', 'last_error', self::ERROR_NO_INTERNET_CONNECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriptionKey = $this->appConfig->getValueString('support', 'potential_subscription_key');
|
||||
|
||||
if (!preg_match('!^[a-zA-Z0-9-]{10,250}$!', $subscriptionKey)) {
|
||||
// fallback to normal subscription key
|
||||
$subscriptionKey = $this->appConfig->getValueString('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('.', $this->serverVersion->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->appConfig->setValueString('support', 'subscription_key', $subscriptionKey);
|
||||
$this->appConfig->setValueInt('support', 'last_check', time());
|
||||
$this->appConfig->setValueArray('support', 'last_response', $body, lazy: true);
|
||||
$this->appConfig->setValueString('support', 'end_date', $body['endDate'] ?? '');
|
||||
$this->appConfig->setValueBool('support', 'extended_support', $body['extendedSupport'] ?? false);
|
||||
|
||||
$this->appConfig->deleteKey('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->appConfig->deleteKey('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->appConfig->setValueInt('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->getLastResponseSubscriptionInfo();
|
||||
|
||||
$now = new \DateTime();
|
||||
$subscriptionEndDate = new \DateTime($subscriptionInfo['endDate'] ?? 'now');
|
||||
$hasSubscription = $subscriptionInfo !== null;
|
||||
$isInvalidSubscription = $now > $subscriptionEndDate;
|
||||
$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 getLastResponseSubscriptionInfo(): ?array {
|
||||
$subscriptionInfo = $this->appConfig->getValueArray('support', 'last_response', lazy: true);
|
||||
if (empty($subscriptionInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $subscriptionInfo;
|
||||
}
|
||||
|
||||
public function checkSubscription(): void {
|
||||
$hasInternetConnection = $this->config->getSystemValue('has_internet_connection', true);
|
||||
|
||||
if (!$hasInternetConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->appConfig->getValueBool('support', 'disable_subscription_emails')) {
|
||||
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 = $this->appConfig->getValueInt('core', 'installedat', $currentTime);
|
||||
|
||||
// skip if installed within the last 30 days
|
||||
if (($installTime + 30 * 24 * 3600) > $currentTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lastNotificationTime = $this->appConfig->getValueInt('support', 'last_notification');
|
||||
|
||||
// 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->appConfig->setValueInt('support', 'last_notification', $currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleOverLimit(string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
|
||||
$currentTime = time();
|
||||
|
||||
$lastNotificationTime = $this->appConfig->getValueInt('support', 'last_over_limit_notification');
|
||||
|
||||
// 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->appConfig->setValueInt('support', 'last_over_limit_notification', $currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleExpired(string $accountManager, string $accountManagerEmail, string $accountManagerPhone): void {
|
||||
$currentTime = time();
|
||||
|
||||
$lastNotificationTime = $this->appConfig->getValueInt('support', 'last_expired_notification');
|
||||
|
||||
// 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->appConfig->setValueInt('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 {
|
||||
/** @var array<string, string> */
|
||||
$enabled = $this->appConfig->searchValues('enabled', false, IAppConfig::VALUE_STRING);
|
||||
/** @var array<string, string> */
|
||||
$installed = $this->appConfig->searchValues('installed_version', false, IAppConfig::VALUE_STRING);
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Settings;
|
||||
|
||||
use OCA\Support\Service\SubscriptionService;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
use OCP\ServerVersion;
|
||||
use OCP\Settings\IDelegatedSettings;
|
||||
|
||||
class Admin implements IDelegatedSettings {
|
||||
|
||||
public function __construct(
|
||||
protected readonly IConfig $config,
|
||||
protected readonly IAppConfig $appConfig,
|
||||
protected readonly IUserManager $userManager,
|
||||
protected readonly IURLGenerator $urlGenerator,
|
||||
protected readonly SubscriptionService $subscriptionService,
|
||||
protected readonly ServerVersion $serverVersion,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getForm(): TemplateResponse {
|
||||
$userCount = $this->subscriptionService->getUserCount();
|
||||
$activeUserCount = $this->userManager->countSeenUsers();
|
||||
|
||||
$instanceSize = 'small';
|
||||
|
||||
if ($userCount > SubscriptionService::THRESHOLD_MEDIUM) {
|
||||
if ($userCount > SubscriptionService::THRESHOLD_LARGE) {
|
||||
$instanceSize = 'large';
|
||||
} else {
|
||||
$instanceSize = 'medium';
|
||||
}
|
||||
}
|
||||
|
||||
$subscriptionKey = $this->appConfig->getValueString('support', 'subscription_key');
|
||||
$potentialSubscriptionKey = $this->appConfig->getValueString('support', 'potential_subscription_key');
|
||||
$subscriptionInfo = $this->appConfig->getValueArray('support', 'last_response', lazy: true);
|
||||
$lastError = $this->appConfig->getValueInt('support', 'last_error');
|
||||
// delete the invalid error, because there is no renewal happening
|
||||
if ($lastError === SubscriptionService::ERROR_FAILED_INVALID) {
|
||||
if ($subscriptionKey !== '') {
|
||||
$this->appConfig->setValueString('support', 'potential_subscription_key', $subscriptionKey);
|
||||
} else {
|
||||
$this->appConfig->deleteKey('support', 'potential_subscription_key');
|
||||
}
|
||||
$this->appConfig->deleteKey('support', 'last_error');
|
||||
} elseif ($lastError === SubscriptionService::ERROR_INVALID_SUBSCRIPTION_KEY) {
|
||||
$this->appConfig->deleteKey('support', 'last_error');
|
||||
}
|
||||
|
||||
$now = new \DateTime();
|
||||
$subscriptionEndDate = new \DateTime($subscriptionInfo['endDate'] ?? 'now');
|
||||
if ($now > $subscriptionEndDate) {
|
||||
$years = 0;
|
||||
$months = 0;
|
||||
$days = 0;
|
||||
$weeks = 0;
|
||||
} else {
|
||||
$diff = $now->diff($subscriptionEndDate);
|
||||
$years = (int)$diff->format('%y');
|
||||
$months = (int)$diff->format('%m');
|
||||
$days = (int)$diff->format('%d');
|
||||
$weeks = floor($days / 7);
|
||||
|
||||
/* run up to the next month for 4 weeks and more */
|
||||
if ($weeks > 3) {
|
||||
$months += 1;
|
||||
$weeks = 0;
|
||||
$days = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$specificSubscriptions = [];
|
||||
|
||||
$collaboraEndDate = new \DateTime($subscriptionInfo['collabora']['endDate'] ?? 'yesterday');
|
||||
if ($now < $collaboraEndDate) {
|
||||
$specificSubscriptions[] = 'Collabora';
|
||||
}
|
||||
$talkEndDate = new \DateTime($subscriptionInfo['talk']['endDate'] ?? 'yesterday');
|
||||
if ($now < $talkEndDate) {
|
||||
$specificSubscriptions[] = 'Talk';
|
||||
}
|
||||
$groupwareEndDate = new \DateTime($subscriptionInfo['groupware']['endDate'] ?? 'yesterday');
|
||||
if ($now < $groupwareEndDate) {
|
||||
$specificSubscriptions[] = 'Groupware';
|
||||
}
|
||||
$allowedUsersCount = $subscriptionInfo['amountOfUsers'] ?? 0;
|
||||
$onlyCountActiveUsers = $subscriptionInfo['onlyCountActiveUsers'] ?? false;
|
||||
|
||||
if ($allowedUsersCount === -1) {
|
||||
$isOverLimit = false;
|
||||
} elseif ($onlyCountActiveUsers) {
|
||||
$isOverLimit = $allowedUsersCount < $activeUserCount;
|
||||
} else {
|
||||
$isOverLimit = $allowedUsersCount < $userCount;
|
||||
}
|
||||
|
||||
if (isset($subscriptionInfo['partnerContact']) && count($subscriptionInfo['partnerContact']) > 0) {
|
||||
$contactInfo = $subscriptionInfo['partnerContact'];
|
||||
} else {
|
||||
$contactInfo = $subscriptionInfo['accountManagerInfo'] ?? '';
|
||||
}
|
||||
|
||||
$params = [
|
||||
'instanceSize' => $instanceSize,
|
||||
'userCount' => $userCount,
|
||||
'activeUserCount' => $activeUserCount,
|
||||
'subscriptionKey' => $subscriptionKey,
|
||||
'potentialSubscriptionKey' => $potentialSubscriptionKey,
|
||||
'lastError' => $lastError,
|
||||
'contactPerson' => $contactInfo,
|
||||
|
||||
'subscriptionType' => $subscriptionInfo['level'] ?? '',
|
||||
'subscriptionUsers' => $allowedUsersCount,
|
||||
'onlyCountActiveUsers' => $onlyCountActiveUsers,
|
||||
'specificSubscriptions' => $specificSubscriptions,
|
||||
'extendedSupport' => $subscriptionInfo['extendedSupport'] ?? false,
|
||||
'expiryYears' => $years,
|
||||
'expiryMonths' => $months,
|
||||
'expiryWeeks' => $weeks,
|
||||
'expiryDays' => $days,
|
||||
|
||||
'validSubscription' => ($years + $months + $days) > 0,
|
||||
'overLimit' => $isOverLimit,
|
||||
|
||||
|
||||
'showSubscriptionDetails' => !empty($subscriptionInfo),
|
||||
'showSubscriptionKeyInput' => empty($subscriptionInfo),
|
||||
'showCommunitySupportSection' => $instanceSize === 'small' && empty($subscriptionInfo),
|
||||
'showEnterpriseSupportSection' => $instanceSize !== 'small' && empty($subscriptionInfo),
|
||||
|
||||
'subscriptionKeyUrl' => $this->urlGenerator->linkToRoute('support.api.setSubscriptionKey'),
|
||||
|
||||
'offlineActivationData' => [
|
||||
'subscriptionKey' => $potentialSubscriptionKey,
|
||||
'instanceId' => $this->config->getSystemValueString('instanceid', ''),
|
||||
'userCount' => $userCount,
|
||||
'activeUserCount' => $activeUserCount,
|
||||
'version' => implode('.', $this->serverVersion->getVersion())
|
||||
],
|
||||
|
||||
'subscriptionEndDate' => $subscriptionEndDate->format('Y-m-d'),
|
||||
];
|
||||
|
||||
return new TemplateResponse('support', 'admin', $params);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getSection(): string {
|
||||
return 'support';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int whether the form should be rather on the top or bottom of
|
||||
* the admin section. The forms are arranged in ascending order of the
|
||||
* priority values. It is required to return a value between 0 and 100.
|
||||
*
|
||||
* keep the server setting at the top, right after "server settings"
|
||||
*/
|
||||
#[\Override]
|
||||
public function getPriority(): int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): ?string {
|
||||
return null; // Only one setting in this section
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getAuthorizedAppConfig(): array {
|
||||
return [
|
||||
'support' => ['.*'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Settings;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
class Section implements IIconSection {
|
||||
public function __construct(
|
||||
protected readonly IL10N $l,
|
||||
protected readonly IURLGenerator $url,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getID(): string {
|
||||
return 'support';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getName(): string {
|
||||
return $this->l->t('Support');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getPriority(): int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getIcon(): string {
|
||||
return $this->url->imagePath('support', 'section.svg');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Support\Subscription;
|
||||
|
||||
use OCA\Support\Service\SubscriptionService;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\IConfig;
|
||||
use OCP\ServerVersion;
|
||||
use OCP\Support\Subscription\ISubscription;
|
||||
use OCP\Support\Subscription\ISupportedApps;
|
||||
|
||||
class SubscriptionAdapter implements ISubscription, ISupportedApps {
|
||||
public function __construct(
|
||||
private readonly SubscriptionService $subscriptionService,
|
||||
private readonly IConfig $config,
|
||||
private readonly IAppConfig $appConfig,
|
||||
private readonly ITimeFactory $timeFactory,
|
||||
private readonly ServerVersion $serverVersion,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a valid subscription is available
|
||||
*/
|
||||
#[\Override]
|
||||
public function hasValidSubscription(): bool {
|
||||
try {
|
||||
$endDate = $this->appConfig->getAppValueString('end_date');
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->subscriptionNotExpired($endDate);
|
||||
}
|
||||
|
||||
private function subscriptionNotExpired(string $endDate): bool {
|
||||
if ($endDate === '' || $endDate === 'now') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscriptionEndDate = $this->timeFactory->getDateTime($endDate);
|
||||
$now = $this->timeFactory->getDateTime();
|
||||
return $now < $subscriptionEndDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of app IDs that are supported by the subscription
|
||||
*
|
||||
* @since 17.0.0
|
||||
*/
|
||||
#[\Override]
|
||||
public function getSupportedApps(): array {
|
||||
[
|
||||
$instanceSize,
|
||||
$hasSubscription,
|
||||
$isInvalidSubscription,
|
||||
$isOverLimit,
|
||||
$subscriptionInfo
|
||||
] = $this->subscriptionService->getSubscriptionInfo();
|
||||
$hasValidGroupwareSubscription = $this->subscriptionNotExpired($subscriptionInfo['groupware']['endDate'] ?? 'now');
|
||||
$hasValidTalkSubscription = $this->subscriptionNotExpired($subscriptionInfo['talk']['endDate'] ?? 'now');
|
||||
$hasValidCollaboraSubscription = $this->subscriptionNotExpired($subscriptionInfo['collabora']['endDate'] ?? 'now');
|
||||
$hasValidOnlyOfficeSubscription = $this->subscriptionNotExpired($subscriptionInfo['onlyoffice']['endDate'] ?? 'now');
|
||||
|
||||
$filesSubscription = [
|
||||
'activity',
|
||||
'admin_audit',
|
||||
'bruteforcesettings',
|
||||
'circles',
|
||||
'cloud_federation_api',
|
||||
'comments',
|
||||
'data_request',
|
||||
'dav',
|
||||
'encryption',
|
||||
'external',
|
||||
'federatedfilesharing',
|
||||
'federation',
|
||||
'files',
|
||||
'files_accesscontrol',
|
||||
'files_antivirus',
|
||||
'files_automatedtagging',
|
||||
'files_external',
|
||||
'files_fulltextsearch',
|
||||
'files_fulltextsearch_tesseract',
|
||||
'files_pdfviewer',
|
||||
'files_retention',
|
||||
'files_sharing',
|
||||
'files_trashbin',
|
||||
'files_versions',
|
||||
'firstrunwizard',
|
||||
'fulltextsearch',
|
||||
'fulltextsearch_elasticsearch',
|
||||
'groupfolders',
|
||||
'guests',
|
||||
'logreader',
|
||||
'lookup_server_connector',
|
||||
'nextcloud_announcements',
|
||||
'notifications',
|
||||
'oauth2',
|
||||
'password_policy',
|
||||
'photos',
|
||||
'privacy',
|
||||
'provisioning_api',
|
||||
'recommendations',
|
||||
'serverinfo',
|
||||
'settings',
|
||||
'sharebymail',
|
||||
'sharepoint',
|
||||
'socialsharing_diaspora',
|
||||
'socialsharing_email',
|
||||
'socialsharing_facebook',
|
||||
'socialsharing_twitter',
|
||||
'support',
|
||||
'survey_client',
|
||||
'suspicious_login',
|
||||
'systemtags',
|
||||
'terms_of_service',
|
||||
'text',
|
||||
'theming',
|
||||
'twofactor_backupcodes',
|
||||
'twofactor_totp',
|
||||
'updatenotification',
|
||||
'user_ldap',
|
||||
'user_oidc',
|
||||
'user_saml',
|
||||
'viewer',
|
||||
'workflowengine',
|
||||
'workflow_script',
|
||||
];
|
||||
|
||||
$nextcloudVersion = $this->serverVersion->getMajorVersion();
|
||||
|
||||
if ($nextcloudVersion >= 30) {
|
||||
$filesSubscription[] = 'app_api';
|
||||
$filesSubscription[] = 'twofactor_nextcloud_notification';
|
||||
if (($subscriptionInfo['level'] ?? 'none') === 'ultimate') {
|
||||
$filesSubscription[] = 'webhook_listeners';
|
||||
}
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 29) {
|
||||
$filesSubscription[] = 'files_downloadlimit';
|
||||
$filesSubscription[] = 'files_reminders';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 28) {
|
||||
$filesSubscription[] = 'files_reminders';
|
||||
$filesSubscription[] = 'security_guard';
|
||||
} else {
|
||||
// Removed in 28
|
||||
$filesSubscription[] = 'files_rightclick';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 26) {
|
||||
$filesSubscription[] = 'files_confidential';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 25) {
|
||||
$filesSubscription[] = 'related_resources';
|
||||
} else {
|
||||
// Removed in 25
|
||||
$filesSubscription[] = 'files_videoplayer';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 24) {
|
||||
$filesSubscription[] = 'files_lock';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 22) {
|
||||
$filesSubscription[] = 'approval';
|
||||
$filesSubscription[] = 'contacts';
|
||||
$filesSubscription[] = 'files_zip';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 20) {
|
||||
$filesSubscription[] = 'dashboard';
|
||||
$filesSubscription[] = 'flow_notifications';
|
||||
$filesSubscription[] = 'user_status';
|
||||
$filesSubscription[] = 'weather_status';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 19) {
|
||||
$filesSubscription[] = 'contactsinteraction';
|
||||
}
|
||||
|
||||
if ($nextcloudVersion >= 18) {
|
||||
$filesSubscription[] = 'globalsiteselector';
|
||||
}
|
||||
|
||||
$supportedApps = [];
|
||||
|
||||
if ($hasSubscription) {
|
||||
$supportedApps = array_merge($supportedApps, $filesSubscription);
|
||||
}
|
||||
if ($hasValidGroupwareSubscription) {
|
||||
$supportedApps[] = 'calendar';
|
||||
$supportedApps[] = 'contacts';
|
||||
$supportedApps[] = 'deck';
|
||||
$supportedApps[] = 'mail';
|
||||
}
|
||||
if ($hasValidTalkSubscription) {
|
||||
$supportedApps[] = 'spreed';
|
||||
}
|
||||
if ($hasValidCollaboraSubscription) {
|
||||
$supportedApps[] = 'richdocuments';
|
||||
}
|
||||
if ($hasValidOnlyOfficeSubscription) {
|
||||
$supportedApps[] = 'onlyoffice';
|
||||
}
|
||||
|
||||
if (isset($subscriptionInfo['supportedApps'])) {
|
||||
foreach ($subscriptionInfo['supportedApps'] as $app) {
|
||||
if ($app !== '' && !in_array($app, $supportedApps)) {
|
||||
$supportedApps[] = $app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $supportedApps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the subscription has extended support
|
||||
*
|
||||
* @since 17.0.0
|
||||
*/
|
||||
#[\Override]
|
||||
public function hasExtendedSupport(): bool {
|
||||
try {
|
||||
return $this->appConfig->getAppValueBool('extended_support');
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a hard user limit is reached and no new users should be created
|
||||
*
|
||||
* @since 21.0.0
|
||||
*/
|
||||
#[\Override]
|
||||
public function isHardUserLimitReached(): bool {
|
||||
[
|
||||
,,
|
||||
$isInvalidSubscription,
|
||||
$isOverLimit,
|
||||
$subscriptionInfo
|
||||
] = $this->subscriptionService->getSubscriptionInfo();
|
||||
|
||||
$configUserLimit = (int)$this->config->getAppValue('support', 'user-limit', '0');
|
||||
if (
|
||||
!$isInvalidSubscription
|
||||
&& $configUserLimit > 0
|
||||
&& $configUserLimit <= $this->subscriptionService->getUserCount()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isset($subscriptionInfo['hasHardUserLimit']) || $subscriptionInfo['hasHardUserLimit'] === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $isOverLimit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user