2015-08-31 12:24:37 +02:00
< ? php
2019-12-03 19:57:53 +01:00
2019-04-10 14:12:10 +02:00
declare ( strict_types = 1 );
2015-08-31 12:24:37 +02:00
/**
2024-05-23 09:26:56 +02:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2015-08-31 12:24:37 +02:00
*/
namespace OC\Notification ;
2021-04-14 08:45:07 +02:00
use OC\AppFramework\Bootstrap\Coordinator ;
2021-10-20 10:29:45 +02:00
use OCP\ICache ;
use OCP\ICacheFactory ;
use OCP\IUserManager ;
2019-04-10 14:45:33 +02:00
use OCP\Notification\AlreadyProcessedException ;
2016-01-14 14:35:24 +01:00
use OCP\Notification\IApp ;
2020-06-04 14:46:49 +02:00
use OCP\Notification\IDeferrableApp ;
2019-12-09 13:19:45 +01:00
use OCP\Notification\IDismissableNotifier ;
2016-01-14 14:35:24 +01:00
use OCP\Notification\IManager ;
2024-04-10 16:07:16 +02:00
use OCP\Notification\IncompleteNotificationException ;
2024-04-10 17:12:31 +02:00
use OCP\Notification\IncompleteParsedNotificationException ;
2016-01-14 14:35:24 +01:00
use OCP\Notification\INotification ;
use OCP\Notification\INotifier ;
2024-04-10 16:44:40 +02:00
use OCP\Notification\UnknownNotificationException ;
2024-09-17 14:37:55 +02:00
use OCP\RichObjectStrings\IRichTextFormatter ;
2016-11-08 15:56:39 +01:00
use OCP\RichObjectStrings\IValidator ;
2021-10-20 10:29:45 +02:00
use OCP\Support\Subscription\IRegistry ;
use Psr\Container\ContainerExceptionInterface ;
use Psr\Log\LoggerInterface ;
2016-01-14 14:35:24 +01:00
2015-08-31 12:24:37 +02:00
class Manager implements IManager {
2021-10-20 10:29:45 +02:00
/** @var ICache */
2023-07-05 21:25:05 +03:30
protected ICache $cache ;
2016-11-08 15:56:39 +01:00
2016-02-14 21:28:22 +01:00
/** @var IApp[] */
2023-07-05 21:25:05 +03:30
protected array $apps ;
2019-04-10 14:45:33 +02:00
/** @var string[] */
2023-07-05 21:25:05 +03:30
protected array $appClasses ;
2015-08-31 12:24:37 +02:00
2019-04-30 12:08:10 +02:00
/** @var INotifier[] */
2023-07-05 21:25:05 +03:30
protected array $notifiers ;
2019-04-10 14:45:33 +02:00
/** @var string[] */
2023-07-05 21:25:05 +03:30
protected array $notifierClasses ;
2015-08-31 12:24:37 +02:00
2018-07-13 10:11:41 +02:00
/** @var bool */
2023-07-05 21:25:05 +03:30
protected bool $preparingPushNotification ;
2020-06-04 14:46:49 +02:00
/** @var bool */
2023-07-05 21:25:05 +03:30
protected bool $deferPushing ;
2021-04-14 08:45:07 +02:00
/** @var bool */
2023-07-05 21:25:05 +03:30
private bool $parsedRegistrationContext ;
2018-07-13 10:11:41 +02:00
2023-07-05 21:25:05 +03:30
public function __construct (
protected IValidator $validator ,
private IUserManager $userManager ,
2021-10-20 10:29:45 +02:00
ICacheFactory $cacheFactory ,
2023-07-05 21:25:05 +03:30
protected IRegistry $subscription ,
protected LoggerInterface $logger ,
private Coordinator $coordinator ,
2024-09-17 14:37:55 +02:00
private IRichTextFormatter $richTextFormatter ,
2023-07-05 21:25:05 +03:30
) {
2021-10-20 10:29:45 +02:00
$this -> cache = $cacheFactory -> createDistributed ( 'notifications' );
2021-04-14 08:45:07 +02:00
2015-09-03 15:24:33 +02:00
$this -> apps = [];
$this -> notifiers = [];
2019-04-10 14:45:33 +02:00
$this -> appClasses = [];
$this -> notifierClasses = [];
2018-07-13 10:11:41 +02:00
$this -> preparingPushNotification = false ;
2020-06-04 14:46:49 +02:00
$this -> deferPushing = false ;
2021-04-14 08:45:07 +02:00
$this -> parsedRegistrationContext = false ;
2015-09-03 15:24:33 +02:00
}
2015-08-31 12:24:37 +02:00
/**
2019-04-10 14:45:33 +02:00
* @ param string $appClass The service must implement IApp , otherwise a
2015-08-31 12:24:37 +02:00
* \InvalidArgumentException is thrown later
2019-07-16 16:58:38 +02:00
* @ since 17.0 . 0
2015-08-31 12:24:37 +02:00
*/
2019-04-10 14:45:33 +02:00
public function registerApp ( string $appClass ) : void {
2025-03-30 16:05:09 +11:00
// other apps may want to rely on the 'main' notification app so make it deterministic that
// the 'main' notification app adds it's notifications first and removes it's notifications last
if ( $appClass === \OCA\Notifications\App :: class ) {
// add 'main' notifications app to start of internal list of apps
array_unshift ( $this -> appClasses , $appClass );
} else {
// add app to end of internal list of apps
$this -> appClasses [] = $appClass ;
}
2015-08-31 12:24:37 +02:00
}
/**
2019-07-16 11:36:32 +02:00
* @ param \Closure $service The service must implement INotifier , otherwise a
* \InvalidArgumentException is thrown later
* @ param \Closure $info An array with the keys 'id' and 'name' containing
* the app id and the app name
* @ deprecated 17.0 . 0 use registerNotifierService instead .
* @ since 8.2 . 0 - Parameter $info was added in 9.0 . 0
*/
2023-07-05 21:25:05 +03:30
public function registerNotifier ( \Closure $service , \Closure $info ) : void {
2019-07-16 11:36:32 +02:00
$infoData = $info ();
2021-10-20 10:29:45 +02:00
$exception = new \InvalidArgumentException (
2019-07-16 11:36:32 +02:00
'Notifier ' . $infoData [ 'name' ] . ' (id: ' . $infoData [ 'id' ] . ') is not considered because it is using the old way to register.'
2021-10-20 10:29:45 +02:00
);
$this -> logger -> error ( $exception -> getMessage (), [ 'exception' => $exception ]);
2019-07-16 11:36:32 +02:00
}
/**
* @ param string $notifierService The service must implement INotifier , otherwise a
2015-08-31 12:24:37 +02:00
* \InvalidArgumentException is thrown later
2019-04-10 14:45:33 +02:00
* @ since 17.0 . 0
2015-08-31 12:24:37 +02:00
*/
2019-07-16 11:36:32 +02:00
public function registerNotifierService ( string $notifierService ) : void {
$this -> notifierClasses [] = $notifierService ;
2015-08-31 12:24:37 +02:00
}
/**
* @ return IApp []
*/
2018-07-13 10:13:49 +02:00
protected function getApps () : array {
2019-04-10 14:45:33 +02:00
if ( empty ( $this -> appClasses )) {
2015-08-31 12:24:37 +02:00
return $this -> apps ;
}
2019-04-10 14:45:33 +02:00
foreach ( $this -> appClasses as $appClass ) {
try {
2021-10-20 10:29:45 +02:00
$app = \OC :: $server -> get ( $appClass );
} catch ( ContainerExceptionInterface $e ) {
$this -> logger -> error ( 'Failed to load notification app class: ' . $appClass , [
'exception' => $e ,
2019-04-10 14:45:33 +02:00
'app' => 'notifications' ,
]);
continue ;
}
2015-08-31 12:24:37 +02:00
if ( ! ( $app instanceof IApp )) {
2019-04-10 14:45:33 +02:00
$this -> logger -> error ( 'Notification app class ' . $appClass . ' is not implementing ' . IApp :: class , [
'app' => 'notifications' ,
]);
continue ;
2015-08-31 12:24:37 +02:00
}
2019-04-10 14:45:33 +02:00
2015-08-31 12:24:37 +02:00
$this -> apps [] = $app ;
}
2019-07-16 11:36:32 +02:00
$this -> appClasses = [];
2015-08-31 12:24:37 +02:00
return $this -> apps ;
}
/**
* @ return INotifier []
*/
2019-04-10 14:45:33 +02:00
public function getNotifiers () : array {
2021-04-14 08:45:07 +02:00
if ( ! $this -> parsedRegistrationContext ) {
$notifierServices = $this -> coordinator -> getRegistrationContext () -> getNotifierServices ();
foreach ( $notifierServices as $notifierService ) {
try {
2021-10-20 10:29:45 +02:00
$notifier = \OC :: $server -> get ( $notifierService -> getService ());
} catch ( ContainerExceptionInterface $e ) {
$this -> logger -> error ( 'Failed to load notification notifier class: ' . $notifierService -> getService (), [
'exception' => $e ,
2021-04-14 08:45:07 +02:00
'app' => 'notifications' ,
]);
continue ;
}
if ( ! ( $notifier instanceof INotifier )) {
$this -> logger -> error ( 'Notification notifier class ' . $notifierService -> getService () . ' is not implementing ' . INotifier :: class , [
'app' => 'notifications' ,
]);
continue ;
}
$this -> notifiers [] = $notifier ;
}
$this -> parsedRegistrationContext = true ;
}
2019-04-10 14:45:33 +02:00
if ( empty ( $this -> notifierClasses )) {
2015-08-31 12:24:37 +02:00
return $this -> notifiers ;
}
2019-04-10 14:45:33 +02:00
foreach ( $this -> notifierClasses as $notifierClass ) {
try {
2021-10-20 10:29:45 +02:00
$notifier = \OC :: $server -> get ( $notifierClass );
} catch ( ContainerExceptionInterface $e ) {
$this -> logger -> error ( 'Failed to load notification notifier class: ' . $notifierClass , [
'exception' => $e ,
2019-04-10 14:45:33 +02:00
'app' => 'notifications' ,
]);
continue ;
}
2015-08-31 12:24:37 +02:00
if ( ! ( $notifier instanceof INotifier )) {
2019-04-10 14:45:33 +02:00
$this -> logger -> error ( 'Notification notifier class ' . $notifierClass . ' is not implementing ' . INotifier :: class , [
'app' => 'notifications' ,
]);
continue ;
2015-08-31 12:24:37 +02:00
}
2019-04-10 14:45:33 +02:00
2015-08-31 12:24:37 +02:00
$this -> notifiers [] = $notifier ;
}
2019-07-16 11:36:32 +02:00
$this -> notifierClasses = [];
2015-08-31 12:24:37 +02:00
return $this -> notifiers ;
}
/**
* @ return INotification
* @ since 8.2 . 0
*/
2018-07-13 10:13:49 +02:00
public function createNotification () : INotification {
2024-09-17 14:37:55 +02:00
return new Notification ( $this -> validator , $this -> richTextFormatter );
2015-08-31 12:24:37 +02:00
}
2015-09-16 14:48:07 +02:00
/**
* @ return bool
* @ since 8.2 . 0
*/
2018-07-13 10:13:49 +02:00
public function hasNotifiers () : bool {
2019-04-30 12:08:10 +02:00
return ! empty ( $this -> notifiers ) || ! empty ( $this -> notifierClasses );
2015-09-16 14:48:07 +02:00
}
2018-07-13 10:11:41 +02:00
/**
* @ param bool $preparingPushNotification
* @ since 14.0 . 0
*/
2019-04-10 14:12:10 +02:00
public function setPreparingPushNotification ( bool $preparingPushNotification ) : void {
2018-07-13 10:11:41 +02:00
$this -> preparingPushNotification = $preparingPushNotification ;
}
/**
* @ return bool
* @ since 14.0 . 0
*/
public function isPreparingPushNotification () : bool {
return $this -> preparingPushNotification ;
}
2020-06-04 14:46:49 +02:00
/**
* The calling app should only " flush " when it got returned true on the defer call
* @ return bool
* @ since 20.0 . 0
*/
public function defer () : bool {
$alreadyDeferring = $this -> deferPushing ;
$this -> deferPushing = true ;
2025-03-30 16:05:09 +11:00
$apps = array_reverse ( $this -> getApps ());
2020-06-04 14:46:49 +02:00
foreach ( $apps as $app ) {
if ( $app instanceof IDeferrableApp ) {
$app -> defer ();
}
}
return ! $alreadyDeferring ;
}
/**
* @ since 20.0 . 0
*/
public function flush () : void {
2025-03-30 16:05:09 +11:00
$apps = array_reverse ( $this -> getApps ());
2020-06-04 14:46:49 +02:00
foreach ( $apps as $app ) {
if ( ! $app instanceof IDeferrableApp ) {
continue ;
}
try {
$app -> flush ();
} catch ( \InvalidArgumentException $e ) {
}
}
$this -> deferPushing = false ;
}
2021-10-20 10:29:45 +02:00
/**
* { @ 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
2021-10-27 10:12:30 +02:00
* use of push notifications . If you need this feature , consider using Nextcloud Enterprise .
2021-10-20 10:29:45 +02:00
*/
2022-11-18 14:44:41 +01:00
$isFairUse = $this -> subscription -> delegateHasValidSubscription () || $this -> userManager -> countSeenUsers () < 1000 ;
2021-10-20 10:29:45 +02:00
$pushAllowed = $isFairUse ? 'yes' : 'no' ;
$this -> cache -> set ( 'push_fair_use' , $pushAllowed , 3600 );
}
return $pushAllowed === 'yes' ;
}
2015-08-31 12:24:37 +02:00
/**
2024-04-10 16:07:16 +02:00
* { @ inheritDoc }
2015-08-31 12:24:37 +02:00
*/
2019-04-10 14:12:10 +02:00
public function notify ( INotification $notification ) : void {
2015-08-31 12:24:37 +02:00
if ( ! $notification -> isValid ()) {
2024-04-10 16:07:16 +02:00
throw new IncompleteNotificationException ( 'The given notification is invalid' );
2015-08-31 12:24:37 +02:00
}
$apps = $this -> getApps ();
foreach ( $apps as $app ) {
2015-09-01 10:24:21 +02:00
try {
$app -> notify ( $notification );
2024-04-10 16:07:16 +02:00
} catch ( IncompleteNotificationException ) {
2015-09-01 10:24:21 +02:00
} catch ( \InvalidArgumentException $e ) {
2024-04-10 16:07:16 +02:00
// todo 33.0.0 Log as warning
// todo 39.0.0 Log as error
$this -> logger -> debug ( get_class ( $app ) . '::notify() threw \InvalidArgumentException which is deprecated. Throw \OCP\Notification\IncompleteNotificationException when the notification is incomplete for your app and otherwise handle all \InvalidArgumentException yourself.' );
2015-09-01 10:24:21 +02:00
}
2015-08-31 12:24:37 +02:00
}
}
2019-04-10 14:45:33 +02:00
/**
* Identifier of the notifier , only use [ a - z0 - 9_ ]
*
* @ return string
* @ since 17.0 . 0
*/
public function getID () : string {
return 'core' ;
}
/**
* Human readable name describing the notifier
*
* @ return string
* @ since 17.0 . 0
*/
public function getName () : string {
return 'core' ;
}
2015-08-31 12:24:37 +02:00
/**
2024-04-10 16:44:40 +02:00
* { @ inheritDoc }
2015-08-31 12:24:37 +02:00
*/
2019-04-10 14:12:10 +02:00
public function prepare ( INotification $notification , string $languageCode ) : INotification {
2015-08-31 12:24:37 +02:00
$notifiers = $this -> getNotifiers ();
foreach ( $notifiers as $notifier ) {
try {
2015-09-02 13:09:46 +02:00
$notification = $notifier -> prepare ( $notification , $languageCode );
2019-04-10 14:45:33 +02:00
} catch ( AlreadyProcessedException $e ) {
$this -> markProcessed ( $notification );
2024-04-10 17:26:52 +02:00
throw $e ;
2024-04-10 16:44:40 +02:00
} catch ( UnknownNotificationException ) {
continue ;
} catch ( \InvalidArgumentException $e ) {
// todo 33.0.0 Log as warning
// todo 39.0.0 Log as error
$this -> logger -> debug ( get_class ( $notifier ) . '::prepare() threw \InvalidArgumentException which is deprecated. Throw \OCP\Notification\UnknownNotificationException when the notification is not known to your notifier and otherwise handle all \InvalidArgumentException yourself.' );
continue ;
2015-09-01 10:24:21 +02:00
}
2015-08-31 12:24:37 +02:00
2022-01-06 16:57:32 +01:00
if ( ! $notification -> isValidParsed ()) {
2024-04-10 17:12:31 +02:00
$this -> logger -> info ( 'Notification was claimed to be parsed, but was not fully parsed by ' . get_class ( $notifier ) . ' [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
throw new IncompleteParsedNotificationException ();
2015-09-01 10:24:21 +02:00
}
2015-08-31 12:24:37 +02:00
}
2022-01-06 16:57:32 +01:00
if ( ! $notification -> isValidParsed ()) {
2024-02-20 16:03:42 +01:00
$this -> logger -> info ( 'Notification was not parsed by any notifier [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
2024-04-10 17:12:31 +02:00
throw new IncompleteParsedNotificationException ();
2015-09-03 15:24:33 +02:00
}
2024-04-10 18:01:50 +02:00
$link = $notification -> getLink ();
if ( $link !== '' && ! str_starts_with ( $link , 'http://' ) && ! str_starts_with ( $link , 'https://' )) {
$this -> logger -> warning ( 'Link of notification is not an absolute URL and does not work in mobile and desktop clients [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
}
$icon = $notification -> getIcon ();
if ( $icon !== '' && ! str_starts_with ( $icon , 'http://' ) && ! str_starts_with ( $icon , 'https://' )) {
$this -> logger -> warning ( 'Icon of notification is not an absolute URL and does not work in mobile and desktop clients [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
}
foreach ( $notification -> getParsedActions () as $action ) {
$link = $action -> getLink ();
if ( $link !== '' && ! str_starts_with ( $link , 'http://' ) && ! str_starts_with ( $link , 'https://' )) {
$this -> logger -> warning ( 'Link of action is not an absolute URL and does not work in mobile and desktop clients [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
}
}
2015-08-31 12:24:37 +02:00
return $notification ;
}
/**
2015-09-01 10:46:32 +02:00
* @ param INotification $notification
2015-08-31 12:24:37 +02:00
*/
2019-04-10 14:12:10 +02:00
public function markProcessed ( INotification $notification ) : void {
2025-03-30 16:05:09 +11:00
$apps = array_reverse ( $this -> getApps ());
2015-08-31 12:24:37 +02:00
foreach ( $apps as $app ) {
2015-09-01 10:46:32 +02:00
$app -> markProcessed ( $notification );
2015-08-31 12:24:37 +02:00
}
}
/**
2015-09-01 10:46:32 +02:00
* @ param INotification $notification
2015-08-31 12:24:37 +02:00
* @ return int
*/
2018-07-13 10:13:49 +02:00
public function getCount ( INotification $notification ) : int {
2025-03-30 16:05:09 +11:00
$apps = array_reverse ( $this -> getApps ());
2015-08-31 12:24:37 +02:00
$count = 0 ;
foreach ( $apps as $app ) {
2015-09-03 15:24:33 +02:00
$count += $app -> getCount ( $notification );
2015-08-31 12:24:37 +02:00
}
return $count ;
}
2019-12-09 13:19:45 +01:00
2024-04-10 16:44:40 +02:00
/**
* { @ inheritDoc }
*/
2019-12-09 13:19:45 +01:00
public function dismissNotification ( INotification $notification ) : void {
$notifiers = $this -> getNotifiers ();
foreach ( $notifiers as $notifier ) {
if ( $notifier instanceof IDismissableNotifier ) {
try {
$notifier -> dismissNotification ( $notification );
2024-04-10 16:44:40 +02:00
} catch ( UnknownNotificationException ) {
continue ;
2019-12-09 13:19:45 +01:00
} catch ( \InvalidArgumentException $e ) {
2024-04-10 16:44:40 +02:00
// todo 33.0.0 Log as warning
// todo 39.0.0 Log as error
$this -> logger -> debug ( get_class ( $notifier ) . '::dismissNotification() threw \InvalidArgumentException which is deprecated. Throw \OCP\Notification\UnknownNotificationException when the notification is not known to your notifier and otherwise handle all \InvalidArgumentException yourself.' );
2019-12-09 13:19:45 +01:00
continue ;
}
}
}
}
2015-08-31 12:24:37 +02:00
}