You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Manager.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. *
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. * @author Roeland Jago Douma <roeland@famdouma.nl>
  9. *
  10. * @license AGPL-3.0
  11. *
  12. * This code is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License, version 3,
  14. * as published by the Free Software Foundation.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License, version 3,
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>
  23. *
  24. */
  25. namespace OC\Notification;
  26. use OC\AppFramework\Bootstrap\Coordinator;
  27. use OCP\AppFramework\Utility\ITimeFactory;
  28. use OCP\ICache;
  29. use OCP\ICacheFactory;
  30. use OCP\IUserManager;
  31. use OCP\Notification\AlreadyProcessedException;
  32. use OCP\Notification\IApp;
  33. use OCP\Notification\IDeferrableApp;
  34. use OCP\Notification\IDismissableNotifier;
  35. use OCP\Notification\IManager;
  36. use OCP\Notification\INotification;
  37. use OCP\Notification\INotifier;
  38. use OCP\RichObjectStrings\IValidator;
  39. use OCP\Support\Subscription\IRegistry;
  40. use Psr\Container\ContainerExceptionInterface;
  41. use Psr\Log\LoggerInterface;
  42. class Manager implements IManager {
  43. /** @var IValidator */
  44. protected $validator;
  45. /** @var IUserManager */
  46. private $userManager;
  47. /** @var ICache */
  48. protected $cache;
  49. /** @var ITimeFactory */
  50. protected $timeFactory;
  51. /** @var IRegistry */
  52. protected $subscription;
  53. /** @var LoggerInterface */
  54. protected $logger;
  55. /** @var Coordinator */
  56. private $coordinator;
  57. /** @var IApp[] */
  58. protected $apps;
  59. /** @var string[] */
  60. protected $appClasses;
  61. /** @var INotifier[] */
  62. protected $notifiers;
  63. /** @var string[] */
  64. protected $notifierClasses;
  65. /** @var bool */
  66. protected $preparingPushNotification;
  67. /** @var bool */
  68. protected $deferPushing;
  69. /** @var bool */
  70. private $parsedRegistrationContext;
  71. public function __construct(IValidator $validator,
  72. IUserManager $userManager,
  73. ICacheFactory $cacheFactory,
  74. ITimeFactory $timeFactory,
  75. IRegistry $subscription,
  76. LoggerInterface $logger,
  77. Coordinator $coordinator) {
  78. $this->validator = $validator;
  79. $this->userManager = $userManager;
  80. $this->cache = $cacheFactory->createDistributed('notifications');
  81. $this->timeFactory = $timeFactory;
  82. $this->subscription = $subscription;
  83. $this->logger = $logger;
  84. $this->coordinator = $coordinator;
  85. $this->apps = [];
  86. $this->notifiers = [];
  87. $this->appClasses = [];
  88. $this->notifierClasses = [];
  89. $this->preparingPushNotification = false;
  90. $this->deferPushing = false;
  91. $this->parsedRegistrationContext = false;
  92. }
  93. /**
  94. * @param string $appClass The service must implement IApp, otherwise a
  95. * \InvalidArgumentException is thrown later
  96. * @since 17.0.0
  97. */
  98. public function registerApp(string $appClass): void {
  99. $this->appClasses[] = $appClass;
  100. }
  101. /**
  102. * @param \Closure $service The service must implement INotifier, otherwise a
  103. * \InvalidArgumentException is thrown later
  104. * @param \Closure $info An array with the keys 'id' and 'name' containing
  105. * the app id and the app name
  106. * @deprecated 17.0.0 use registerNotifierService instead.
  107. * @since 8.2.0 - Parameter $info was added in 9.0.0
  108. */
  109. public function registerNotifier(\Closure $service, \Closure $info) {
  110. $infoData = $info();
  111. $exception = new \InvalidArgumentException(
  112. 'Notifier ' . $infoData['name'] . ' (id: ' . $infoData['id'] . ') is not considered because it is using the old way to register.'
  113. );
  114. $this->logger->error($exception->getMessage(), ['exception' => $exception]);
  115. }
  116. /**
  117. * @param string $notifierService The service must implement INotifier, otherwise a
  118. * \InvalidArgumentException is thrown later
  119. * @since 17.0.0
  120. */
  121. public function registerNotifierService(string $notifierService): void {
  122. $this->notifierClasses[] = $notifierService;
  123. }
  124. /**
  125. * @return IApp[]
  126. */
  127. protected function getApps(): array {
  128. if (empty($this->appClasses)) {
  129. return $this->apps;
  130. }
  131. foreach ($this->appClasses as $appClass) {
  132. try {
  133. $app = \OC::$server->get($appClass);
  134. } catch (ContainerExceptionInterface $e) {
  135. $this->logger->error('Failed to load notification app class: ' . $appClass, [
  136. 'exception' => $e,
  137. 'app' => 'notifications',
  138. ]);
  139. continue;
  140. }
  141. if (!($app instanceof IApp)) {
  142. $this->logger->error('Notification app class ' . $appClass . ' is not implementing ' . IApp::class, [
  143. 'app' => 'notifications',
  144. ]);
  145. continue;
  146. }
  147. $this->apps[] = $app;
  148. }
  149. $this->appClasses = [];
  150. return $this->apps;
  151. }
  152. /**
  153. * @return INotifier[]
  154. */
  155. public function getNotifiers(): array {
  156. if (!$this->parsedRegistrationContext) {
  157. $notifierServices = $this->coordinator->getRegistrationContext()->getNotifierServices();
  158. foreach ($notifierServices as $notifierService) {
  159. try {
  160. $notifier = \OC::$server->get($notifierService->getService());
  161. } catch (ContainerExceptionInterface $e) {
  162. $this->logger->error('Failed to load notification notifier class: ' . $notifierService->getService(), [
  163. 'exception' => $e,
  164. 'app' => 'notifications',
  165. ]);
  166. continue;
  167. }
  168. if (!($notifier instanceof INotifier)) {
  169. $this->logger->error('Notification notifier class ' . $notifierService->getService() . ' is not implementing ' . INotifier::class, [
  170. 'app' => 'notifications',
  171. ]);
  172. continue;
  173. }
  174. $this->notifiers[] = $notifier;
  175. }
  176. $this->parsedRegistrationContext = true;
  177. }
  178. if (empty($this->notifierClasses)) {
  179. return $this->notifiers;
  180. }
  181. foreach ($this->notifierClasses as $notifierClass) {
  182. try {
  183. $notifier = \OC::$server->get($notifierClass);
  184. } catch (ContainerExceptionInterface $e) {
  185. $this->logger->error('Failed to load notification notifier class: ' . $notifierClass, [
  186. 'exception' => $e,
  187. 'app' => 'notifications',
  188. ]);
  189. continue;
  190. }
  191. if (!($notifier instanceof INotifier)) {
  192. $this->logger->error('Notification notifier class ' . $notifierClass . ' is not implementing ' . INotifier::class, [
  193. 'app' => 'notifications',
  194. ]);
  195. continue;
  196. }
  197. $this->notifiers[] = $notifier;
  198. }
  199. $this->notifierClasses = [];
  200. return $this->notifiers;
  201. }
  202. /**
  203. * @return INotification
  204. * @since 8.2.0
  205. */
  206. public function createNotification(): INotification {
  207. return new Notification($this->validator);
  208. }
  209. /**
  210. * @return bool
  211. * @since 8.2.0
  212. */
  213. public function hasNotifiers(): bool {
  214. return !empty($this->notifiers) || !empty($this->notifierClasses);
  215. }
  216. /**
  217. * @param bool $preparingPushNotification
  218. * @since 14.0.0
  219. */
  220. public function setPreparingPushNotification(bool $preparingPushNotification): void {
  221. $this->preparingPushNotification = $preparingPushNotification;
  222. }
  223. /**
  224. * @return bool
  225. * @since 14.0.0
  226. */
  227. public function isPreparingPushNotification(): bool {
  228. return $this->preparingPushNotification;
  229. }
  230. /**
  231. * The calling app should only "flush" when it got returned true on the defer call
  232. * @return bool
  233. * @since 20.0.0
  234. */
  235. public function defer(): bool {
  236. $alreadyDeferring = $this->deferPushing;
  237. $this->deferPushing = true;
  238. $apps = $this->getApps();
  239. foreach ($apps as $app) {
  240. if ($app instanceof IDeferrableApp) {
  241. $app->defer();
  242. }
  243. }
  244. return !$alreadyDeferring;
  245. }
  246. /**
  247. * @since 20.0.0
  248. */
  249. public function flush(): void {
  250. $apps = $this->getApps();
  251. foreach ($apps as $app) {
  252. if (!$app instanceof IDeferrableApp) {
  253. continue;
  254. }
  255. try {
  256. $app->flush();
  257. } catch (\InvalidArgumentException $e) {
  258. }
  259. }
  260. $this->deferPushing = false;
  261. }
  262. /**
  263. * {@inheritDoc}
  264. */
  265. public function isFairUseOfFreePushService(): bool {
  266. $pushAllowed = $this->cache->get('push_fair_use');
  267. if ($pushAllowed === null) {
  268. /**
  269. * We want to keep offering our push notification service for free, but large
  270. * users overload our infrastructure. For this reason we have to rate-limit the
  271. * use of push notifications. If you need this feature, consider using Nextcloud Enterprise.
  272. */
  273. // TODO Remove time check after 1st March 2022
  274. $isFairUse = $this->timeFactory->getTime() < 1646089200
  275. || $this->subscription->delegateHasValidSubscription()
  276. || $this->userManager->countSeenUsers() < 5000;
  277. $pushAllowed = $isFairUse ? 'yes' : 'no';
  278. $this->cache->set('push_fair_use', $pushAllowed, 3600);
  279. }
  280. return $pushAllowed === 'yes';
  281. }
  282. /**
  283. * @param INotification $notification
  284. * @throws \InvalidArgumentException When the notification is not valid
  285. * @since 8.2.0
  286. */
  287. public function notify(INotification $notification): void {
  288. if (!$notification->isValid()) {
  289. throw new \InvalidArgumentException('The given notification is invalid');
  290. }
  291. $apps = $this->getApps();
  292. foreach ($apps as $app) {
  293. try {
  294. $app->notify($notification);
  295. } catch (\InvalidArgumentException $e) {
  296. }
  297. }
  298. }
  299. /**
  300. * Identifier of the notifier, only use [a-z0-9_]
  301. *
  302. * @return string
  303. * @since 17.0.0
  304. */
  305. public function getID(): string {
  306. return 'core';
  307. }
  308. /**
  309. * Human readable name describing the notifier
  310. *
  311. * @return string
  312. * @since 17.0.0
  313. */
  314. public function getName(): string {
  315. return 'core';
  316. }
  317. /**
  318. * @param INotification $notification
  319. * @param string $languageCode The code of the language that should be used to prepare the notification
  320. * @return INotification
  321. * @throws \InvalidArgumentException When the notification was not prepared by a notifier
  322. * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted
  323. * @since 8.2.0
  324. */
  325. public function prepare(INotification $notification, string $languageCode): INotification {
  326. $notifiers = $this->getNotifiers();
  327. foreach ($notifiers as $notifier) {
  328. try {
  329. $notification = $notifier->prepare($notification, $languageCode);
  330. } catch (\InvalidArgumentException $e) {
  331. continue;
  332. } catch (AlreadyProcessedException $e) {
  333. $this->markProcessed($notification);
  334. throw new \InvalidArgumentException('The given notification has been processed');
  335. }
  336. if (!$notification->isValidParsed()) {
  337. throw new \InvalidArgumentException('The given notification has not been handled');
  338. }
  339. }
  340. if (!$notification->isValidParsed()) {
  341. throw new \InvalidArgumentException('The given notification has not been handled');
  342. }
  343. return $notification;
  344. }
  345. /**
  346. * @param INotification $notification
  347. */
  348. public function markProcessed(INotification $notification): void {
  349. $apps = $this->getApps();
  350. foreach ($apps as $app) {
  351. $app->markProcessed($notification);
  352. }
  353. }
  354. /**
  355. * @param INotification $notification
  356. * @return int
  357. */
  358. public function getCount(INotification $notification): int {
  359. $apps = $this->getApps();
  360. $count = 0;
  361. foreach ($apps as $app) {
  362. $count += $app->getCount($notification);
  363. }
  364. return $count;
  365. }
  366. public function dismissNotification(INotification $notification): void {
  367. $notifiers = $this->getNotifiers();
  368. foreach ($notifiers as $notifier) {
  369. if ($notifier instanceof IDismissableNotifier) {
  370. try {
  371. $notifier->dismissNotification($notification);
  372. } catch (\InvalidArgumentException $e) {
  373. continue;
  374. }
  375. }
  376. }
  377. }
  378. }