Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+5 -5
View File
@@ -13,21 +13,21 @@ namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* API Notification Model
*/
class _Api extends \IPS\Notification\Inline
class Api extends Inline
{
/**
* Get sent time
*
* @return int
*/
public function get_sent()
public function get_sent(): int
{
return $this->_data['sent_time'];
}
@@ -37,7 +37,7 @@ class _Api extends \IPS\Notification\Inline
*
* @return int
*/
public function get_updated()
public function get_updated(): int
{
return $this->_data['updated_time'];
}
@@ -47,7 +47,7 @@ class _Api extends \IPS\Notification\Inline
*
* @return string
*/
public function get_app()
public function get_app(): string
{
return $this->_data['notification_app'];
}
+60 -48
View File
@@ -11,33 +11,45 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\DateTime;
use IPS\Http\Url;
use IPS\Member;
use IPS\Patterns\ActiveRecord;
use LogicException;
use OutOfRangeException;
use RuntimeException;
use function defined;
use function get_class;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Inline Notification Model
*/
class _Inline extends \IPS\Patterns\ActiveRecord
class Inline extends ActiveRecord
{
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
protected static array $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_notifications';
public static ?string $databaseTable = 'core_notifications';
/**
* Set Default Values
*
* @return void
*/
public function setDefaultValues()
public function setDefaultValues() : void
{
$this->sent_time = time();
$this->updated_time = time();
@@ -46,40 +58,40 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Get sent time
*
* @return \IPS\DateTime
* @return DateTime
*/
public function get_sent_time()
public function get_sent_time(): DateTime
{
return \IPS\DateTime::ts( $this->_data['sent_time'] );
return DateTime::ts( $this->_data['sent_time'] );
}
/**
* Get updated time
*
* @return \IPS\DateTime
* @return DateTime
*/
public function get_updated_time()
public function get_updated_time(): DateTime
{
return \IPS\DateTime::ts( $this->_data['updated_time'] );
return DateTime::ts( $this->_data['updated_time'] );
}
/**
* Get member
*
* @return \IPS\Member
* @return Member
*/
public function get_member()
public function get_member(): Member
{
return \IPS\Member::load( $this->_data['member'] );
return Member::load( $this->_data['member'] );
}
/**
* Set member
*
* @param |IPS\Member $member The member
* @return void
* @param Member $member
* @return void
*/
public function set_member( \IPS\Member $member )
public function set_member( Member $member ) : void
{
$this->_data['member'] = $member->member_id;
}
@@ -89,7 +101,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
*
* @return array
*/
public function get_member_data()
public function get_member_data(): array
{
return json_decode( $this->_data['member_data'], TRUE );
}
@@ -100,7 +112,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
* @param mixed $data Member data
* @return void
*/
public function set_member_data( $data )
public function set_member_data( mixed $data ) : void
{
$this->_data['member_data'] = $data ? json_encode( $data ) : NULL;
}
@@ -110,7 +122,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
*
* @return object|NULL
*/
public function get_item()
public function get_item(): ?object
{
if ( $this->_data['item_class'] and $this->_data['item_id'] )
{
@@ -122,7 +134,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
return $class::load( $this->_data['item_id'] );
}
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
return NULL;
}
@@ -133,13 +145,13 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Set item
*
* @param object $item The item
* @param object $item The item
* @return void
*/
public function set_item( $item )
public function set_item( object $item ) : void
{
$idColumn = $item::$databaseColumnId;
$this->_data['item_class'] = \get_class( $item );
$this->_data['item_class'] = get_class( $item );
$this->_data['item_id'] = $item->$idColumn;
}
@@ -148,7 +160,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
*
* @return object|NULL
*/
public function get_item_sub()
public function get_item_sub(): ?object
{
if ( $this->_data['item_sub_class'] and $this->_data['item_sub_id'] )
{
@@ -160,7 +172,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
return $class::load( $this->_data['item_sub_id'] );
}
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException $e )
{
return NULL;
}
@@ -171,20 +183,20 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Get application
*
* @return \IPS\Application
* @return Application
*/
public function get_notification_app()
public function get_notification_app(): Application
{
return \IPS\Application::load( $this->_data['notification_app'] );
return Application::load( $this->_data['notification_app'] );
}
/**
* Set application
*
* @param mixed $data Member data
* @param Application $app
* @return void
*/
public function set_notification_app( \IPS\Application $app )
public function set_notification_app( Application $app ) : void
{
$this->_data['notification_app'] = $app->directory;
}
@@ -194,7 +206,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
*
* @return array
*/
public function get_extra()
public function get_extra(): array
{
return $this->_data['extra'] ? json_decode( $this->_data['extra'], TRUE ) : array();
}
@@ -205,7 +217,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
* @param mixed $data Member data
* @return void
*/
public function set_extra( $data )
public function set_extra( mixed $data ) : void
{
$this->_data['extra'] = $data ? json_encode( $data ) : NULL;
}
@@ -213,9 +225,9 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Save Changed Columns
*
* @return void
* @return void
*/
public function save()
public function save(): void
{
parent::save();
$this->member->recountNotifications();
@@ -224,11 +236,11 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Get data from extension
*
* @param bool $htmlEscape TRUE to escape HTML
* @param bool $htmlEscape TRUE to escape HTML
* @return array
* @throws \RuntimeException
* @throws RuntimeException
*/
public function getData( $htmlEscape = TRUE )
public function getData( bool $htmlEscape = TRUE ): array
{
$method = "parse_{$this->notification_key}";
@@ -246,13 +258,13 @@ class _Inline extends \IPS\Patterns\ActiveRecord
return $return;
}
}
throw new \RuntimeException;
throw new RuntimeException;
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse string notificationType The type (key) of notification sent
* @apiresponse string notificationApp The app that processes this type of notification
@@ -267,13 +279,13 @@ class _Inline extends \IPS\Patterns\ActiveRecord
* @apiresponse datetime|null readDate Date and time the notification was read by the user
* @apiresponse array notificationData Array of additional data relevant to this specific notification
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
public function apiOutput( Member $authorizedMember = NULL ): array
{
try
{
$ourData = $this->getData();
}
catch( \LogicException | \RuntimeException $e )
catch( LogicException | RuntimeException $e )
{
$ourData = array();
}
@@ -289,17 +301,17 @@ class _Inline extends \IPS\Patterns\ActiveRecord
'itemSub' => ( $this->item_sub AND method_exists( $this->item_sub, 'apiOutput' ) ) ? $this->item_sub->apiOutput( $authorizedMember ) : NULL,
'sentDate' => $this->sent_time->rfc3339(),
'updatedDate' => $this->updated_time->rfc3339(),
'readDate' => $this->read_time ? \IPS\DateTime::ts( $this->read_time )->rfc3339() : NULL,
'readDate' => $this->read_time ? DateTime::ts( $this->read_time )->rfc3339() : NULL,
'notificationData' => array_map( function( $val ) {
if( $val instanceof \IPS\Http\Url )
if( $val instanceof Url )
{
return (string) $val;
}
elseif( $val instanceof \IPS\Member )
elseif( $val instanceof Member )
{
return $val->apiOutput();
}
elseif( $val instanceof \IPS\DateTime )
elseif( $val instanceof DateTime )
{
return $val->rfc3339();
}
+242 -209
View File
@@ -11,26 +11,54 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Base64Url\Base64Url;
use DateInterval;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Custom;
use IPS\Http\Request\Exception;
use IPS\Http\Url;
use IPS\Http\Url\Internal;
use IPS\Member\Device;
use IPS\Node\Model;
use IPS\Notification\Inline;
use IPS\Notification\Recipients;
use IPS\Patterns\ActiveRecord;
use IPS\Platform\Bridge;
use Minishlink\WebPush\Encryption;
use Minishlink\WebPush\Utils;
use Minishlink\WebPush\VAPID;
use OutOfRangeException;
use UnderflowException;
use function array_keys;
use function count;
use function defined;
use function function_exists;
use function get_class;
use function in_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Notification Class
*/
class _Notification
class Notification
{
/**
* @brief Default Configuration
*/
protected static $defaultConfiguration = NULL;
protected static ?array $defaultConfiguration = NULL;
/**
* @brief Cache of vapid headers to save processing time
*/
protected static $vapidHeaderCache = array();
protected static array $vapidHeaderCache = array();
/**
* @brief Pre-defined web push TTL values
@@ -51,7 +79,7 @@ class _Notification
/**
* @brief Allow notifications to be programatically silenced
*/
protected static $silenced = FALSE;
protected static bool $silenced = FALSE;
/**
* Silence notifications programatically
@@ -59,7 +87,7 @@ class _Notification
*
* @return void
*/
public static function silence()
public static function silence() : void
{
static::$silenced = TRUE;
}
@@ -69,7 +97,7 @@ class _Notification
*
* @return void
*/
public static function unsilence()
public static function unsilence() : void
{
static::$silenced = FALSE;
}
@@ -81,12 +109,13 @@ class _Notification
*/
public static function canUseWebPush(): bool
{
if ( \IPS\IN_DEV )
if ( Application::appIsEnabled('cloud') or IN_DEV )
{
/* Cloud is clearly the superior premium platform, so it already has GMP */
return TRUE;
}
return \function_exists('gmp_init');
return function_exists('gmp_init');
}
/**
@@ -96,23 +125,23 @@ class _Notification
*/
public static function webPushEnabled(): bool
{
return static::canUseWebPush() and ( ( isset( \IPS\Settings::i()->vapid_public_key ) && !empty( \IPS\Settings::i()->vapid_public_key ) &&
isset( \IPS\Settings::i()->vapid_private_key ) && !empty( \IPS\Settings::i()->vapid_private_key ) ) );
return static::canUseWebPush() and ( ( isset( Settings::i()->vapid_public_key ) && !empty( Settings::i()->vapid_public_key ) &&
isset( Settings::i()->vapid_private_key ) && !empty( Settings::i()->vapid_private_key ) ) );
}
/**
* Get default configuration
*
* @return array
* @return array|null
*/
public static function defaultConfiguration()
public static function defaultConfiguration(): ?array
{
if ( static::$defaultConfiguration === NULL )
{
/* Get data from the actual extensions */
$extensionDefaults = array();
$notificationGroups = [];
foreach( \IPS\Application::allExtensions( 'core', 'Notifications' ) as $group => $class )
foreach(Application::allExtensions( 'core', 'Notifications' ) as $group => $class )
{
if ( method_exists( $class, 'configurationOptions' ) )
{
@@ -150,7 +179,7 @@ class _Notification
}
/* Combine that with what the admin has set */
static::$defaultConfiguration = iterator_to_array( \IPS\Db::i()->select( '*', 'core_notification_defaults' )->setKeyField('notification_key') );
static::$defaultConfiguration = iterator_to_array( Db::i()->select( '*', 'core_notification_defaults' )->setKeyField('notification_key') );
foreach ( $extensionDefaults as $key => $data )
{
if ( !isset( static::$defaultConfiguration[ $key ] ) )
@@ -158,7 +187,7 @@ class _Notification
/* If parent group already has defaults stored, use those */
if( isset( $notificationGroups[ $key ] ) AND isset( static::$defaultConfiguration[ $notificationGroups[ $key ] ] ) )
{
\IPS\Db::i()->insert( 'core_notification_defaults', array(
Db::i()->insert( 'core_notification_defaults', array(
'notification_key' => $key,
'default' => implode( ',', static::$defaultConfiguration[ $notificationGroups[ $key ] ]['default'] ),
'disabled' => implode( ',', static::$defaultConfiguration[ $notificationGroups[ $key ] ]['disabled'] )
@@ -169,7 +198,7 @@ class _Notification
else
{
/* Row isn't in DB, add it */
\IPS\Db::i()->insert( 'core_notification_defaults', array(
Db::i()->insert( 'core_notification_defaults', array(
'notification_key' => $key,
'default' => implode( ',', $data['default'] ),
'disabled' => implode( ',', $data['disabled'] )
@@ -181,7 +210,7 @@ class _Notification
else
{
static::$defaultConfiguration[ $key ]['default'] = array_filter( explode( ',', static::$defaultConfiguration[ $key ]['default'] ) );
static::$defaultConfiguration[ $key ]['disabled'] = array_filter( array_merge( $extensionDefaults[ $key ]['disabled'], explode( ',', static::$defaultConfiguration[ $key ]['disabled'] ) ) );
static::$defaultConfiguration[ $key ]['disabled'] = array_filter( array_merge( $data['disabled'], explode( ',', static::$defaultConfiguration[ $key ]['disabled'] ) ) );
}
}
}
@@ -192,38 +221,18 @@ class _Notification
/**
* Get available options for an extension
*
* @param \IPS\Member|NULL $member The member or NULL to get all
* @param object $extension The noticiations extension
* @return void
* @param Member|null $member The member or NULL to get all
* @param object $extension The notifications extension
* @return array
*/
public static function availableOptions( $member, $extension )
public static function availableOptions( ?Member $member, object $extension ) : array
{
/* First of all, get the settings from the extension, maintaining backwards compatibility with older extensions */
if ( method_exists( $extension, 'configurationOptions' ) )
{
$settings = $extension::configurationOptions( $member );
}
else
{
$configuration = $extension->getConfiguration( $member );
$settings = array();
foreach ( $configuration as $key => $details )
{
$settings[ $key ] = array(
'type' => 'standard',
'notificationTypes' => array( $key ),
'title' => "notifications__{$key}",
'showTitle' => TRUE,
'description' => NULL,
);
}
}
$settings = $extension::configurationOptions( $member );
/* Now loop through each of those and get the value for each */
if ( $member )
{
$defaultConfiguration = \IPS\Notification::defaultConfiguration();
$defaultConfiguration = Notification::defaultConfiguration();
$memberConfiguration = $member->notificationsConfiguration();
$finalSettings = array();
foreach ( $settings as $key => $details )
@@ -232,14 +241,14 @@ class _Notification
{
$options = array();
$availableTypes = array('inline');
if ( static::webPushEnabled() && \count( $member->getPwaAuths() ) )
if ( static::webPushEnabled() && count( $member->getPwaAuths() ) )
{
$availableTypes[] = 'push';
}
$availableTypes[] = 'email';
foreach ( $availableTypes as $type )
{
if ( !\in_array( $type, $defaultConfiguration[ $key ]['disabled'] ) )//if ( !\in_array( $type, $defaultConfiguration[ $key ]['disabled'] ) and ( $type !== 'push' or !\in_array( 'inline', $defaultConfiguration[ $key ]['disabled'] ) ) )
if ( !in_array( $type, $defaultConfiguration[ $key ]['disabled'] ) )//if ( !\in_array( $type, $defaultConfiguration[ $key ]['disabled'] ) and ( $type !== 'push' or !\in_array( 'inline', $defaultConfiguration[ $key ]['disabled'] ) ) )
{
$enabled = array();
$haveMemberPreferences = FALSE;
@@ -261,13 +270,13 @@ class _Notification
$options[ $type ] = array(
'title' => "member_notifications_{$type}",
'value' => \in_array( $type, $enabled ),
'value' => in_array( $type, $enabled ),
'editable' => $defaultConfiguration[ $key ]['editable']
);
}
}
if ( \count( $options ) or ( isset( $details['extra'] ) and \count( $details['extra'] ) ) )
if ( count( $options ) or ( isset( $details['extra'] ) and count( $details['extra'] ) ) )
{
$details['options'] = $options;
$finalSettings[ $key ] = $details;
@@ -291,11 +300,11 @@ class _Notification
/**
* Get the notification categories to show to a member
*
* @param \IPS\Member $member The member
* @param array $extensions The notification extensions (passed so we don't have to load it more than once)
* @param Member $member The member
* @param array $extensions The notification extensions (passed so we don't have to load it more than once)
* @return array
*/
public static function membersOptionCategories( \IPS\Member $member, $extensions )
public static function membersOptionCategories( Member $member, array $extensions ): array
{
$categories = array();
foreach( $extensions as $group => $extension )
@@ -316,7 +325,7 @@ class _Notification
{
if ( $extraDetails['value'] )
{
$categories[ $group ][ $extraKey ] = array( 'title' => \IPS\Member::loggedIn()->language()->get( $extraDetails['title'] ), 'icon' => $extraDetails['icon'], 'description' => isset( $extraDetails['description'] ) ? $extraDetails['description'] : NULL );
$categories[ $group ][ $extraKey ] = array( 'title' => Member::loggedIn()->language()->get( $extraDetails['title'] ), 'icon' => $extraDetails['icon'], 'description' => $extraDetails['description'] ?? NULL );
}
}
}
@@ -333,17 +342,17 @@ class _Notification
{
continue 2;
}
$icon = 'bell-o';
$icon = 'bell';
break;
case 'push':
$icon = 'mobile';
break;
case 'email':
$icon = 'envelope-o';
$icon = 'envelope';
break;
}
$categories[ $group ][ $type ] = array( 'title' => \IPS\Member::loggedIn()->language()->get( 'member_notifications_' . $type ), 'icon' => $icon );
$categories[ $group ][ $type ] = array( 'title' => Member::loggedIn()->language()->get( 'member_notifications_' . $type ), 'icon' => $icon );
}
}
}
@@ -357,21 +366,21 @@ class _Notification
/**
* Get the form for editing a member's notification preferences for a given notification extension
*
* @param \IPS\Member $member The member
* @param object $extension The notification extension to edit preferences for
* @return \IPS\Helpers\Form|NULL|TRUE
* @param Member $member The member
* @param object $extension The notification extension to edit preferences for
* @return Form|NULL|TRUE
*/
public static function membersTypeForm( \IPS\Member $member, $extension )
public static function membersTypeForm( Member $member, object $extension ): Form|bool|null
{
$form = NULL;
$formIsEditable = FALSE;
if ( $options = \IPS\Notification::availableOptions( $member, $extension ) )
if ( $options = Notification::availableOptions( $member, $extension ) )
{
$form = new \IPS\Helpers\Form;
$form = new Form;
foreach ( $options as $key => $details )
{
if ( $details['type'] === 'separator' and \IPS\Dispatcher::i()->controllerLocation === 'front' )
if ( $details['type'] === 'separator' and Dispatcher::i()->controllerLocation === 'front' )
{
$form->addSeparator();
}
@@ -388,7 +397,7 @@ class _Notification
{
if ( !$formIsEditable )
{
if ( isset( $details['extra'] ) and \count( $details['extra'] ) )
if ( isset( $details['extra'] ) and count( $details['extra'] ) )
{
$formIsEditable = TRUE;
}
@@ -405,9 +414,9 @@ class _Notification
}
}
$form->add( new \IPS\Helpers\Form\Custom( "notifications_{$key}", NULL, TRUE, array(
$form->add( new Custom( "notifications_{$key}", NULL, TRUE, array(
'rowHtml' => function( $field ) use ( $details, $options ) {
return \IPS\Theme::i()->getTemplate( 'members', 'core', 'global' )->notificationsSettingsRow( $field, $details );
return Theme::i()->getTemplate( 'members', 'core', 'global' )->notificationsSettingsRow( $field, $details );
}
) ) );
}
@@ -429,7 +438,7 @@ class _Notification
$value['inline'] = 'inline';
$value['push'] = 'push';
}
elseif ( \in_array( $k, array( 'inline', 'push', 'email' ) ) and $v )
elseif ( in_array( $k, array( 'inline', 'push', 'email' ) ) and $v )
{
$value[ $k ] = $k;
}
@@ -438,7 +447,7 @@ class _Notification
foreach ( $details['notificationTypes'] as $notificationKey )
{
\IPS\Db::i()->insert( 'core_notification_preferences', array(
Db::i()->insert( 'core_notification_preferences', array(
'member_id' => $member->member_id,
'notification_key' => $notificationKey,
'preference' => implode( ',', $value )
@@ -477,23 +486,23 @@ class _Notification
/**
* @brief Application
*/
protected $app;
protected Application $app;
/**
* @brief Notification key
*/
protected $key;
protected ?string $key = NULL;
/**
* @brief Email template key
* @note Typically this is "notification__{key}"
*/
protected $emailKey;
protected ?string $emailKey = NULL;
/**
* @brief Item
*/
protected $item;
protected ?object $item;
/**
* @brief An \IPS\Notification\Recipients object which contains \IPS\Member objects and replacements to use for that member in the notification content.
@@ -502,48 +511,48 @@ class _Notification
$notification->recipients->attach( $member2, array( 'foo' => 'baz' ) );
* @endcode
*/
public $recipients;
public Recipients $recipients;
/**
* @brief Data for notification emails
*/
protected $emailParams = array();
protected array $emailParams = array();
/**
* @brief Extra data to save with inline notifications
*/
protected $inlineExtra = array();
protected array $inlineExtra = array();
/**
* @brief Unsubscribe Type
*/
public $unsubscribeType = 'notification';
public string $unsubscribeType = 'notification';
/**
* @brief Allow merging of notifications
*/
protected $allowMerging = TRUE;
protected bool $allowMerging = TRUE;
/**
* Constructor
*
* @param \IPS\Application $app The application the notification belongs to
* @param string $key Notification key
* @param object|NULL $item The thing the notification is about
* @param array $emailParams Data for notification emails
* @param array $inlineExtra Extra data to save with inline notifications. Use sparingly: only in cases where it is not possible to obtain the same data later. Will be merged for duplicate notifications.
* @param bool $allowMerging Allow two identical notification types to be merged
* @param string|NULL $emailKey Custom email template to use, or NULL to use default
* @param Application $app The application the notification belongs to
* @param string $key Notification key
* @param object|null $item The thing the notification is about
* @param array $emailParams Data for notification emails
* @param array|null $inlineExtra Extra data to save with inline notifications. Use sparingly: only in cases where it is not possible to obtain the same data later. Will be merged for duplicate notifications.
* @param bool $allowMerging Allow two identical notification types to be merged
* @param string|null $emailKey Custom email template to use, or NULL to use default
* @return void
*/
public function __construct( \IPS\Application $app, $key, $item=NULL, $emailParams=array(), $inlineExtra=array(), $allowMerging=TRUE, $emailKey=NULL )
public function __construct( Application $app, string $key, object $item=NULL, array $emailParams=array(), ?array $inlineExtra=array(), bool $allowMerging=TRUE, string $emailKey=NULL )
{
$this->app = $app;
$this->key = $key;
$this->item = $item;
$this->recipients = new \IPS\Notification\Recipients;
$this->recipients = new Recipients;
$this->emailParams = $emailParams;
$this->inlineExtra = $inlineExtra;
$this->inlineExtra = $inlineExtra ?: array();
$this->allowMerging = $allowMerging;
$this->emailKey = ( $emailKey === NULL ) ? 'notification_' . $this->key : $emailKey;
}
@@ -551,10 +560,10 @@ class _Notification
/**
* Send Notification
*
* @param array $sentTo Members who have already received a notification and how (same format as the return value) to prevent duplicates
* @param array $sentTo Members who have already received a notification and how (same format as the return value) to prevent duplicates
* @return array The members that were notified and how they were notified
*/
public function send( $sentTo = array() )
public function send( array $sentTo = array() ) : array
{
if ( static::$silenced === TRUE )
{
@@ -579,7 +588,7 @@ class _Notification
$membersForNotifications[ $member->member_id ] = $member;
}
if( \count( $membersForNotifications ) )
if( count( $membersForNotifications ) )
{
/* Fill in any that may not have customized their preferences */
foreach( $membersForNotifications as $member )
@@ -594,7 +603,7 @@ class _Notification
$preferenceSet = array();
foreach (
\IPS\Db::i()->select(
Db::i()->select(
'd.*, p.preference, p.member_id',
array( 'core_notification_defaults', 'd' )
)->join(
@@ -603,7 +612,7 @@ class _Notification
)
as $row
) {
if( !\in_array( $row['notification_key'], $preferenceSet ) )
if( !in_array( $row['notification_key'], $preferenceSet ) )
{
foreach( $membersForNotifications as $member )
{
@@ -634,9 +643,9 @@ class _Notification
{
/* Permission check */
$item = $this->item;
if ( $item instanceof \IPS\Content\Item )
if ( $item instanceof Item )
{
$application = \IPS\Application::load( $item::$application );
$application = Application::load( $item::$application );
if ( !$application->canAccess( $member ) )
{
continue;
@@ -653,7 +662,7 @@ class _Notification
/* Not ignoring the comment this is about */
foreach( $this->emailParams AS $param )
{
if ( $param instanceof \IPS\Content\Comment OR $param instanceof \IPS\Content\Review )
if ( $param instanceof Comment )
{
if ( $member->isIgnoring( $param->author(), 'topics' ) )
{
@@ -661,7 +670,7 @@ class _Notification
}
}
if ( $param instanceof \IPS\Member )
if ( $param instanceof Member)
{
if ( $member->isIgnoring( $param, 'topics' ) )
{
@@ -692,13 +701,13 @@ class _Notification
}
/* They want to receive an email (we don't send until the end once we've collated all the emails to send) */
if ( isset( $notificationPreferences[ $keyToCheck ] ) AND \in_array( 'email', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !\in_array( 'email', $sentTo[ $member->member_id ] ) ) )
if ( isset( $notificationPreferences[ $keyToCheck ] ) AND in_array( 'email', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !in_array( 'email', $sentTo[ $member->member_id ] ) ) )
{
$language = $member->language()->id;
if ( !isset( $emails[ $language ] ) )
{
$email = \IPS\Email::buildFromTemplate( $this->app->directory, $this->emailKey, $this->emailParams, \IPS\Email::TYPE_LIST );
$email = Email::buildFromTemplate( $this->app->directory, $this->emailKey, $this->emailParams, Email::TYPE_LIST );
if ( $info )
{
@@ -714,7 +723,6 @@ class _Notification
$unsubscribeBlurb = NULL;
$unfollowLink = NULL;
$listUnsubscribeLink = null;
$okToEmail = TRUE;
if ( $info )
@@ -723,11 +731,12 @@ class _Notification
{
if ( $info['follow_app'] === 'core' and $info['follow_area'] === 'member' )
{
$thingsBeingFollowed[ $info['follow_app'] ][ $info['follow_area'] ][ $info['follow_rel_id'] ] = \IPS\Member::load( $info['follow_rel_id'] );
$thingsBeingFollowed[ $info['follow_app'] ][ $info['follow_area'] ][ $info['follow_rel_id'] ] = Member::load( $info['follow_rel_id'] );
}
else
{
$classname = 'IPS\\' . $info['follow_app'] . '\\' . mb_ucfirst( $info['follow_area'] );
/* @var ActiveRecord $classname */
$classname = 'IPS\\' . $info['follow_app'] . '\\' . IPS::mb_ucfirst( $info['follow_area'] );
$thingsBeingFollowed[ $info['follow_app'] ][ $info['follow_area'] ][ $info['follow_rel_id'] ] = $classname::load( $info['follow_rel_id'] );
/* Set some parameters so the best advertisement possible can be loaded later */
@@ -736,11 +745,11 @@ class _Notification
}
$thingBeingFollowed = $thingsBeingFollowed[ $info['follow_app'] ][ $info['follow_area'] ][ $info['follow_rel_id'] ];
if ( $thingBeingFollowed instanceof \IPS\Member )
if ( $thingBeingFollowed instanceof Member)
{
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow_member', FALSE, array( 'htmlsprintf' => array( $thingBeingFollowed->name ) ) );
}
elseif ( $thingBeingFollowed instanceof \IPS\Node\Model )
elseif ( $thingBeingFollowed instanceof Model )
{
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow', FALSE, array( 'htmlsprintf' => array( $member->language()->addToStack( $thingBeingFollowed::$nodeTitle . '_sg' ), $thingBeingFollowed->getTitleForLanguage( $member->language() ) ) ) );
}
@@ -750,11 +759,10 @@ class _Notification
}
$guestKey = md5( $info['follow_app'] . ';' . $info['follow_area'] . ';' . $info['follow_rel_id'] . ';' . $info['follow_member_id'] . ';' . $info['follow_added'] ) . '-' . md5( $member->email . ';' . $member->ip_address . ';' . $member->joined->getTimestamp() );
$unfollowLink = \IPS\Http\Url::internal( "app=core&module=system&controller=notifications&do=unfollowFromEmail&follow_app={$info['follow_app']}&follow_area={$info['follow_area']}&follow_id={$info['follow_rel_id']}&gkey={$guestKey}", 'front' );
$listUnsubscribeLink = $unfollowLink->setQueryString( 'listunsubscribe', 1 );
$unfollowLink = Url::internal( "app=core&module=system&controller=notifications&do=unfollowFromEmail&follow_app={$info['follow_app']}&follow_area={$info['follow_area']}&follow_id={$info['follow_rel_id']}&gkey={$guestKey}", 'front' );
/* If we are tracking email views/clicks, add the tracking info to this URL as the email handler won't be able to */
if( \IPS\Settings::i()->prune_log_emailstats != 0 )
if( Settings::i()->prune_log_emailstats != 0 )
{
$unfollowLink = $unfollowLink->setQueryString( array( 'email' => 1, 'type' => $this->emailKey ) );
}
@@ -772,8 +780,7 @@ class _Notification
$emailRecipients[ $language ][ $member->email ] = array(
'member_name' => $member->name,
'unsubscribe_blurb' => $unsubscribeBlurb,
'unfollow_link' => $unfollowLink,
'list_unsubscribe_link' => $listUnsubscribeLink
'unfollow_link' => $unfollowLink
);
}
@@ -783,77 +790,85 @@ class _Notification
/* They want to receive an inline notification... (ignore for report center which is treated special and the 'inline' notification
preference actually instead controls whether the bubble should be shown on the report center icon at the top or not) */
$notification = NULL;
if ( $this->key != 'report_center' and isset( $notificationPreferences[ $keyToCheck ] ) and \in_array( 'inline', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !\in_array( 'inline', $sentTo[ $member->member_id ] ) ) )
if ( $this->key != 'report_center' and isset( $notificationPreferences[ $keyToCheck ] ) and in_array( 'inline', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !in_array( 'inline', $sentTo[ $member->member_id ] ) ) )
{
if ( $this->item AND $this->allowMerging )
$hasMerged = false;
if ( $this->item and $this->allowMerging )
{
try
{
$item = $this->item;
$idColumn = $item::$databaseColumnId;
$notification = \IPS\Notification\Inline::constructFromData( \IPS\Db::i()->select( '*', 'core_notifications', array( 'notification_key=? AND item_class=? AND item_id=? AND `member`=? AND read_time IS NULL', $this->key, \get_class( $this->item ), $item->$idColumn, $member->member_id ) )->first() );
$notification = Inline::constructFromData( Db::i()->select( '*', 'core_notifications', ['notification_key=? AND item_class=? AND item_id=? AND `member`=? AND read_time IS NULL', $this->key, get_class( $this->item ), $item->$idColumn, $member->member_id] )->first() );
$notification->member = $member;
$notification->updated_time = time();
$notification->extra = array_merge( $notification->extra, $this->inlineExtra );
$notification->save();
continue;
$hasMerged = true;
}
catch ( \UnderflowException $e ) { }
}
$notification = new \IPS\Notification\Inline;
$notification->member = $member;
$notification->notification_app = $this->app;
$notification->notification_key = $this->key;
if ( $this->item )
{
$notification->item = $this->item;
}
$notification->member_data = $info;
foreach( $this->emailParams AS $param )
{
if ( $param instanceof \IPS\Content )
catch ( UnderflowException )
{
$subIdColumn = $param::$databaseColumnId;
$notification->item_sub_class = \get_class( $param );
$notification->item_sub_id = $param->$subIdColumn;
}
}
/*
* If this is a grouped comment or review, set the sent time to the same time as the comment just in case there is a slight delay
*/
if ( ( $param instanceof \IPS\Content\Comment OR $param instanceof \IPS\Content\Review ) && \in_array( $this->key, array( 'new_comment', 'new_review', 'quote', 'new_likes' ) ) )
if ( ! $hasMerged )
{
$notification = new Inline;
$notification->member = $member;
$notification->notification_app = $this->app;
$notification->notification_key = $this->key;
if ( $this->item )
{
$notification->item = $this->item;
}
$notification->member_data = $info;
foreach ( $this->emailParams as $param )
{
if ( $param instanceof Content )
{
if ( $this->key === 'new_likes' and $this->emailParams[1] instanceof \IPS\Member )
$subIdColumn = $param::$databaseColumnId;
$notification->item_sub_class = get_class( $param );
$notification->item_sub_id = $param->$subIdColumn;
/*
* If this is a grouped comment or review, set the sent time to the same time as the comment just in case there is a slight delay
*/
if ( ( $param instanceof Comment ) && in_array( $this->key, ['new_comment', 'new_review', 'quote', 'new_likes'] ) )
{
/* Reset the time to the time of the rep to prevent a slight delay from missing this notification */
try
if ( $this->key === 'new_likes' and $this->emailParams[1] instanceof Member )
{
$where = $param->getReactionWhereClause();
$where[] = array( 'member_id = ?', $this->emailParams[1]->member_id );
$notification->sent_time = \IPS\Db::i()->select( 'rep_date', 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' )->first();
/* Reset the time to the time of the rep to prevent a slight delay from missing this notification */
try
{
$where = $param->getReactionWhereClause();
$where[] = ['member_id = ?', $this->emailParams[1]->member_id];
$notification->sent_time = Db::i()->select( 'rep_date', 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' )->first();
}
catch ( \Exception $ex )
{
}
}
else
{
$notification->sent_time = $param->mapped( 'date' );
}
catch( \Exception $ex ) { }
}
else
{
$notification->sent_time = $param->mapped('date');
}
}
}
$notification->extra = $this->inlineExtra;
$notification->save();
}
$notification->extra = $this->inlineExtra;
$notification->save();
$sentTo[ $member->member_id ][] = 'inline';
$sentTo[$member->member_id][] = 'inline';
}
/* They want to receive a push notification (we don't send until the end once we've collated all the notifications to send) */
if ( ( static::webPushEnabled() && \count( $member->getPwaAuths() ) ) and isset( $notificationPreferences[ $keyToCheck ] ) AND \in_array( 'push', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !\in_array( 'push', $sentTo[ $member->member_id ] ) ) )
if ( ( static::webPushEnabled() && count( $member->getPwaAuths() ) ) and isset( $notificationPreferences[ $keyToCheck ] ) AND in_array( 'push', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !in_array( 'push', $sentTo[ $member->member_id ] ) ) )
{
$language = $member->language();
if ( !isset( $pushNotifications[ $language->id ] ) )
@@ -874,8 +889,8 @@ class _Notification
if ( isset( $data['data']['url'] ) )
{
$url = \IPS\Http\Url::createFromString( $data['data']['url'] );
if ( $url instanceof \IPS\Http\Url\Internal )
$url = Url::createFromString( $data['data']['url'] );
if ( $url instanceof Internal )
{
foreach ( array( 'app', 'module', 'controller', 'id', 'comment', 'review' ) as $k )
{
@@ -907,19 +922,39 @@ class _Notification
$pushRecipients[ $member->member_id ] = $pushNotifications[ $language->id ];
$pushRecipients[ $member->member_id ]['member'] = $member->member_id;
$pushRecipients[ $member->member_id ]['unreadCount'] = $member->notification_cnt;
$pushRecipients[ $member->member_id ]['notificationId'] = $notification ? $notification->id : NULL;
$pushRecipients[ $member->member_id ]['notificationId'] = $notification?->id;
}
$sentTo[ $member->member_id ][] = 'push';
}
}
/* On cloud, push this to the browser */
if ( Bridge::i()->featureIsEnabled( 'realtime' ) )
{
$memberIds = [];
foreach ( $sentTo as $memberId => $data )
{
if ( in_array( 'push', $data ) OR in_array( 'inline', $data ) )
{
$memberIds[] = $memberId;
}
}
if ( count( $memberIds ) )
{
Bridge::i()->publishRealtimeEvent( 'notifications_available', location: array_map( 'intval', $memberIds ), locationAsMember: true );
}
}
/* Send any emails and push notifications */
if ( \count( $pushNotifications ) )
if ( count( $pushNotifications ) )
{
$this->sendPushNotifications( $pushRecipients );
}
if ( \count( $emails ) )
if ( count( $emails ) )
{
$this->sendEmails( $emails, $emailRecipients );
}
@@ -931,20 +966,17 @@ class _Notification
/**
* Send emails
*
* @param array $emails Emails to send
* @param array $emailRecipients Email recipients
* @param array $emails Emails to send
* @param array $emailRecipients Email recipients
* @return void
*/
protected function sendEmails( $emails, $emailRecipients )
protected function sendEmails( array $emails, array $emailRecipients ) : void
{
foreach ( $emails as $languageId => $email )
{
if ( !empty( $emailRecipients[ $languageId ] ) )
{
$email->mergeAndSend( $emailRecipients[ $languageId ], NULL, NULL, array(
'List-Unsubscribe' => '<*|list_unsubscribe_link|*>',
'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click'
), \IPS\Lang::load( $languageId ) );
$email->mergeAndSend( $emailRecipients[ $languageId ], NULL, NULL, array(), Lang::load( $languageId ) );
}
}
}
@@ -952,10 +984,10 @@ class _Notification
/**
* Send push notifications
*
* @param array $pushNotifications Push notifications to send
* @param array $pushNotifications Push notifications to send
* @return void
*/
protected function sendPushNotifications( $pushNotifications )
protected function sendPushNotifications( array $pushNotifications ) : void
{
/* Send PWA notifications */
$pwaPushNotifications = [];
@@ -964,9 +996,9 @@ class _Notification
{
$data = json_encode( array(
'member' => $memberId,
'title' => static::textForPushNotification( $data['title'] ?? \IPS\Settings::i()->board_name ),
'title' => static::textForPushNotification( $data['title'] ?? Settings::i()->board_name ),
'body' => static::textForPushNotification( $data['body'] ),
'url' => (string) $data['data']['url'],
'url' => isset( $data['data']['url'] ) ? (string) $data['data']['url'] : NULL,
'icon' => $data['icon'] ?? NULL,
'image' => $data['image'] ?? NULL,
'tag' => $data['tag'] ?? NULL,
@@ -975,7 +1007,7 @@ class _Notification
'groupedUrl' => isset( $data['data']['groupedUrl'] ) ? (string) $data['data']['groupedUrl'] : NULL
) );
$notificationId = \IPS\Db::i()->insert( 'core_notifications_pwa_queue', array( 'notification_data' => $data, 'expiration' => \IPS\DateTime::ts( time() )->add( new \DateInterval('P1D') )->getTimestamp() ) );
$notificationId = Db::i()->insert( 'core_notifications_pwa_queue', array( 'notification_data' => $data, 'expiration' => DateTime::ts( time() )->add( new DateInterval('P1D') )->getTimestamp() ) );
$pwaPushNotifications[ $memberId ] = [
'member' => $memberId,
@@ -987,8 +1019,8 @@ class _Notification
];
}
$memberIds = \array_keys( $pwaPushNotifications );
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_notifications_pwa_keys', \IPS\Db::i()->in( '`member`', $memberIds ) )->first();
$memberIds = array_keys( $pwaPushNotifications );
$count = Db::i()->select( 'COUNT(*)', 'core_notifications_pwa_keys', Db::i()->in( '`member`', $memberIds ) )->first();
if ( $count )
{
@@ -998,7 +1030,7 @@ class _Notification
}
else
{
\IPS\Task::queue( 'core', 'PWANotifications', $pwaPushNotifications, 2 );
Task::queue( 'core', 'PWANotifications', $pwaPushNotifications, 2 );
}
}
}
@@ -1010,64 +1042,65 @@ class _Notification
*/
public static function generateVapidKeys(): array
{
\IPS\IPS::$PSR0Namespaces['Jose'] = \IPS\ROOT_PATH . '/system/3rd_party/JwtFramework/src';
\IPS\IPS::$PSR0Namespaces['Minishlink'] = \IPS\ROOT_PATH . '/system/3rd_party/Minishlink';
\IPS\IPS::$PSR0Namespaces['Base64Url'] = \IPS\ROOT_PATH .'/system/3rd_party/Base64Url';
\IPS\IPS::$PSR0Namespaces['Brick'] = \IPS\ROOT_PATH . '/system/3rd_party/Brick';
IPS::$PSR0Namespaces['Jose'] = ROOT_PATH . '/system/3rd_party/JwtFramework/src';
IPS::$PSR0Namespaces['Minishlink'] = ROOT_PATH . '/system/3rd_party/Minishlink';
IPS::$PSR0Namespaces['Base64Url'] = ROOT_PATH .'/system/3rd_party/Base64Url';
IPS::$PSR0Namespaces['Brick'] = ROOT_PATH . '/system/3rd_party/Brick';
return \Minishlink\WebPush\VAPID::createVapidKeys();
return VAPID::createVapidKeys();
}
/**
* Send PWA notifications
*
* @param array $data PWA notifications to send
* @param array $data PWA notifications to send
* @return void
*/
public static function sendPWANotifications( $data )
public static function sendPWANotifications( array $data ) : void
{
\IPS\IPS::$PSR0Namespaces['Jose'] = \IPS\ROOT_PATH . '/system/3rd_party/JwtFramework/src';
\IPS\IPS::$PSR0Namespaces['Minishlink'] = \IPS\ROOT_PATH . '/system/3rd_party/Minishlink';
\IPS\IPS::$PSR0Namespaces['Base64Url'] = \IPS\ROOT_PATH .'/system/3rd_party/Base64Url';
\IPS\IPS::$PSR0Namespaces['Brick'] = \IPS\ROOT_PATH . '/system/3rd_party/Brick';
IPS::$PSR0Namespaces['Jose'] = ROOT_PATH . '/system/3rd_party/JwtFramework/src';
IPS::$PSR0Namespaces['Minishlink'] = ROOT_PATH . '/system/3rd_party/Minishlink';
IPS::$PSR0Namespaces['Base64Url'] = ROOT_PATH .'/system/3rd_party/Base64Url';
IPS::$PSR0Namespaces['Brick'] = ROOT_PATH . '/system/3rd_party/Brick';
// Step 1: Validate VAPID details
$vapid = \Minishlink\WebPush\VAPID::validate( array(
'subject' => "mailto:" . \IPS\Settings::i()->email_in,
'publicKey' => \IPS\Settings::i()->vapid_public_key,
'privateKey' => \IPS\Settings::i()->vapid_private_key
$vapid = VAPID::validate( array(
'subject' => "mailto:" . Settings::i()->email_in,
'publicKey' => Settings::i()->vapid_public_key,
'privateKey' => Settings::i()->vapid_private_key
) );
foreach ( \IPS\Db::i()->select( '*', 'core_notifications_pwa_keys', \IPS\Db::i()->in( '`member`', array_keys( $data ) ) ) as $auth )
foreach (Db::i()->select( '*', 'core_notifications_pwa_keys', Db::i()->in( '`member`', array_keys( $data ) ) ) as $auth )
{
/* Check device is logged in */
try
{
$device = \IPS\Member\Device::load( $auth['device'], NULL, [ 'member_id=?', $auth['member'] ] );
$device = Device::load( $auth['device'], NULL, [ 'member_id=?', $auth['member'] ] );
if ( !$device->login_key )
{
/* Is not, continue */
continue;
}
}
catch( \OutOfRangeException $e )
catch( OutOfRangeException $e )
{
/* Device does not exist so delete & skip */
\IPS\Db::i()->delete( 'core_notifications_pwa_keys', array( 'device=?', $auth['device'] ) );
Db::i()->delete( 'core_notifications_pwa_keys', array( 'device=?', $auth['device'] ) );
continue;
}
$endpoint = \IPS\Http\Url::external( $auth['endpoint'] );
$endpoint = Url::external( $auth['endpoint'] );
// Step 2: Content encoding (provided by browser)
$contentEncoding = $auth['encoding'];
// Step 3: Get and pad the payload
$payload = json_encode( $data[ $auth['member'] ]['data'] );
$payload = \Minishlink\WebPush\Encryption::padPayload( $payload, \Minishlink\WebPush\Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH, $contentEncoding );
$payload = Encryption::padPayload( $payload, Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH, $contentEncoding );
// Step 4: Build Vapid headers
$audience = $endpoint->data[ \IPS\Http\Url::COMPONENT_SCHEME ] . "://" . $endpoint->data[ \IPS\Http\Url::COMPONENT_HOST ];
$audience = $endpoint->data[ Url::COMPONENT_SCHEME ] . "://" . $endpoint->data[ Url::COMPONENT_HOST ];
$cacheKey = implode( '#', array( $audience, $contentEncoding, md5(json_encode($vapid)) ) );
if( isset( static::$vapidHeaderCache[ $cacheKey ] ) )
@@ -1076,18 +1109,18 @@ class _Notification
}
else
{
$vapidHeaders = \Minishlink\WebPush\VAPID::getVapidHeaders( $audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding );
$vapidHeaders = VAPID::getVapidHeaders( $audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding );
static::$vapidHeaderCache[ $cacheKey ] = $vapidHeaders;
}
// Step 5: Encrypt the payload with user's keys
$encryptedPayload = \Minishlink\WebPush\Encryption::encrypt( $payload, $auth['p256dh'], $auth['auth'], $contentEncoding );
$encryptedPayload = Encryption::encrypt( $payload, $auth['p256dh'], $auth['auth'], $contentEncoding );
$salt = $encryptedPayload['salt'];
$localPublicKey = $encryptedPayload['localPublicKey'];
$cipherText = $encryptedPayload['cipherText'];
// Step 6: Get the content coding header and prepend it to the content
$encryptionContentCodingHeader = \Minishlink\WebPush\Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding);
$encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding);
$content = $encryptionContentCodingHeader.$cipherText;
// Step 7: Set headers
@@ -1103,11 +1136,11 @@ class _Notification
if ( $contentEncoding === "aesgcm" )
{
$headers['Encryption'] = 'salt=' . \Base64Url\Base64Url::encode($salt);
$headers['Crypto-Key'] = 'dh=' . \Base64Url\Base64Url::encode($localPublicKey) . ';' . $vapidHeaders['Crypto-Key'];
$headers['Encryption'] = 'salt=' . Base64Url::encode($salt);
$headers['Crypto-Key'] = 'dh=' . Base64Url::encode($localPublicKey) . ';' . $vapidHeaders['Crypto-Key'];
}
$headers['Content-Length'] = \Minishlink\WebPush\Utils::safeStrlen($content);
$headers['Content-Length'] = Utils::safeStrlen($content);
$headers['Authorization'] = $vapidHeaders['Authorization'];
try
@@ -1115,17 +1148,17 @@ class _Notification
// Step 8: Send the damn thing
$response = $endpoint->request()->setHeaders( $headers )->post( $content );
}
catch( \IPS\Http\Request\Exception $e )
catch( Exception $e )
{
/* If the request failed (DNS issues, etc.). Log it and move on. */
\IPS\Log::log( $e, 'pwa_notification' );
Log::log( $e, 'pwa_notification' );
continue;
}
// Step 9: Check the response - 404/410 indicates a permanent problem, so delete that key
if( \in_array( $response->httpResponseCode, array( 404, 410 ) ) )
if( in_array( $response->httpResponseCode, array( 404, 410 ) ) )
{
\IPS\Db::i()->delete( 'core_notifications_pwa_keys', array('id = ?', $auth['id'] ) );
Db::i()->delete( 'core_notifications_pwa_keys', array('id = ?', $auth['id'] ) );
}
}
}
@@ -1133,11 +1166,11 @@ class _Notification
/**
* Convert HTML to plaintext for use in notifications
*
* @param string $html HTML Text
* @param string $html HTML Text
* @return string
* @todo Almost certainly will need to make this more thorough
*/
public static function textForPushNotification( $html )
public static function textForPushNotification( string $html ): string
{
return preg_replace( "/\n\s+/", "\n", trim( html_entity_decode( strip_tags( $html ) ) ) );
}
+32 -25
View File
@@ -11,40 +11,47 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use ArrayAccess;
use Countable;
use IPS\Member;
use Iterator;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Data Structure for storing notification recipieints - basically a non-unique SplObjectStorage
*/
class _Recipients implements \Countable, \Iterator, \ArrayAccess
class Recipients implements Countable, Iterator, ArrayAccess
{
/**
* @brief Recipients
*/
protected $recipients = array();
protected array $recipients = array();
/**
* @brief Count
*/
protected $count = 0;
protected int $count = 0;
/**
* @brief Current position
*/
protected $position = 0;
protected int $position = 0;
/**
* [SplObjectStorage] Add a recipient
*
* @param \IPS\Member $member The member object
* @param array|null $followData If the notification is about something being followed, the appropriate row from core_follow
* @param Member $member The member object
* @param array|null $followData If the notification is about something being followed, the appropriate row from core_follow
* @return void
*/
public function attach( \IPS\Member $member, $followData = NULL )
public function attach( Member $member, array $followData = NULL ) : void
{
$this->recipients[] = array( 'member' => $member, 'followData' => $followData );
$this->count++;
@@ -53,10 +60,10 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
/**
* [SplObjectStorage] Remove a recipient
*
* @param \IPS\Member $member The member object
* @param Member $member The member object
* @return void
*/
public function detach( \IPS\Member $member, $followData = NULL )
public function detach( Member $member ) : void
{
foreach ( $this->recipients as $k => $data )
{
@@ -73,7 +80,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return int
*/
public function count()
public function count(): int
{
return $this->count;
}
@@ -81,9 +88,9 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
/**
* [Iterator] Get current
*
* @return \IPS\Member
* @return Member
*/
public function current()
public function current(): Member
{
return $this->recipients[ $this->position ]['member'];
}
@@ -93,7 +100,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return array|null
*/
public function getInfo()
public function getInfo(): ?array
{
return $this->recipients[ $this->position ]['followData'];
}
@@ -103,7 +110,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return int
*/
public function key()
public function key(): int
{
return $this->position;
}
@@ -113,7 +120,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return void
*/
public function next()
public function next() : void
{
$this->position++;
}
@@ -123,7 +130,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return void
*/
public function rewind()
public function rewind() : void
{
$this->position = 0;
}
@@ -133,7 +140,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @return bool
*/
public function valid()
public function valid(): bool
{
return $this->position < $this->count;
}
@@ -144,7 +151,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
* @param mixed $offset The offset
* @return bool
*/
public function offsetExists( $offset )
public function offsetExists( mixed $offset ): bool
{
return $offset < $this->count;
}
@@ -153,9 +160,9 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
* [ArrayAccess] Get offset
*
* @param mixed $offset The offset
* @return \IPS\Member
* @return Member
*/
public function offsetGet( $offset )
public function offsetGet( mixed $offset ): Member
{
return $this->recipients[ $offset ]['member'];
}
@@ -165,9 +172,9 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
*
* @param mixed $offset The offset
* @param mixed $value The value
* @return bool
* @return void
*/
public function offsetSet( $offset, $value )
public function offsetSet( mixed $offset, mixed $value ): void
{
$this->recipients[] = array( 'member' => $value, 'followData' => NULL );
@@ -183,7 +190,7 @@ class _Recipients implements \Countable, \Iterator, \ArrayAccess
* @param mixed $offset The offset
* @return void
*/
public function offsetUnset( $offset )
public function offsetUnset( mixed $offset ) : void
{
if ( isset( $this->recipients[ $offset ] ) )
{
+4 -4
View File
@@ -13,21 +13,21 @@ namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* REST API Notification RESPONSE
*/
class _RestApi extends \IPS\Notification\Inline
class RestApi extends Inline
{
/**
* Get data from extension
*
* @param bool $htmlEscape TRUE to escape HTML
* @param bool $htmlEscape TRUE to escape HTML
*/
public function getData( $htmlEscape = TRUE )
public function getData( bool $htmlEscape = TRUE ): array
{
$methodName = "parse_rest_{$this->notification_key}";
+37 -24
View File
@@ -11,54 +11,67 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\Db;
use IPS\Helpers\Table\Table as TableHelper;
use IPS\Http\Url;
use IPS\Member;
use IPS\Theme;
use LogicException;
use RuntimeException;
use function defined;
use function in_array;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Notifications Table Helper
*/
class _Table extends \IPS\Helpers\Table\Table
class Table extends TableHelper
{
/**
* @brief Sort options
*/
public $sortOptions = array( 'updated_time' );
public array $sortOptions = array( 'updated_time' );
/**
* @brief Rows
*/
protected static $rows = null;
protected static ?array $rows = null;
/**
* @brief WHERE clause
*/
protected $where = array();
protected ?array $where = array();
/**
* Constructor
*
* @param \IPS\Http\Url Base URL
* @param Url|null $url Base URL
* @return void
*/
public function __construct( \IPS\Http\Url $url=NULL )
public function __construct( Url $url=NULL )
{
/* Init */
parent::__construct( $url );
$this->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' ), 'notificationsTable' );
$this->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' ), 'notificationsRows' );
$this->tableTemplate = array( Theme::i()->getTemplate( 'system', 'core', 'front' ), 'notificationsTable' );
$this->rowsTemplate = array( Theme::i()->getTemplate( 'system', 'core', 'front' ), 'notificationsRows' );
}
/**
* Set member
*
* @param \IPS\Member $member The member to filter by
* @param Member $member The member to filter by
* @return void
*/
public function setMember( \IPS\Member $member )
public function setMember( Member $member ) : void
{
$this->where[] = array( '`member`=?', $member->member_id );
}
@@ -66,48 +79,48 @@ class _Table extends \IPS\Helpers\Table\Table
/**
* Get rows
*
* @param array $advancedSearchValues Values from the advanced search form
* @param array|null $advancedSearchValues Values from the advanced search form
* @return array
*/
public function getRows( $advancedSearchValues=NULL )
public function getRows( array $advancedSearchValues=NULL ): array
{
if ( static::$rows === NULL )
{
static::$rows = array();
/* Check sortBy */
$this->sortBy = \in_array( $this->sortBy, $this->sortOptions ) ? $this->sortBy : 'updated_time';
$this->sortBy = in_array( $this->sortBy, $this->sortOptions ) ? $this->sortBy : 'updated_time';
/* What are we sorting by? */
$sortBy = $this->sortBy . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
$sortBy = $this->sortBy . ' ' . ( ( $this->sortDirection and mb_strtolower( $this->sortDirection ) == 'asc' ) ? 'asc' : 'desc' );
/* Specify filter in where clause */
$where = isset( $this->where ) ? \is_array( $this->where ) ? $this->where : array( $this->where ) : array();
$where = isset( $this->where ) ? is_array( $this->where ) ? $this->where : array( $this->where ) : array();
/* Limit applications */
$where[] = array( "notification_app IN('" . implode( "','", array_keys( \IPS\Application::enabledApplications() ) ) . "')" );
$where[] = array( "notification_app IN('" . implode( "','", array_keys( Application::enabledApplications() ) ) . "')" );
if ( $this->filter and isset( $this->filters[ $this->filter ] ) )
{
$where[] = \is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
$where[] = is_array( $this->filters[ $this->filter ] ) ? $this->filters[ $this->filter ] : array( $this->filters[ $this->filter ] );
}
/* Get Count */
$count = \IPS\Db::i()->select( 'COUNT(*) as cnt', 'core_notifications', $where )->first();
$count = Db::i()->select( 'COUNT(*) as cnt', 'core_notifications', $where )->first();
$this->pages = ceil( $count / $this->limit );
/* Get results */
$it = \IPS\Db::i()->select( '*', 'core_notifications', $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ) );
$it = Db::i()->select( '*', 'core_notifications', $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ) );
$rows = iterator_to_array( $it );
foreach( $rows as $index => $row )
{
try
{
$notification = \IPS\Notification\Inline::constructFromData( $row );
$notification = Inline::constructFromData( $row );
static::$rows[ $index ] = array( 'notification' => $notification, 'data' => $notification->getData() );
}
catch( \LogicException | \RuntimeException $e ){}
catch( LogicException | RuntimeException $e ){}
}
}
@@ -121,7 +134,7 @@ class _Table extends \IPS\Helpers\Table\Table
* @param array|NULL $advancedSearchValues Advanced search values
* @return array
*/
public function getHeaders( $advancedSearchValues )
public function getHeaders( array $advancedSearchValues=NULL ): array
{
return array();
}