diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 679e85f1f1421..5e79823f4434f 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -4269,6 +4269,7 @@ !($notification instanceof INotification) !($notification instanceof INotification) + @@ -4824,11 +4825,12 @@ $this->createUserFromBackend($uid, $password, $backend) $this->createUserFromBackend($uid, $password, $backend) - + checkPassword checkPassword countUsers createUser + delegateIsHardUserLimitReached getUsersForUserValueCaseInsensitive diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php index fb3a46d5f5d02..f120a4bc791b8 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,26 @@ 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 3e30861f2a4a6..4b7a0b3f91100 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; @@ -373,7 +374,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 $registry */ + $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/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)); } }