diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2021-10-23 10:53:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-23 10:53:22 +0200 |
commit | a06001e0851abc6073af678b742da3e1aa96eec9 (patch) | |
tree | 1fefd580e28d0c5d74b8503b6a22bf00a09bcbaf | |
parent | 7e117da4357cda049bc41c1e146e1b0f8a06ddee (diff) | |
parent | 6f7ca3432c0e6d1e1925179e952f79c6273295f3 (diff) | |
download | nextcloud-server-a06001e0851abc6073af678b742da3e1aa96eec9.tar.gz nextcloud-server-a06001e0851abc6073af678b742da3e1aa96eec9.zip |
Merge pull request #29363 from nextcloud/fair-use-push
-rw-r--r-- | apps/settings/lib/Controller/CheckSetupController.php | 16 | ||||
-rw-r--r-- | apps/settings/lib/Settings/Personal/PersonalInfo.php | 17 | ||||
-rw-r--r-- | apps/settings/templates/settings/personal/personal.info.php | 7 | ||||
-rw-r--r-- | build/psalm-baseline.xml | 1 | ||||
-rw-r--r-- | core/Controller/LoginController.php | 18 | ||||
-rw-r--r-- | core/js/setupchecks.js | 6 | ||||
-rw-r--r-- | core/js/tests/specs/setupchecksSpec.js | 19 | ||||
-rw-r--r-- | lib/private/Notification/Manager.php | 80 | ||||
-rw-r--r-- | lib/private/Support/Subscription/Registry.php | 23 | ||||
-rw-r--r-- | lib/private/User/Manager.php | 7 | ||||
-rw-r--r-- | lib/public/Notification/IManager.php | 12 | ||||
-rw-r--r-- | lib/public/Support/Subscription/IRegistry.php | 4 | ||||
-rw-r--r-- | tests/Core/Controller/LoginControllerTest.php | 32 | ||||
-rw-r--r-- | tests/lib/Notification/ManagerTest.php | 86 | ||||
-rw-r--r-- | tests/lib/Support/Subscription/RegistryTest.php | 9 |
15 files changed, 285 insertions, 52 deletions
diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index e18dfb6028b..99e731b594c 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -78,6 +78,7 @@ use OCP\IRequest; use OCP\ITempManager; use OCP\IURLGenerator; use OCP\Lock\ILockingProvider; +use OCP\Notification\IManager; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -114,6 +115,8 @@ class CheckSetupController extends Controller { private $connection; /** @var ITempManager */ private $tempManager; + /** @var IManager */ + private $manager; public function __construct($AppName, IRequest $request, @@ -131,7 +134,8 @@ class CheckSetupController extends Controller { ISecureRandom $secureRandom, IniGetWrapper $iniGetWrapper, IDBConnection $connection, - ITempManager $tempManager) { + ITempManager $tempManager, + IManager $manager) { parent::__construct($AppName, $request); $this->config = $config; $this->clientService = $clientService; @@ -148,6 +152,15 @@ class CheckSetupController extends Controller { $this->iniGetWrapper = $iniGetWrapper; $this->connection = $connection; $this->tempManager = $tempManager; + $this->manager = $manager; + } + + /** + * Check if is fair use of free push service + * @return bool + */ + private function isFairUseOfFreePushService(): bool { + return $this->manager->isFairUseOfFreePushService(); } /** @@ -761,6 +774,7 @@ Raw output 'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(), 'cronInfo' => $this->getLastCronInfo(), 'cronErrors' => $this->getCronErrors(), + 'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(), 'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(), 'isMemcacheConfigured' => $this->isMemcacheConfigured(), 'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'), diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 928c18998df..72c443ed1b6 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -52,6 +52,7 @@ use OCP\IUser; use OCP\IUserManager; use OCP\L10N\IFactory; use OC\Profile\ProfileManager; +use OCP\Notification\IManager; use OCP\Settings\ISettings; class PersonalInfo implements ISettings { @@ -84,6 +85,9 @@ class PersonalInfo implements ISettings { /** @var IInitialState */ private $initialStateService; + /** @var IManager */ + private $manager; + public function __construct( IConfig $config, IUserManager $userManager, @@ -93,7 +97,8 @@ class PersonalInfo implements ISettings { IAppManager $appManager, IFactory $l10nFactory, IL10N $l, - IInitialState $initialStateService + IInitialState $initialStateService, + IManager $manager ) { $this->config = $config; $this->userManager = $userManager; @@ -104,6 +109,7 @@ class PersonalInfo implements ISettings { $this->l10nFactory = $l10nFactory; $this->l = $l; $this->initialStateService = $initialStateService; + $this->manager = $manager; } public function getForm(): TemplateResponse { @@ -160,6 +166,7 @@ class PersonalInfo implements ISettings { 'twitterScope' => $account->getProperty(IAccountManager::PROPERTY_TWITTER)->getScope(), 'twitterVerification' => $account->getProperty(IAccountManager::PROPERTY_TWITTER)->getVerified(), 'groups' => $this->getGroups($user), + 'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService() ] + $messageParameters + $languageParameters + $localeParameters; $personalInfoParameters = [ @@ -191,6 +198,14 @@ class PersonalInfo implements ISettings { } /** + * Check if is fair use of free push service + * @return boolean + */ + private function isFairUseOfFreePushService(): bool { + return $this->manager->isFairUseOfFreePushService(); + } + + /** * returns the primary biography in an * associative array */ diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index d258f616229..4cd0e4e34d6 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -35,6 +35,13 @@ script('settings', [ 'vue-settings-personal-info', ]); ?> +<?php if (!$_['isFairUseOfFreePushService']) : ?> + <div class="section"> + <div class="warning"> + <?php p($l->t('This community release of Nextcloud is unsupported and instant notifications are unavailable.')); ?> + </div> + </div> +<?php endif; ?> <div id="personal-settings" data-federation-enabled="<?php p($_['federationEnabled'] ? 'true' : 'false') ?>" data-lookup-server-upload-enabled="<?php p($_['lookupServerUploadEnabled'] ? 'true' : 'false') ?>"> diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index a55b6a2dac1..9ec176a4ae9 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -4253,6 +4253,7 @@ </ParamNameMismatch> </file> <file src="lib/private/Notification/Manager.php"> + <InvalidCatch occurrences="3"/> <TypeDoesNotContainType occurrences="2"> <code>!($notification instanceof INotification)</code> <code>!($notification instanceof INotification)</code> diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 15ec8365c19..b68f91f986e 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -46,6 +46,7 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; use OCP\IConfig; use OCP\IInitialStateService; +use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; use OCP\ISession; @@ -53,6 +54,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Notification\IManager; use OCP\Util; class LoginController extends Controller { @@ -81,6 +83,10 @@ class LoginController extends Controller { private $initialStateService; /** @var WebAuthnManager */ private $webAuthnManager; + /** @var IManager */ + private $manager; + /** @var IL10N */ + private $l10n; public function __construct(?string $appName, IRequest $request, @@ -94,7 +100,9 @@ class LoginController extends Controller { Throttler $throttler, Chain $loginChain, IInitialStateService $initialStateService, - WebAuthnManager $webAuthnManager) { + WebAuthnManager $webAuthnManager, + IManager $manager, + IL10N $l10n) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->config = $config; @@ -107,6 +115,8 @@ class LoginController extends Controller { $this->loginChain = $loginChain; $this->initialStateService = $initialStateService; $this->webAuthnManager = $webAuthnManager; + $this->manager = $manager; + $this->l10n = $l10n; } /** @@ -153,6 +163,12 @@ class LoginController extends Controller { } $loginMessages = $this->session->get('loginMessages'); + if (!$this->manager->isFairUseOfFreePushService()) { + if (!is_array($loginMessages)) { + $loginMessages = [[], []]; + } + $loginMessages[1][] = $this->l10n->t('This community release of Nextcloud is unsupported and instant notifications are unavailable.'); + } if (is_array($loginMessages)) { [$errors, $messages] = $loginMessages; $this->initialStateService->provideInitialState('core', 'loginMessages', $messages); diff --git a/core/js/setupchecks.js b/core/js/setupchecks.js index a16f50e122a..f5f48fdf384 100644 --- a/core/js/setupchecks.js +++ b/core/js/setupchecks.js @@ -254,6 +254,12 @@ type: OC.SetupChecks.MESSAGE_TYPE_ERROR }); } + if (!data.isFairUseOfFreePushService) { + messages.push({ + msg: t('core', 'This is the unsupported community build of Nextcloud. Given the size of this instance, performance, reliability and scalability cannot be guaranteed. Push notifications have been disabled to avoid overloading our free service. Learn more about the benefits of Nextcloud Enterprise at nextcloud.com/enterprise.'), + type: OC.SetupChecks.MESSAGE_TYPE_ERROR + }); + } if (data.serverHasInternetConnectionProblems) { messages.push({ msg: t('core', 'This server has no working internet connection: Multiple endpoints could not be reached. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. Establish a connection from this server to the internet to enjoy all features.'), diff --git a/core/js/tests/specs/setupchecksSpec.js b/core/js/tests/specs/setupchecksSpec.js index 82f9c26224c..451c49fb2e9 100644 --- a/core/js/tests/specs/setupchecksSpec.js +++ b/core/js/tests/specs/setupchecksSpec.js @@ -230,6 +230,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: true, memcacheDocs: 'https://docs.nextcloud.com/server/go.php?to=admin-performance', forwardedForHeadersWorking: true, @@ -287,6 +288,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: true, memcacheDocs: 'https://docs.nextcloud.com/server/go.php?to=admin-performance', forwardedForHeadersWorking: true, @@ -345,6 +347,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: true, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -401,6 +404,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: false, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -455,6 +459,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -509,6 +514,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -564,6 +570,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: false, @@ -618,6 +625,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -672,6 +680,7 @@ describe('OC.SetupChecks tests', function() { hasValidTransactionIsolationLevel: true, suggestedOverwriteCliURL: '', isRandomnessSecure: true, + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -747,6 +756,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -802,6 +812,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -857,6 +868,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -912,6 +924,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -967,6 +980,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -1025,6 +1039,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -1080,6 +1095,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -1132,6 +1148,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -1186,6 +1203,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, @@ -1240,6 +1258,7 @@ describe('OC.SetupChecks tests', function() { suggestedOverwriteCliURL: '', isRandomnessSecure: true, securityDocs: 'https://docs.nextcloud.com/myDocs.html', + isFairUseOfFreePushService: true, serverHasInternetConnectionProblems: false, isMemcacheConfigured: true, forwardedForHeadersWorking: true, diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php index fb3a46d5f5d..4e0992053f2 100644 --- a/lib/private/Notification/Manager.php +++ b/lib/private/Notification/Manager.php @@ -27,8 +27,10 @@ declare(strict_types=1); namespace OC\Notification; use OC\AppFramework\Bootstrap\Coordinator; -use OCP\AppFramework\QueryException; -use OCP\ILogger; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUserManager; use OCP\Notification\AlreadyProcessedException; use OCP\Notification\IApp; use OCP\Notification\IDeferrableApp; @@ -37,11 +39,22 @@ use OCP\Notification\IManager; use OCP\Notification\INotification; use OCP\Notification\INotifier; use OCP\RichObjectStrings\IValidator; +use OCP\Support\Subscription\IRegistry; +use Psr\Container\ContainerExceptionInterface; +use Psr\Log\LoggerInterface; class Manager implements IManager { /** @var IValidator */ protected $validator; - /** @var ILogger */ + /** @var IUserManager */ + private $userManager; + /** @var ICache */ + protected $cache; + /** @var ITimeFactory */ + protected $timeFactory; + /** @var IRegistry */ + protected $subscription; + /** @var LoggerInterface */ protected $logger; /** @var Coordinator */ private $coordinator; @@ -64,9 +77,17 @@ class Manager implements IManager { private $parsedRegistrationContext; public function __construct(IValidator $validator, - ILogger $logger, + IUserManager $userManager, + ICacheFactory $cacheFactory, + ITimeFactory $timeFactory, + IRegistry $subscription, + LoggerInterface $logger, Coordinator $coordinator) { $this->validator = $validator; + $this->userManager = $userManager; + $this->cache = $cacheFactory->createDistributed('notifications'); + $this->timeFactory = $timeFactory; + $this->subscription = $subscription; $this->logger = $logger; $this->coordinator = $coordinator; @@ -97,9 +118,10 @@ class Manager implements IManager { */ public function registerNotifier(\Closure $service, \Closure $info) { $infoData = $info(); - $this->logger->logException(new \InvalidArgumentException( + $exception = new \InvalidArgumentException( 'Notifier ' . $infoData['name'] . ' (id: ' . $infoData['id'] . ') is not considered because it is using the old way to register.' - )); + ); + $this->logger->error($exception->getMessage(), ['exception' => $exception]); } /** @@ -121,10 +143,10 @@ class Manager implements IManager { foreach ($this->appClasses as $appClass) { try { - $app = \OC::$server->query($appClass); - } catch (QueryException $e) { - $this->logger->logException($e, [ - 'message' => 'Failed to load notification app class: ' . $appClass, + $app = \OC::$server->get($appClass); + } catch (ContainerExceptionInterface $e) { + $this->logger->error('Failed to load notification app class: ' . $appClass, [ + 'exception' => $e, 'app' => 'notifications', ]); continue; @@ -153,10 +175,10 @@ class Manager implements IManager { $notifierServices = $this->coordinator->getRegistrationContext()->getNotifierServices(); foreach ($notifierServices as $notifierService) { try { - $notifier = \OC::$server->query($notifierService->getService()); - } catch (QueryException $e) { - $this->logger->logException($e, [ - 'message' => 'Failed to load notification notifier class: ' . $notifierService->getService(), + $notifier = \OC::$server->get($notifierService->getService()); + } catch (ContainerExceptionInterface $e) { + $this->logger->error('Failed to load notification notifier class: ' . $notifierService->getService(), [ + 'exception' => $e, 'app' => 'notifications', ]); continue; @@ -181,10 +203,10 @@ class Manager implements IManager { foreach ($this->notifierClasses as $notifierClass) { try { - $notifier = \OC::$server->query($notifierClass); - } catch (QueryException $e) { - $this->logger->logException($e, [ - 'message' => 'Failed to load notification notifier class: ' . $notifierClass, + $notifier = \OC::$server->get($notifierClass); + } catch (ContainerExceptionInterface $e) { + $this->logger->error('Failed to load notification notifier class: ' . $notifierClass, [ + 'exception' => $e, 'app' => 'notifications', ]); continue; @@ -278,6 +300,28 @@ class Manager implements IManager { } /** + * {@inheritDoc} + */ + public function isFairUseOfFreePushService(): bool { + $pushAllowed = $this->cache->get('push_fair_use'); + if ($pushAllowed === null) { + /** + * We want to keep offering our push notification service for free, but large + * users overload our infrastructure. For this reason we have to rate-limit the + * use of push notifications. If you need this feature, consider setting up your + * own push server or using Nextcloud Enterprise. + */ + // TODO Remove time check after 1st March 2022 + $isFairUse = $this->timeFactory->getTime() < 1646089200 + || $this->subscription->delegateHasValidSubscription() + || $this->userManager->countSeenUsers() < 5000; + $pushAllowed = $isFairUse ? 'yes' : 'no'; + $this->cache->set('push_fair_use', $pushAllowed, 3600); + } + return $pushAllowed === 'yes'; + } + + /** * @param INotification $notification * @throws \InvalidArgumentException When the notification is not valid * @since 8.2.0 diff --git a/lib/private/Support/Subscription/Registry.php b/lib/private/Support/Subscription/Registry.php index e64eaac1fa2..1298337acb2 100644 --- a/lib/private/Support/Subscription/Registry.php +++ b/lib/private/Support/Subscription/Registry.php @@ -59,21 +59,17 @@ class Registry implements IRegistry { private $groupManager; /** @var LoggerInterface */ private $logger; - /** @var IManager */ - private $notificationManager; public function __construct(IConfig $config, IServerContainer $container, IUserManager $userManager, IGroupManager $groupManager, - LoggerInterface $logger, - IManager $notificationManager) { + LoggerInterface $logger) { $this->config = $config; $this->container = $container; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->logger = $logger; - $this->notificationManager = $notificationManager; } private function getSubscription(): ?ISubscription { @@ -158,15 +154,16 @@ class Registry implements IRegistry { /** * Indicates if a hard user limit is reached and no new users should be created * + * @param IManager|null $notificationManager * @since 21.0.0 */ - public function delegateIsHardUserLimitReached(): bool { + public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool { $subscription = $this->getSubscription(); if ($subscription instanceof ISubscription && $subscription->hasValidSubscription()) { $userLimitReached = $subscription->isHardUserLimitReached(); - if ($userLimitReached) { - $this->notifyAboutReachedUserLimit(); + if ($userLimitReached && $notificationManager instanceof IManager) { + $this->notifyAboutReachedUserLimit($notificationManager); } return $userLimitReached; } @@ -181,8 +178,8 @@ class Registry implements IRegistry { $hardUserLimit = $this->config->getSystemValue('one-click-instance.user-limit', 50); $userLimitReached = $userCount >= $hardUserLimit; - if ($userLimitReached) { - $this->notifyAboutReachedUserLimit(); + if ($userLimitReached && $notificationManager instanceof IManager) { + $this->notifyAboutReachedUserLimit($notificationManager); } return $userLimitReached; } @@ -216,17 +213,17 @@ class Registry implements IRegistry { return $userCount; } - private function notifyAboutReachedUserLimit() { + private function notifyAboutReachedUserLimit(IManager $notificationManager) { $admins = $this->groupManager->get('admin')->getUsers(); foreach ($admins as $admin) { - $notification = $this->notificationManager->createNotification(); + $notification = $notificationManager->createNotification(); $notification->setApp('core') ->setUser($admin->getUID()) ->setDateTime(new \DateTime()) ->setObject('user_limit_reached', '1') ->setSubject('user_limit_reached'); - $this->notificationManager->notify($notification); + $notificationManager->notify($notification); } $this->logger->warning('The user limit was reached and the new user was not created', ['app' => 'lib']); diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index dbbfc2b53a2..7ed80bc5bc2 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -44,6 +44,7 @@ use OCP\IGroup; use OCP\IUser; use OCP\IUserBackend; use OCP\IUserManager; +use OCP\Notification\IManager; use OCP\Support\Subscription\IRegistry; use OCP\User\Backend\IGetRealUIDBackend; use OCP\User\Backend\ISearchKnownUsersBackend; @@ -379,7 +380,11 @@ class Manager extends PublicEmitter implements IUserManager { */ public function createUser($uid, $password) { // DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency - if (\OC::$server->get(IRegistry::class)->delegateIsHardUserLimitReached()) { + /** @var IRegistry $registry */ + $registry = \OC::$server->get(IRegistry::class); + /** @var IManager $notificationManager */ + $notificationManager = \OC::$server->get(IManager::class); + if ($registry->delegateIsHardUserLimitReached($notificationManager)) { $l = \OC::$server->getL10N('lib'); throw new HintException($l->t('The user limit has been reached and the user was not created.')); } diff --git a/lib/public/Notification/IManager.php b/lib/public/Notification/IManager.php index 66fe78b723e..e2f37176850 100644 --- a/lib/public/Notification/IManager.php +++ b/lib/public/Notification/IManager.php @@ -107,4 +107,16 @@ interface IManager extends IApp, INotifier { * @since 20.0.0 */ public function flush(): void; + + /** + * Whether the server can use the hosted push notification service + * + * We want to keep offering our push notification service for free, but large + * users overload our infrastructure. For this reason we have to rate-limit the + * use of push notifications. If you need this feature, consider setting up your + * own push server or using Nextcloud Enterprise. + * + * @since 23.0.0 + */ + public function isFairUseOfFreePushService(): bool; } diff --git a/lib/public/Support/Subscription/IRegistry.php b/lib/public/Support/Subscription/IRegistry.php index 1082f12ab58..4a34cc91c5e 100644 --- a/lib/public/Support/Subscription/IRegistry.php +++ b/lib/public/Support/Subscription/IRegistry.php @@ -27,6 +27,7 @@ declare(strict_types=1); */ namespace OCP\Support\Subscription; +use OCP\Notification\IManager; use OCP\Support\Subscription\Exception\AlreadyRegisteredException; /** @@ -81,7 +82,8 @@ interface IRegistry { /** * Indicates if a hard user limit is reached and no new users should be created * + * @param IManager|null $notificationManager * @since 21.0.0 */ - public function delegateIsHardUserLimitReached(): bool; + public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool; } diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index fe42b55db15..30a625a612b 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -33,12 +33,14 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; use OCP\IConfig; use OCP\IInitialStateService; +use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use OCP\Notification\IManager; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -86,6 +88,12 @@ class LoginControllerTest extends TestCase { /** @var \OC\Authentication\WebAuthn\Manager|MockObject */ private $webAuthnManager; + /** @var IManager|MockObject */ + private $notificationManager; + + /** @var IL10N|MockObject */ + private $l; + protected function setUp(): void { parent::setUp(); $this->request = $this->createMock(IRequest::class); @@ -101,6 +109,13 @@ class LoginControllerTest extends TestCase { $this->chain = $this->createMock(LoginChain::class); $this->initialStateService = $this->createMock(IInitialStateService::class); $this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class); + $this->notificationManager = $this->createMock(IManager::class); + $this->l = $this->createMock(IL10N::class); + $this->l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); $this->request->method('getRemoteAddress') @@ -124,7 +139,9 @@ class LoginControllerTest extends TestCase { $this->throttler, $this->chain, $this->initialStateService, - $this->webAuthnManager + $this->webAuthnManager, + $this->notificationManager, + $this->l ); } @@ -249,6 +266,7 @@ class LoginControllerTest extends TestCase { [ 'MessageArray1', 'MessageArray2', + 'This community release of Nextcloud is unsupported and instant notifications are unavailable.', ] ); $this->initialStateService->expects($this->at(1)) @@ -278,7 +296,7 @@ class LoginControllerTest extends TestCase { ->expects($this->once()) ->method('isLoggedIn') ->willReturn(false); - $this->initialStateService->expects($this->at(2)) + $this->initialStateService->expects($this->at(4)) ->method('provideInitialState') ->with( 'core', @@ -339,14 +357,14 @@ class LoginControllerTest extends TestCase { ->method('get') ->with('LdapUser') ->willReturn($user); - $this->initialStateService->expects($this->at(0)) + $this->initialStateService->expects($this->at(2)) ->method('provideInitialState') ->with( 'core', 'loginUsername', 'LdapUser' ); - $this->initialStateService->expects($this->at(4)) + $this->initialStateService->expects($this->at(6)) ->method('provideInitialState') ->with( 'core', @@ -386,21 +404,21 @@ class LoginControllerTest extends TestCase { ->method('get') ->with('0') ->willReturn($user); - $this->initialStateService->expects($this->at(1)) + $this->initialStateService->expects($this->at(3)) ->method('provideInitialState') ->with( 'core', 'loginAutocomplete', true ); - $this->initialStateService->expects($this->at(3)) + $this->initialStateService->expects($this->at(5)) ->method('provideInitialState') ->with( 'core', 'loginResetPasswordLink', false ); - $this->initialStateService->expects($this->at(4)) + $this->initialStateService->expects($this->at(6)) ->method('provideInitialState') ->with( 'core', diff --git a/tests/lib/Notification/ManagerTest.php b/tests/lib/Notification/ManagerTest.php index b1201d31c42..400ae3a53ef 100644 --- a/tests/lib/Notification/ManagerTest.php +++ b/tests/lib/Notification/ManagerTest.php @@ -1,4 +1,6 @@ <?php + +declare(strict_types=1); /** * @author Joas Schilling <nickvergessen@owncloud.com> * @@ -25,11 +27,16 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\RegistrationContext; use OC\AppFramework\Bootstrap\ServiceRegistration; use OC\Notification\Manager; -use OCP\ILogger; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUserManager; use OCP\Notification\IManager; use OCP\Notification\INotification; use OCP\RichObjectStrings\IValidator; +use OCP\Support\Subscription\IRegistry; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; class ManagerTest extends TestCase { @@ -38,7 +45,17 @@ class ManagerTest extends TestCase { /** @var IValidator|MockObject */ protected $validator; - /** @var ILogger|MockObject */ + /** @var IUserManager|MockObject */ + protected $userManager; + /** @var ICacheFactory|MockObject */ + protected $cacheFactory; + /** @var ICache|MockObject */ + protected $cache; + /** @var ITimeFactory|MockObject */ + protected $timeFactory; + /** @var IRegistry|MockObject */ + protected $subscriptionRegistry; + /** @var LoggerInterface|MockObject */ protected $logger; /** @var Coordinator|MockObject */ protected $coordinator; @@ -49,14 +66,23 @@ class ManagerTest extends TestCase { parent::setUp(); $this->validator = $this->createMock(IValidator::class); - $this->logger = $this->createMock(ILogger::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->cache = $this->createMock(ICache::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->subscriptionRegistry = $this->createMock(IRegistry::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cacheFactory->method('createDistributed') + ->with('notifications') + ->willReturn($this->cache); $this->registrationContext = $this->createMock(RegistrationContext::class); $this->coordinator = $this->createMock(Coordinator::class); $this->coordinator->method('getRegistrationContext') ->willReturn($this->registrationContext); - $this->manager = new Manager($this->validator, $this->logger, $this->coordinator); + $this->manager = new Manager($this->validator, $this->userManager, $this->cacheFactory, $this->timeFactory, $this->subscriptionRegistry, $this->logger, $this->coordinator); } public function testRegisterApp() { @@ -128,6 +154,10 @@ class ManagerTest extends TestCase { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -156,6 +186,10 @@ class ManagerTest extends TestCase { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -177,6 +211,10 @@ class ManagerTest extends TestCase { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -199,6 +237,10 @@ class ManagerTest extends TestCase { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -211,4 +253,40 @@ class ManagerTest extends TestCase { $manager->getCount($notification); } + + public function dataIsFairUseOfFreePushService() { + return [ + // Before 1st March + [1646089199, true, 4999, true], + [1646089199, true, 5000, true], + [1646089199, false, 4999, true], + [1646089199, false, 5000, true], + + // After 1st March + [1646089200, true, 4999, true], + [1646089200, true, 5000, true], + [1646089200, false, 4999, true], + [1646089200, false, 5000, false], + ]; + } + + /** + * @dataProvider dataIsFairUseOfFreePushService + * @param int $time + * @param bool $hasValidSubscription + * @param int $userCount + * @param bool $isFair + */ + public function testIsFairUseOfFreePushService(int $time, bool $hasValidSubscription, int $userCount, bool $isFair): void { + $this->timeFactory->method('getTime') + ->willReturn($time); + + $this->subscriptionRegistry->method('delegateHasValidSubscription') + ->willReturn($hasValidSubscription); + + $this->userManager->method('countSeenUsers') + ->willReturn($userCount); + + $this->assertSame($isFair, $this->manager->isFairUseOfFreePushService()); + } } diff --git a/tests/lib/Support/Subscription/RegistryTest.php b/tests/lib/Support/Subscription/RegistryTest.php index 5349b041d8b..260232ac95d 100644 --- a/tests/lib/Support/Subscription/RegistryTest.php +++ b/tests/lib/Support/Subscription/RegistryTest.php @@ -75,8 +75,7 @@ class RegistryTest extends TestCase { $this->serverContainer, $this->userManager, $this->groupManager, - $this->logger, - $this->notificationManager + $this->logger ); } @@ -177,7 +176,7 @@ class RegistryTest extends TestCase { ->method('get') ->willReturn($dummyGroup); - $this->assertSame(true, $this->registry->delegateIsHardUserLimitReached()); + $this->assertSame(true, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); } public function testDelegateIsHardUserLimitReachedWithoutSupportApp() { @@ -186,7 +185,7 @@ class RegistryTest extends TestCase { ->with('one-click-instance') ->willReturn(false); - $this->assertSame(false, $this->registry->delegateIsHardUserLimitReached()); + $this->assertSame(false, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); } public function dataForUserLimitCheck() { @@ -237,6 +236,6 @@ class RegistryTest extends TestCase { ->willReturn($dummyGroup); } - $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached()); + $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); } } |