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

@@ -11,16 +11,36 @@
namespace IPS\core;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Application;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Email;
use IPS\Helpers\Form\FormAbstract;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Patterns\ActiveRecord;
use OutOfRangeException;
use function count;
use function defined;
use function get_class;
use function in_array;
use function method_exists;
use function strlen;
use function substr;
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;
}
/**
* @brief ACP Member Profile: Block
*/
abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
abstract class AdminNotification extends ActiveRecord
{
/**
* @brief Dynamic severity
@@ -91,29 +111,29 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons = array();
protected static array $multitons = array();
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array( 'acpNotifications', 'acpNotificationIds' );
protected array $caches = array( 'acpNotifications', 'acpNotificationIds' );
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'core_acp_notifications';
public static ?string $databaseTable = 'core_acp_notifications';
/**
* Get Number of Notifications
*
* @param \IPS\Member|NULL $member The member viewing, or NULL for currently logged in
* @param Member|NULL $member The member viewing, or NULL for currently logged in
* @param array $severities The severities
* @return int
*/
public static function notificationCount( \IPS\Member $member = NULL, $severities = array( 'dynamic', 'optional', 'normal', 'high', 'critical' ) )
public static function notificationCount( Member $member = NULL, array $severities = array( 'dynamic', 'optional', 'normal', 'high', 'critical' ) ) : int
{
$member = $member ?: \IPS\Member::loggedIn();
$member = $member ?: Member::loggedIn();
$notificationIds = static::notificationIdsForMember( $member );
$return = 0;
@@ -132,7 +152,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
}
else
{
$return += \count( $notificationIds[ $s ] );
$return += count( $notificationIds[ $s ] );
}
}
@@ -142,20 +162,20 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Get Notifications that a particular member can see by severity
*
* @param \IPS\Member|NULL $member The member viewing, or NULL for currently logged in
* @param Member|NULL $member The member viewing, or NULL for currently logged in
* @param array $severities The severities
* @return int
* @return array
*/
public static function notifications( \IPS\Member $member = NULL, $severities = array( 'dynamic', 'optional', 'normal', 'high', 'critical' ) )
public static function notifications( Member $member = NULL, array $severities = array( 'dynamic', 'optional', 'normal', 'high', 'critical' ) ) : array
{
$member = $member ?: \IPS\Member::loggedIn();
$member = $member ?: Member::loggedIn();
$notificationIds = static::notificationIdsForMember( $member );
$return = array();
foreach ( static::allNotifications() as $notification )
{
$severity = $notification->severity();
if ( \in_array( $severity, $severities ) and \in_array( $notification->id, $notificationIds[ $severity ] ) and ( $severity !== static::SEVERITY_DYNAMIC or $notification->dynamicShow( $member ) ) )
if ( in_array( $severity, $severities ) and in_array( $notification->id, $notificationIds[ $severity ] ) and ( $severity !== static::SEVERITY_DYNAMIC or $notification->dynamicShow( $member ) ) )
{
/* Check if this notification should dismiss itself first */
if ( $notification->selfDismiss() )
@@ -174,32 +194,32 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Get Cached IDs of Notifications that a particular member can see by severity
*
* @param \IPS\Member|NULL $member The member
* @return int
* @param Member $member The member
* @return array
*/
protected static function notificationIdsForMember( \IPS\Member $member )
protected static function notificationIdsForMember( Member $member ) : array
{
if ( !isset( \IPS\Data\Store::i()->acpNotificationIds ) or !isset( \IPS\Data\Store::i()->acpNotificationIds[ $member->member_id ] ) )
if ( !isset( Store::i()->acpNotificationIds ) or !isset( Store::i()->acpNotificationIds[ $member->member_id ] ) )
{
/* Init */
$data = isset( \IPS\Data\Store::i()->acpNotificationIds ) ? \IPS\Data\Store::i()->acpNotificationIds : array();
$data = isset( Store::i()->acpNotificationIds ) ? Store::i()->acpNotificationIds : array();
$data[ $member->member_id ] = array( static::SEVERITY_DYNAMIC => array(), static::SEVERITY_OPTIONAL => array(), static::SEVERITY_NORMAL => array(), static::SEVERITY_HIGH => array(), static::SEVERITY_CRITICAL => array() );
/* Get our preferences */
$preferences = iterator_to_array( \IPS\Db::i()->select( '*', 'core_acp_notifications_preferences', array( '`member`=?', $member->member_id ) )->setKeyField('type') );
$preferences = iterator_to_array( Db::i()->select( '*', 'core_acp_notifications_preferences', array( '`member`=?', $member->member_id ) )->setKeyField('type') );
/* Get our dismissals */
$dismissals = array();
foreach ( \IPS\Db::i()->select( '*', 'core_acp_notifcations_dismissals', array( '`member`=?', $member->member_id ) ) as $row )
foreach ( Db::i()->select( '*', 'core_acp_notifcations_dismissals', array( '`member`=?', $member->member_id ) ) as $row )
{
$dismissals[ $row['notification'] ] = $row['time'];
}
/* Loop through them */
foreach ( static::allNotifications( $member ) as $notification )
foreach ( static::allNotifications() as $notification )
{
/* Check we want to see it if it's optional */
$exploded = explode( '\\', \get_class( $notification ) );
$exploded = explode( '\\', get_class( $notification ) );
$key = "{$exploded[1]}_{$exploded[5]}";
$view = isset( $preferences[ $key ] ) ? $preferences[ $key ]['view'] : $notification::defaultValue();
if ( !$view )
@@ -212,7 +232,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
{
if ( $notification->dismissible() === static::DISMISSIBLE_TEMPORARY and $dismissals[ $notification->id ] < ( time() - 86400 ) )
{
\IPS\Db::i()->delete( 'core_acp_notifcations_dismissals', array( 'notification=? AND `member`=?', $notification->id, $member->member_id ) );
Db::i()->delete( 'core_acp_notifcations_dismissals', array( 'notification=? AND `member`=?', $notification->id, $member->member_id ) );
}
elseif ( $notification->dismissible() === static::DISMISSIBLE_UNTIL_RECUR and $dismissals[ $notification->id ] < $notification->sent->getTimestamp() )
{
@@ -231,10 +251,10 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
}
}
\IPS\Data\Store::i()->acpNotificationIds = $data;
Store::i()->acpNotificationIds = $data;
}
return \IPS\Data\Store::i()->acpNotificationIds[ $member->member_id ];
return Store::i()->acpNotificationIds[ $member->member_id ];
}
/**
@@ -242,28 +262,28 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return array
*/
protected static function allNotifications()
protected static function allNotifications() : array
{
if ( !isset( \IPS\Data\Store::i()->acpNotifications ) )
if ( !isset( Store::i()->acpNotifications ) )
{
\IPS\Data\Store::i()->acpNotifications = iterator_to_array( \IPS\Db::i()->select( '*', 'core_acp_notifications', NULL, 'sent DESC' ) );
Store::i()->acpNotifications = iterator_to_array( Db::i()->select( '*', 'core_acp_notifications', NULL, 'sent DESC' ) );
}
$notifications = array();
foreach ( \IPS\Data\Store::i()->acpNotifications as $notification )
foreach ( Store::i()->acpNotifications as $notification )
{
if ( \IPS\Application::appIsEnabled( $notification['app'] ) )
if ( Application::appIsEnabled( $notification['app'] ) AND class_exists( 'IPS\\' . $notification['app'] . '\extensions\core\AdminNotifications\\' . IPS::mb_ucfirst( $notification['ext'] ) ) )
{
$notificationObject = \IPS\core\AdminNotification::constructFromData( $notification );
$notificationObject = static::constructFromData( $notification );
if( $notificationObject )
if ( $notificationObject )
{
$notifications[ $notificationObject->id ] = $notificationObject;
$notifications[$notificationObject->id] = $notificationObject;
}
else
{
/* Remove orphan entry */
\IPS\Db::i()->delete( 'core_acp_notifications', array( 'id=?', $notification['id'] ) );
Db::i()->delete( 'core_acp_notifications', ['id=?', $notification['id']] );
}
}
}
@@ -276,10 +296,21 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
* @param string $app Application key
* @param string $extension Extension key
* @param string|null $extra Any additional information
* @return void
* @param bool $forceRebuild Force a rebuild of the cache; used primarily for sending, not removing
* @return static|null
*/
public static function find( $app, $extension, $extra = NULL )
public static function find( string $app, string $extension, ?string $extra = NULL, bool $forceRebuild=true ) : ?static
{
/* Drop the cache before we search, to ensure that we have the latest data */
if( $forceRebuild )
{
try
{
unset( Store::i()->acpNotifications );
}
catch( OutOfRangeException ){}
}
foreach ( static::allNotifications() as $notification )
{
if ( $notification->app === $app and $notification->ext === $extension and $notification->extra === $extra )
@@ -298,10 +329,11 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
* @param string|null $extra Any additional information which persists if the notification is resent
* @param bool|null $resend If an existing notification exists, it will be bumped / resent
* @param mixed $extraForEmail Any additional information specific to this instance which is used for the email but not saved
* @param bool|\IPS\Member $bypassEmail If TRUE, no email will be sent, regardless of admin preferences - or if a member object, that admin will be skipped. Should only be used if the action is initiated by an admin making an email unnecessary
* @param bool|Member $bypassEmail If TRUE, no email will be sent, regardless of admin preferences - or if a member object, that admin will be skipped. Should only be used if the action is initiated by an admin making an email unnecessary
* @param array $additionalData Any additional data to save to the notification
* @return void
*/
public static function send( $app, $extension, $extra = NULL, $resend = TRUE, $extraForEmail = NULL, $bypassEmail = FALSE )
public static function send( string $app, string $extension, ?string $extra = NULL, ?bool $resend = TRUE, mixed $extraForEmail = NULL, bool|Member $bypassEmail = FALSE, array $additionalData = [] ) : void
{
/* Create or update */
if ( $notification = static::find( $app, $extension, $extra ) )
@@ -314,13 +346,14 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
}
else
{
$classname = 'IPS\\' . $app . '\extensions\core\AdminNotifications\\' . mb_ucfirst( $extension );
$classname = 'IPS\\' . $app . '\extensions\core\AdminNotifications\\' . IPS::mb_ucfirst( $extension );
$notification = new $classname;
$notification->app = $app;
$notification->ext = $extension;
$notification->extra = $extra;
$notification->additionalData = $additionalData;
unset( \IPS\Data\Store::i()->acpNotifications );
unset( Store::i()->acpNotifications );
}
/* Is this a new notification? */
@@ -330,7 +363,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
}
/* Get where clause for email notifications */
$exploded = explode( '\\', \get_class( $notification ) );
$exploded = explode( '\\', get_class( $notification ) );
$key = "{$exploded[1]}_{$exploded[5]}";
$where = array( array( 'type=?', $key ) );
$where[] = $notification->emailWhereClause( $extraForEmail );
@@ -343,12 +376,12 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
{
/* Work out if we need to email this to anyone */
$emailRecipients = array();
foreach ( \IPS\Db::i()->select( '`member`', 'core_acp_notifications_preferences', $where ) as $memberId )
foreach ( Db::i()->select( '`member`', 'core_acp_notifications_preferences', $where ) as $memberId )
{
$member = \IPS\Member::load( $memberId );
$member = Member::load( $memberId );
if ( $member->member_id and $notification->visibleTo( $member ) )
{
if ( !( $bypassEmail instanceof \IPS\Member ) or $bypassEmail->member_id !== $member->member_id )
if ( !( $bypassEmail instanceof Member ) or $bypassEmail->member_id !== $member->member_id )
{
$emailRecipients[] = $member;
}
@@ -356,10 +389,10 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
}
/* And if we do, do it */
if ( \count( $emailRecipients ) )
if ( count( $emailRecipients ) )
{
$email = \IPS\Email::buildFromTemplate( $exploded[1], 'acp_notification_' . $exploded[5], array( $notification, $extraForEmail ), \IPS\Email::TYPE_TRANSACTIONAL );
$email->setUnsubscribe( 'core', 'unsubscribeAcpNotification', array( \get_class( $notification ) ) );
$email = Email::buildFromTemplate( $exploded[1], 'acp_notification_' . $exploded[5], array( $notification, $extraForEmail ), Email::TYPE_TRANSACTIONAL );
$email->setUnsubscribe( 'core', 'unsubscribeAcpNotification', array( get_class( $notification ) ) );
foreach ( $emailRecipients as $member )
{
$email->send( $member );
@@ -374,12 +407,12 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
* @param string $app Application key
* @param string $extension Extension key
* @param string|null $extra Any additional information
* @param \IPS\DateTime|null $newTime If provided, rather than deleting the notification, it will modify it's sent time to the specified time
* @param DateTime|null $newTime If provided, rather than deleting the notification, it will modify it's sent time to the specified time
* @return void
*/
public static function remove( $app, $extension, $extra = NULL, \IPS\DateTime $newTime = NULL )
public static function remove( string $app, string $extension, ?string $extra = NULL, ?DateTime $newTime = NULL ) : void
{
if ( $notification = static::find( $app, $extension, $extra ) )
if ( $notification = static::find( $app, $extension, $extra, false ) )
{
if ( $newTime )
{
@@ -396,42 +429,37 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Construct ActiveRecord from database row
*
* @param array $data Row from database table
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
* @return static|false
* @param array $data Row from database table
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
* @return ActiveRecord
*/
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE ): static|false
public static function constructFromData( array $data, bool $updateMultitonStoreIfExists = TRUE ): ActiveRecord
{
$classname = 'IPS\\' . $data['app'] . '\extensions\core\AdminNotifications\\' . mb_ucfirst( $data['ext'] );
if( !class_exists( $classname ) )
{
return false;
}
$classname = 'IPS\\' . $data['app'] . '\extensions\core\AdminNotifications\\' . IPS::mb_ucfirst( $data['ext'] );
/* Initiate an object */
$obj = new $classname;
$obj->_new = FALSE;
/* Import data */
$databasePrefixLength = \strlen( static::$databasePrefix );
$databasePrefixLength = strlen( static::$databasePrefix );
foreach ( $data as $k => $v )
{
if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
{
$k = \substr( $k, $databasePrefixLength );
$k = substr( $k, $databasePrefixLength );
}
$obj->_data[ $k ] = $v;
}
$obj->changed = array();
/* Init */
if ( method_exists( $obj, 'init' ) )
{
$obj->init();
}
/* Return */
return $obj;
}
@@ -441,43 +469,64 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return void
*/
public function setDefaultValues()
public function setDefaultValues() : void
{
$this->sent = time();
}
/**
* Set Additional Data
*
* @param $data
* @return void
*/
public function set_additionalData( $data ): void
{
$this->_data['additional_data'] = json_encode( $data );
}
/**
* Get Additional Data
*
* @return array
*/
public function get_additionalData(): array
{
return $this->_data['additional_data'] ? json_decode( $this->_data['additional_data'], TRUE ) : [];
}
/**
* Get sent time
*
* @return \IPS\DateTime
* @return DateTime
*/
public function get_sent()
public function get_sent() : DateTime
{
return \IPS\DateTime::ts( $this->_data['sent'] );
return DateTime::ts( $this->_data['sent'] );
}
/**
* @brief Identifier for what to group this notification type with on the settings form
*/
public static $group = 'other';
public static string $group = 'other';
/**
* @brief Priority 1-5 (1 being highest) for this group compared to others
*/
public static $groupPriority = 5;
public static int $groupPriority = 5;
/**
* @brief Priority 1-5 (1 being highest) for this notification type compared to others in the same group
*/
public static $itemPriority = 3;
public static int $itemPriority = 3;
/**
* Can a member access this type of notification?
*
* @param \IPS\Member $member The member
* @param Member $member The member
* @return bool
*/
public static function permissionCheck( \IPS\Member $member )
public static function permissionCheck( Member $member ) : bool
{
return TRUE;
}
@@ -487,18 +536,15 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return string
*/
public static function settingsTitle()
{
return \get_class(); // This is intended to be abstract but PHP 5.6 won't let you have abstract static functions
}
abstract public static function settingsTitle() : string;
/**
* Can a member view this notification?
*
* @param \IPS\Member $member The member
* @param Member $member The member
* @return bool
*/
public function visibleTo( \IPS\Member $member )
public function visibleTo( Member $member ) : bool
{
return static::permissionCheck( $member );
}
@@ -507,10 +553,10 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
* For dynamic notifications: should this show for this member?
*
* @note This is checked every time the notification shows. Should be lightweight.
* @param \IPS\Member $member The member
* @param Member $member The member
* @return bool
*/
public function dynamicShow( \IPS\Member $member )
public function dynamicShow( Member $member ) : bool
{
return FALSE;
}
@@ -521,7 +567,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
* @note This is checked every time the notification shows. Should be lightweight.
* @return bool
*/
public function selfDismiss()
public function selfDismiss() : bool
{
return FALSE;
}
@@ -531,7 +577,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return bool
*/
public static function defaultValue()
public static function defaultValue() : bool
{
return TRUE;
}
@@ -541,7 +587,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return bool
*/
public static function mayBeOptional()
public static function mayBeOptional() : bool
{
return TRUE;
}
@@ -551,7 +597,7 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return bool
*/
public static function mayRecur()
public static function mayRecur() : bool
{
return TRUE;
}
@@ -559,11 +605,11 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Custom per-admin setting for if email shoild be sent for this notification
*
* @param string $key Setting field key
* @param mixed $value Current value
* @return \IPS\Helpers\Form\FormAbstract|NULL
* @param string $key Setting field key
* @param mixed $value Current value
* @return FormAbstract|NULL
*/
public static function customEmailConfigurationSetting( $key, $value )
public static function customEmailConfigurationSetting( string $key, mixed $value ): ?FormAbstract
{
return NULL;
}
@@ -571,10 +617,10 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* WHERE clause to use against core_acp_notifications_preferences for fetching members to email
*
* @param mixed $extraForEmail Any additional information specific to this instance which is used for the email but not saved
* @return bool
* @param mixed $extraForEmail Any additional information specific to this instance which is used for the email but not saved
* @return array
*/
public function emailWhereClause( $extraForEmail )
public function emailWhereClause( mixed $extraForEmail ): array
{
if ( $this->_new )
{
@@ -591,14 +637,14 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @return string
*/
abstract public function title();
abstract public function title() : string;
/**
* Notification Subtitle (no HTML)
*
* @return string
* @return string|null
*/
public function subtitle()
public function subtitle() : ?string
{
return NULL;
}
@@ -606,30 +652,30 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Notification Body (full HTML, must be escaped where necessary)
*
* @return string
* @return string|null
*/
abstract public function body();
abstract public function body() : ?string;
/**
* Severity
*
* @return string
*/
abstract public function severity();
abstract public function severity() : string;
/**
* Dismissible?
*
* @return bool
* @return string
*/
abstract public function dismissible();
abstract public function dismissible() : string;
/**
* Style
*
* @return bool
* @return string
*/
public function style()
public function style() : string
{
switch ( $this->severity() )
{
@@ -645,21 +691,21 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Quick link from popup menu
*
* @return bool
* @return Url|null
*/
public function link()
public function link() : Url|null
{
return \IPS\Http\Url::internal( 'app=core&module=overview&controller=notifications&highlightedId=' . $this->id );
return Url::internal( 'app=core&module=overview&controller=notifications&highlightedId=' . $this->id );
}
/**
* Delete
*
* @return void
* @return void
*/
public function delete()
public function delete(): void
{
\IPS\Db::i()->delete( 'core_acp_notifcations_dismissals', array( 'notification=?', $this->id ) );
Db::i()->delete( 'core_acp_notifcations_dismissals', array( 'notification=?', $this->id ) );
parent::delete();
}
@@ -667,31 +713,31 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
/**
* Dismiss a notification for a member and rebuild the datastore
*
* @param $notificationId
* @param \IPS\Member|NULL $member
* @param int $notificationId
* @param Member|null $member
*
* @return void
*/
public static function dismissNotification( $notificationId, \IPS\Member $member = NULL )
public static function dismissNotification( int $notificationId, ?Member $member = NULL ) : void
{
$member = $member ?: \IPS\Member::loggedIn();
$member = $member ?: Member::loggedIn();
\IPS\Db::i()->insert( 'core_acp_notifcations_dismissals', array(
Db::i()->insert( 'core_acp_notifcations_dismissals', array(
'notification' => $notificationId,
'member' => $member->member_id,
'time' => time()
), TRUE );
if( isset( \IPS\Data\Store::i()->acpNotificationIds ) )
if( isset( Store::i()->acpNotificationIds ) )
{
$notificationCache = \IPS\Data\Store::i()->acpNotificationIds;
$notificationCache = Store::i()->acpNotificationIds;
if( isset( $notificationCache[ $member->member_id ] ) )
{
unset( $notificationCache[ $member->member_id ] );
}
\IPS\Data\Store::i()->acpNotificationIds = $notificationCache;
Store::i()->acpNotificationIds = $notificationCache;
}
}
}