diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index e18dfb6028bc6..99e731b594cda 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -78,6 +78,7 @@ 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 @@ public function __construct($AppName, 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 @@ public function __construct($AppName, $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 @@ public function check() { '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 928c18998df4e..72c443ed1b695 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -52,6 +52,7 @@ 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 @@ public function __construct( IAppManager $appManager, IFactory $l10nFactory, IL10N $l, - IInitialState $initialStateService + IInitialState $initialStateService, + IManager $manager ) { $this->config = $config; $this->userManager = $userManager; @@ -104,6 +109,7 @@ public function __construct( $this->l10nFactory = $l10nFactory; $this->l = $l; $this->initialStateService = $initialStateService; + $this->manager = $manager; } public function getForm(): TemplateResponse { @@ -160,6 +166,7 @@ public function getForm(): TemplateResponse { '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 = [ @@ -190,6 +197,14 @@ public function getForm(): TemplateResponse { return new TemplateResponse('settings', 'settings/personal/personal.info', $parameters, ''); } + /** + * 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 d258f616229b1..4cd0e4e34d6a2 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -35,6 +35,13 @@ 'vue-settings-personal-info', ]); ?> + +
+
+ t('This community release of Nextcloud is unsupported and instant notifications are unavailable.')); ?> +
+
+
diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index a55b6a2dac189..9ec176a4ae9b8 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -4253,6 +4253,7 @@ + !($notification instanceof INotification) !($notification instanceof INotification) diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 15ec8365c192e..b68f91f986e92 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -46,6 +46,7 @@ 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\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 @@ public function __construct(?string $appName, 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 @@ public function __construct(?string $appName, $this->loginChain = $loginChain; $this->initialStateService = $initialStateService; $this->webAuthnManager = $webAuthnManager; + $this->manager = $manager; + $this->l10n = $l10n; } /** @@ -153,6 +163,12 @@ public function showLoginForm(string $user = null, string $redirect_url = null): } $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 a16f50e122aec..f5f48fdf38490 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 82f9c26224c39..451c49fb2e963 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 fb3a46d5f5d02..4e0992053f2eb 100644 --- a/lib/private/Notification/Manager.php +++ b/lib/private/Notification/Manager.php @@ -27,8 +27,10 @@ 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\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 @@ public function registerApp(string $appClass): void { */ 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 @@ protected function getApps(): array { 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 @@ public function getNotifiers(): array { $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 @@ public function getNotifiers(): array { 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; @@ -277,6 +299,28 @@ public function flush(): void { $this->deferPushing = false; } + /** + * {@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 diff --git a/lib/private/Support/Subscription/Registry.php b/lib/private/Support/Subscription/Registry.php index e64eaac1fa231..1298337acb2ab 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 @@ public function delegateHasExtendedSupport(): bool { /** * 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 @@ public function delegateIsHardUserLimitReached(): bool { $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 @@ private function getUserCount(): int { 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 dbbfc2b53a282..7ed80bc5bc29e 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -44,6 +44,7 @@ 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 @@ public function searchKnownUsersByDisplayName(string $searcher, string $pattern, */ 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 66fe78b723e6e..e2f371768503e 100644 --- a/lib/public/Notification/IManager.php +++ b/lib/public/Notification/IManager.php @@ -107,4 +107,16 @@ public function defer(): bool; * @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 1082f12ab5834..4a34cc91c5ef5 100644 --- a/lib/public/Support/Subscription/IRegistry.php +++ b/lib/public/Support/Subscription/IRegistry.php @@ -27,6 +27,7 @@ */ namespace OCP\Support\Subscription; +use OCP\Notification\IManager; use OCP\Support\Subscription\Exception\AlreadyRegisteredException; /** @@ -81,7 +82,8 @@ public function delegateHasExtendedSupport(): bool; /** * 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 fe42b55db157d..30a625a612b68 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -33,12 +33,14 @@ 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 @@ protected function setUp(): void { $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 @@ protected function setUp(): void { $this->throttler, $this->chain, $this->initialStateService, - $this->webAuthnManager + $this->webAuthnManager, + $this->notificationManager, + $this->l ); } @@ -249,6 +266,7 @@ public function testShowLoginFormWithErrorsInSession() { [ 'MessageArray1', 'MessageArray2', + 'This community release of Nextcloud is unsupported and instant notifications are unavailable.', ] ); $this->initialStateService->expects($this->at(1)) @@ -278,7 +296,7 @@ public function testShowLoginFormForFlowAuth() { ->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 @@ public function testShowLoginFormWithPasswordResetOption($canChangePassword, ->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 @@ public function testShowLoginFormForUserNamed0() { ->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 b1201d31c42fb..400ae3a53ef77 100644 --- a/tests/lib/Notification/ManagerTest.php +++ b/tests/lib/Notification/ManagerTest.php @@ -1,4 +1,6 @@ * @@ -25,11 +27,16 @@ 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 @@ protected function setUp(): void { 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 @@ public function testNotify() { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -156,6 +186,10 @@ public function testNotifyInvalid() { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -177,6 +211,10 @@ public function testMarkProcessed() { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -199,6 +237,10 @@ public function testGetCount() { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->validator, + $this->userManager, + $this->cacheFactory, + $this->timeFactory, + $this->subscriptionRegistry, $this->logger, $this->coordinator, ]) @@ -211,4 +253,40 @@ public function testGetCount() { $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 5349b041d8bdb..260232ac95d0f 100644 --- a/tests/lib/Support/Subscription/RegistryTest.php +++ b/tests/lib/Support/Subscription/RegistryTest.php @@ -75,8 +75,7 @@ protected function setUp(): void { $this->serverContainer, $this->userManager, $this->groupManager, - $this->logger, - $this->notificationManager + $this->logger ); } @@ -177,7 +176,7 @@ public function testDelegateIsHardUserLimitReached() { ->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 @@ public function testDelegateIsHardUserLimitReachedWithoutSupportApp() { ->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 @@ public function testDelegateIsHardUserLimitReachedWithoutSupportAppAndUserCount( ->willReturn($dummyGroup); } - $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached()); + $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached($this->notificationManager)); } }