2015-08-31 06:24:37 -04:00
< ? php
2019-12-03 13:57:53 -05:00
2019-04-10 08:12:10 -04:00
declare ( strict_types = 1 );
2015-08-31 06:24:37 -04:00
/**
2024-05-23 03:26:56 -04: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 06:24:37 -04:00
*/
namespace OC\Notification ;
2021-04-14 02:45:07 -04:00
use OC\AppFramework\Bootstrap\Coordinator ;
2021-10-20 04:29:45 -04:00
use OCP\ICache ;
use OCP\ICacheFactory ;
use OCP\IUserManager ;
2019-04-10 08:45:33 -04:00
use OCP\Notification\AlreadyProcessedException ;
2016-01-14 08:35:24 -05:00
use OCP\Notification\IApp ;
2020-06-04 08:46:49 -04:00
use OCP\Notification\IDeferrableApp ;
2019-12-09 07:19:45 -05:00
use OCP\Notification\IDismissableNotifier ;
2016-01-14 08:35:24 -05:00
use OCP\Notification\IManager ;
2024-04-10 10:07:16 -04:00
use OCP\Notification\IncompleteNotificationException ;
2024-04-10 11:12:31 -04:00
use OCP\Notification\IncompleteParsedNotificationException ;
2016-01-14 08:35:24 -05:00
use OCP\Notification\INotification ;
use OCP\Notification\INotifier ;
2025-08-04 03:12:57 -04:00
use OCP\Notification\IPreloadableNotifier ;
2025-08-12 05:28:36 -04:00
use OCP\Notification\NotificationPreloadReason ;
2024-04-10 10:44:40 -04:00
use OCP\Notification\UnknownNotificationException ;
2024-09-17 08:37:55 -04:00
use OCP\RichObjectStrings\IRichTextFormatter ;
2016-11-08 09:56:39 -05:00
use OCP\RichObjectStrings\IValidator ;
2021-10-20 04:29:45 -04:00
use OCP\Support\Subscription\IRegistry ;
use Psr\Container\ContainerExceptionInterface ;
use Psr\Log\LoggerInterface ;
2016-01-14 08:35:24 -05:00
2015-08-31 06:24:37 -04:00
class Manager implements IManager {
2021-10-20 04:29:45 -04:00
/** @var ICache */
2023-07-05 13:55:05 -04:00
protected ICache $cache ;
2016-11-08 09:56:39 -05:00
2016-02-14 15:28:22 -05:00
/** @var IApp[] */
2023-07-05 13:55:05 -04:00
protected array $apps ;
2019-04-10 08:45:33 -04:00
/** @var string[] */
2023-07-05 13:55:05 -04:00
protected array $appClasses ;
2015-08-31 06:24:37 -04:00
2019-04-30 06:08:10 -04:00
/** @var INotifier[] */
2023-07-05 13:55:05 -04:00
protected array $notifiers ;
2019-04-10 08:45:33 -04:00
/** @var string[] */
2023-07-05 13:55:05 -04:00
protected array $notifierClasses ;
2015-08-31 06:24:37 -04:00
2018-07-13 04:11:41 -04:00
/** @var bool */
2023-07-05 13:55:05 -04:00
protected bool $preparingPushNotification ;
2020-06-04 08:46:49 -04:00
/** @var bool */
2023-07-05 13:55:05 -04:00
protected bool $deferPushing ;
2021-04-14 02:45:07 -04:00
/** @var bool */
2023-07-05 13:55:05 -04:00
private bool $parsedRegistrationContext ;
2018-07-13 04:11:41 -04:00
2023-07-05 13:55:05 -04:00
public function __construct (
protected IValidator $validator ,
private IUserManager $userManager ,
2023-11-23 04:22:34 -05:00
ICacheFactory $cacheFactory ,
2023-07-05 13:55:05 -04:00
protected IRegistry $subscription ,
protected LoggerInterface $logger ,
private Coordinator $coordinator ,
2024-09-17 08:37:55 -04:00
private IRichTextFormatter $richTextFormatter ,
2023-07-05 13:55:05 -04:00
) {
2021-10-20 04:29:45 -04:00
$this -> cache = $cacheFactory -> createDistributed ( 'notifications' );
2021-04-14 02:45:07 -04:00
2015-09-03 09:24:33 -04:00
$this -> apps = [];
$this -> notifiers = [];
2019-04-10 08:45:33 -04:00
$this -> appClasses = [];
$this -> notifierClasses = [];
2018-07-13 04:11:41 -04:00
$this -> preparingPushNotification = false ;
2020-06-04 08:46:49 -04:00
$this -> deferPushing = false ;
2021-04-14 02:45:07 -04:00
$this -> parsedRegistrationContext = false ;
2015-09-03 09:24:33 -04:00
}
2015-08-31 06:24:37 -04:00
/**
2019-04-10 08:45:33 -04:00
* @ param string $appClass The service must implement IApp , otherwise a
2024-08-23 09:10:27 -04:00
* \InvalidArgumentException is thrown later
2019-07-16 10:58:38 -04:00
* @ since 17.0 . 0
2015-08-31 06:24:37 -04:00
*/
2019-04-10 08:45:33 -04:00
public function registerApp ( string $appClass ) : void {
2025-03-30 01:05:09 -04: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 06:24:37 -04:00
}
/**
2019-07-16 05:36:32 -04:00
* @ param \Closure $service The service must implement INotifier , otherwise a
* \InvalidArgumentException is thrown later
2024-08-23 09:10:27 -04:00
* @ param \Closure $info An array with the keys 'id' and 'name' containing
* the app id and the app name
2019-07-16 05:36:32 -04:00
* @ deprecated 17.0 . 0 use registerNotifierService instead .
* @ since 8.2 . 0 - Parameter $info was added in 9.0 . 0
*/
2023-07-05 13:55:05 -04:00
public function registerNotifier ( \Closure $service , \Closure $info ) : void {
2019-07-16 05:36:32 -04:00
$infoData = $info ();
2021-10-20 04:29:45 -04:00
$exception = new \InvalidArgumentException (
2019-07-16 05:36:32 -04:00
'Notifier ' . $infoData [ 'name' ] . ' (id: ' . $infoData [ 'id' ] . ') is not considered because it is using the old way to register.'
2021-10-20 04:29:45 -04:00
);
$this -> logger -> error ( $exception -> getMessage (), [ 'exception' => $exception ]);
2019-07-16 05:36:32 -04:00
}
/**
* @ param string $notifierService The service must implement INotifier , otherwise a
2024-08-23 09:10:27 -04:00
* \InvalidArgumentException is thrown later
2019-04-10 08:45:33 -04:00
* @ since 17.0 . 0
2015-08-31 06:24:37 -04:00
*/
2019-07-16 05:36:32 -04:00
public function registerNotifierService ( string $notifierService ) : void {
$this -> notifierClasses [] = $notifierService ;
2015-08-31 06:24:37 -04:00
}
/**
* @ return IApp []
*/
2018-07-13 04:13:49 -04:00
protected function getApps () : array {
2019-04-10 08:45:33 -04:00
if ( empty ( $this -> appClasses )) {
2015-08-31 06:24:37 -04:00
return $this -> apps ;
}
2019-04-10 08:45:33 -04:00
foreach ( $this -> appClasses as $appClass ) {
try {
2021-10-20 04:29:45 -04:00
$app = \OC :: $server -> get ( $appClass );
} catch ( ContainerExceptionInterface $e ) {
$this -> logger -> error ( 'Failed to load notification app class: ' . $appClass , [
'exception' => $e ,
2019-04-10 08:45:33 -04:00
'app' => 'notifications' ,
]);
continue ;
}
2015-08-31 06:24:37 -04:00
if ( ! ( $app instanceof IApp )) {
2019-04-10 08:45:33 -04:00
$this -> logger -> error ( 'Notification app class ' . $appClass . ' is not implementing ' . IApp :: class , [
'app' => 'notifications' ,
]);
continue ;
2015-08-31 06:24:37 -04:00
}
2019-04-10 08:45:33 -04:00
2015-08-31 06:24:37 -04:00
$this -> apps [] = $app ;
}
2019-07-16 05:36:32 -04:00
$this -> appClasses = [];
2015-08-31 06:24:37 -04:00
return $this -> apps ;
}
/**
* @ return INotifier []
*/
2019-04-10 08:45:33 -04:00
public function getNotifiers () : array {
2021-04-14 02:45:07 -04:00
if ( ! $this -> parsedRegistrationContext ) {
$notifierServices = $this -> coordinator -> getRegistrationContext () -> getNotifierServices ();
foreach ( $notifierServices as $notifierService ) {
try {
2021-10-20 04:29:45 -04: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 02:45:07 -04: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 08:45:33 -04:00
if ( empty ( $this -> notifierClasses )) {
2015-08-31 06:24:37 -04:00
return $this -> notifiers ;
}
2019-04-10 08:45:33 -04:00
foreach ( $this -> notifierClasses as $notifierClass ) {
try {
2021-10-20 04:29:45 -04:00
$notifier = \OC :: $server -> get ( $notifierClass );
} catch ( ContainerExceptionInterface $e ) {
$this -> logger -> error ( 'Failed to load notification notifier class: ' . $notifierClass , [
'exception' => $e ,
2019-04-10 08:45:33 -04:00
'app' => 'notifications' ,
]);
continue ;
}
2015-08-31 06:24:37 -04:00
if ( ! ( $notifier instanceof INotifier )) {
2019-04-10 08:45:33 -04:00
$this -> logger -> error ( 'Notification notifier class ' . $notifierClass . ' is not implementing ' . INotifier :: class , [
'app' => 'notifications' ,
]);
continue ;
2015-08-31 06:24:37 -04:00
}
2019-04-10 08:45:33 -04:00
2015-08-31 06:24:37 -04:00
$this -> notifiers [] = $notifier ;
}
2019-07-16 05:36:32 -04:00
$this -> notifierClasses = [];
2015-08-31 06:24:37 -04:00
return $this -> notifiers ;
}
/**
* @ return INotification
* @ since 8.2 . 0
*/
2018-07-13 04:13:49 -04:00
public function createNotification () : INotification {
2024-09-17 08:37:55 -04:00
return new Notification ( $this -> validator , $this -> richTextFormatter );
2015-08-31 06:24:37 -04:00
}
2015-09-16 08:48:07 -04:00
/**
* @ return bool
* @ since 8.2 . 0
*/
2018-07-13 04:13:49 -04:00
public function hasNotifiers () : bool {
2025-06-04 00:32:26 -04:00
return ! empty ( $this -> notifiers )
|| ! empty ( $this -> notifierClasses )
|| ( ! $this -> parsedRegistrationContext && ! empty ( $this -> coordinator -> getRegistrationContext () -> getNotifierServices ()));
2015-09-16 08:48:07 -04:00
}
2018-07-13 04:11:41 -04:00
/**
* @ param bool $preparingPushNotification
* @ since 14.0 . 0
*/
2019-04-10 08:12:10 -04:00
public function setPreparingPushNotification ( bool $preparingPushNotification ) : void {
2018-07-13 04:11:41 -04:00
$this -> preparingPushNotification = $preparingPushNotification ;
}
/**
* @ return bool
* @ since 14.0 . 0
*/
public function isPreparingPushNotification () : bool {
return $this -> preparingPushNotification ;
}
2020-06-04 08:46:49 -04: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 01:05:09 -04:00
$apps = array_reverse ( $this -> getApps ());
2020-06-04 08:46:49 -04:00
foreach ( $apps as $app ) {
if ( $app instanceof IDeferrableApp ) {
$app -> defer ();
}
}
return ! $alreadyDeferring ;
}
/**
* @ since 20.0 . 0
*/
public function flush () : void {
2025-03-30 01:05:09 -04:00
$apps = array_reverse ( $this -> getApps ());
2020-06-04 08:46:49 -04:00
foreach ( $apps as $app ) {
if ( ! $app instanceof IDeferrableApp ) {
continue ;
}
try {
$app -> flush ();
} catch ( \InvalidArgumentException $e ) {
}
}
$this -> deferPushing = false ;
}
2021-10-20 04:29:45 -04: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 04:12:30 -04:00
* use of push notifications . If you need this feature , consider using Nextcloud Enterprise .
2021-10-20 04:29:45 -04:00
*/
2022-11-18 08:44:41 -05:00
$isFairUse = $this -> subscription -> delegateHasValidSubscription () || $this -> userManager -> countSeenUsers () < 1000 ;
2021-10-20 04:29:45 -04:00
$pushAllowed = $isFairUse ? 'yes' : 'no' ;
$this -> cache -> set ( 'push_fair_use' , $pushAllowed , 3600 );
}
return $pushAllowed === 'yes' ;
}
2015-08-31 06:24:37 -04:00
/**
2024-04-10 10:07:16 -04:00
* { @ inheritDoc }
2015-08-31 06:24:37 -04:00
*/
2019-04-10 08:12:10 -04:00
public function notify ( INotification $notification ) : void {
2015-08-31 06:24:37 -04:00
if ( ! $notification -> isValid ()) {
2024-04-10 10:07:16 -04:00
throw new IncompleteNotificationException ( 'The given notification is invalid' );
2015-08-31 06:24:37 -04:00
}
$apps = $this -> getApps ();
foreach ( $apps as $app ) {
2015-09-01 04:24:21 -04:00
try {
$app -> notify ( $notification );
2024-04-10 10:07:16 -04:00
} catch ( IncompleteNotificationException ) {
2015-09-01 04:24:21 -04:00
} catch ( \InvalidArgumentException $e ) {
2024-04-10 10:07:16 -04: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 04:24:21 -04:00
}
2015-08-31 06:24:37 -04:00
}
}
2019-04-10 08:45:33 -04: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 06:24:37 -04:00
/**
2024-04-10 10:44:40 -04:00
* { @ inheritDoc }
2015-08-31 06:24:37 -04:00
*/
2019-04-10 08:12:10 -04:00
public function prepare ( INotification $notification , string $languageCode ) : INotification {
2015-08-31 06:24:37 -04:00
$notifiers = $this -> getNotifiers ();
foreach ( $notifiers as $notifier ) {
try {
2015-09-02 07:09:46 -04:00
$notification = $notifier -> prepare ( $notification , $languageCode );
2019-04-10 08:45:33 -04:00
} catch ( AlreadyProcessedException $e ) {
$this -> markProcessed ( $notification );
2024-04-10 11:26:52 -04:00
throw $e ;
2024-04-10 10:44:40 -04: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 04:24:21 -04:00
}
2015-08-31 06:24:37 -04:00
2022-01-06 10:57:32 -05:00
if ( ! $notification -> isValidParsed ()) {
2024-04-10 11:12:31 -04: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 04:24:21 -04:00
}
2015-08-31 06:24:37 -04:00
}
2022-01-06 10:57:32 -05:00
if ( ! $notification -> isValidParsed ()) {
2024-02-20 10:03:42 -05:00
$this -> logger -> info ( 'Notification was not parsed by any notifier [app: ' . $notification -> getApp () . ', subject: ' . $notification -> getSubject () . ']' );
2024-04-10 11:12:31 -04:00
throw new IncompleteParsedNotificationException ();
2015-09-03 09:24:33 -04:00
}
2024-04-10 12:01:50 -04: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 06:24:37 -04:00
return $notification ;
}
2025-08-12 05:28:36 -04:00
public function preloadDataForParsing (
array $notifications ,
string $languageCode ,
NotificationPreloadReason $reason ,
) : void {
2025-08-04 03:12:57 -04:00
$notifiers = $this -> getNotifiers ();
foreach ( $notifiers as $notifier ) {
if ( ! ( $notifier instanceof IPreloadableNotifier )) {
continue ;
}
2025-08-12 05:28:36 -04:00
$notifier -> preloadDataForParsing ( $notifications , $languageCode , $reason );
2025-08-04 03:12:57 -04:00
}
}
2015-08-31 06:24:37 -04:00
/**
2015-09-01 04:46:32 -04:00
* @ param INotification $notification
2015-08-31 06:24:37 -04:00
*/
2019-04-10 08:12:10 -04:00
public function markProcessed ( INotification $notification ) : void {
2025-03-30 01:05:09 -04:00
$apps = array_reverse ( $this -> getApps ());
2015-08-31 06:24:37 -04:00
foreach ( $apps as $app ) {
2015-09-01 04:46:32 -04:00
$app -> markProcessed ( $notification );
2015-08-31 06:24:37 -04:00
}
}
/**
2015-09-01 04:46:32 -04:00
* @ param INotification $notification
2015-08-31 06:24:37 -04:00
* @ return int
*/
2018-07-13 04:13:49 -04:00
public function getCount ( INotification $notification ) : int {
2025-03-30 01:05:09 -04:00
$apps = array_reverse ( $this -> getApps ());
2015-08-31 06:24:37 -04:00
$count = 0 ;
foreach ( $apps as $app ) {
2015-09-03 09:24:33 -04:00
$count += $app -> getCount ( $notification );
2015-08-31 06:24:37 -04:00
}
return $count ;
}
2019-12-09 07:19:45 -05:00
2024-04-10 10:44:40 -04:00
/**
* { @ inheritDoc }
*/
2019-12-09 07:19:45 -05: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 10:44:40 -04:00
} catch ( UnknownNotificationException ) {
continue ;
2019-12-09 07:19:45 -05:00
} catch ( \InvalidArgumentException $e ) {
2024-04-10 10:44:40 -04: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 07:19:45 -05:00
continue ;
}
}
}
}
2015-08-31 06:24:37 -04:00
}