Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* @brief Provides notifications results in API-friendly format
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 3 July 2014
*/
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' );
exit;
}
/**
* API Notification Model
*/
class _Api extends \IPS\Notification\Inline
{
/**
* Get sent time
*
* @return int
*/
public function get_sent()
{
return $this->_data['sent_time'];
}
/**
* Get updated time
*
* @return int
*/
public function get_updated()
{
return $this->_data['updated_time'];
}
/**
* Get notification app
*
* @return string
*/
public function get_app()
{
return $this->_data['notification_app'];
}
}
+67 -4
View File
@@ -11,7 +11,7 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -139,7 +139,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
public function set_item( $item )
{
$idColumn = $item::$databaseColumnId;
$this->_data['item_class'] = get_class( $item );
$this->_data['item_class'] = \get_class( $item );
$this->_data['item_id'] = $item->$idColumn;
}
@@ -224,10 +224,11 @@ class _Inline extends \IPS\Patterns\ActiveRecord
/**
* Get data from extension
*
* @param bool $htmlEscape TRUE to escape HTML
* @return array
* @throws \RuntimeException
*/
public function getData()
public function getData( $htmlEscape = TRUE )
{
$method = "parse_{$this->notification_key}";
@@ -235,7 +236,7 @@ class _Inline extends \IPS\Patterns\ActiveRecord
{
if ( method_exists( $class, $method ) )
{
$return = $class->$method( $this );
$return = $class->$method( $this, $htmlEscape );
if ( !isset( $return['unread'] ) )
{
@@ -247,4 +248,66 @@ class _Inline extends \IPS\Patterns\ActiveRecord
}
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
* @return array
* @apiresponse string notificationType The type (key) of notification sent
* @apiresponse string notificationApp The app that processes this type of notification
* @apiresponse string itemClass The class that triggered the notification
* @apiresponse int itemId The ID of the content that triggered the notification
* @apiresponse string subItemClass The sub-class that triggered the notification (e.g. for comments or reviews)
* @apiresponse int subItemId The sub-ID of the content that triggered the notification (e.g. for comments or reviews)
* @apiresponse object|null item A representation of the item being notified about (if supported), or null
* @apiresponse object|null itemSub A representation of the subitem being notified about (if supported), or null
* @apiresponse datetime sentDate Date and time the notification was sent
* @apiresponse datetime updatedDate Date and time the notification was last updated
* @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 )
{
try
{
$ourData = $this->getData();
}
catch( \LogicException $e )
{
$ourData = array();
}
return array(
'notificationType' => $this->notification_key,
'notificationApp' => $this->notification_app->directory,
'itemClass' => $this->item_class,
'itemId' => $this->item_id,
'subItemClass' => $this->item_sub_class,
'subItemId' => $this->item_sub_id,
'item' => ( $this->item AND method_exists( $this->item, 'apiOutput' ) ) ? $this->item->apiOutput( $authorizedMember ) : NULL,
'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,
'notificationData' => array_map( function( $val ) {
if( $val instanceof \IPS\Http\Url )
{
return (string) $val;
}
elseif( $val instanceof \IPS\Member )
{
return $val->apiOutput();
}
elseif( $val instanceof \IPS\DateTime )
{
return $val->rfc3339();
}
else
{
return $val;
}
}, $ourData )
);
}
}
+519 -125
View File
@@ -11,7 +11,7 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -36,149 +36,353 @@ class _Notification
{
if ( static::$defaultConfiguration === NULL )
{
static::$defaultConfiguration = iterator_to_array( \IPS\Db::i()->select( '*', 'core_notification_defaults' )->setKeyField('notification_key') );
/* Get data from the actual extensions */
$extensionDefaults = array();
foreach( \IPS\Application::allExtensions( 'core', 'Notifications' ) as $group => $class )
{
$configuration = $class->getConfiguration( NULL );
if ( !empty( $configuration ) )
if ( method_exists( $class, 'configurationOptions' ) )
{
foreach ( $configuration as $key => $data )
foreach ( $class->configurationOptions( NULL ) as $key => $details )
{
if ( !isset( static::$defaultConfiguration[ $key ] ) )
if ( $details['type'] === 'standard' )
{
/* Row isn't in DB, add it */
\IPS\Db::i()->insert( 'core_notification_defaults', array(
'notification_key' => $key,
'default' => implode( ',', $data['default'] ),
'disabled' => implode( ',', $data['disabled'] )
) );
$extensionDefaults[ $key ] = array(
'default' => $details['default'],
'disabled' => $details['disabled']
);
static::$defaultConfiguration[ $key ] = array_merge( $data, array( 'editable' => TRUE ) );
foreach ( $details['notificationTypes'] as $type )
{
$extensionDefaults[ $type ] = array(
'default' => $details['default'],
'disabled' => $details['disabled']
);
}
}
else
{
static::$defaultConfiguration[ $key ]['default'] = array_filter( explode( ',', static::$defaultConfiguration[ $key ]['default'] ) );
static::$defaultConfiguration[ $key ]['disabled'] = array_filter( explode( ',', static::$defaultConfiguration[ $key ]['disabled'] ) );
}
}
}
}
}
return static::$defaultConfiguration;
}
/**
* Build Matrix
*
* @param \IPS\Member $member The member
* @return \IPS\Helpers\Form\Matrix
*/
public static function buildMatrix( \IPS\Member $member )
{
$matrix = new \IPS\Helpers\Form\Matrix();
$matrix->manageable = FALSE;
$matrix->langPrefix = FALSE;
$matrix->columns = array(
'label' => function( $key, $value, $data )
{
if ( mb_substr( $key, 0, -7 ) === 'new_likes' )
{
if ( \IPS\Content\Reaction::isLikeMode() )
{
$return = 'notifications__new_likes_like';
}
else
{
$return = 'notifications__new_likes_rep';
}
}
else
{
$return = 'notifications__' . mb_substr( $key, 0, -7 );
$configuration = $class->getConfiguration( NULL );
if ( !empty( $configuration ) )
{
foreach ( $configuration as $key => $data )
{
$extensionDefaults[ $key ] = $data;
}
}
}
return \IPS\Theme::i()->getTemplate( 'members', 'core', 'global' )->notificationLabel( $return, $data );
},
'member_notifications_inline' => function( $key, $value, $data )
}
/* Combine that with what the admin has set */
static::$defaultConfiguration = iterator_to_array( \IPS\Db::i()->select( '*', 'core_notification_defaults' )->setKeyField('notification_key') );
foreach ( $extensionDefaults as $key => $data )
{
return new \IPS\Helpers\Form\YesNo( $key, in_array( 'inline', ( is_array( $data['selected'] ) ) ? $data['selected'] : array() ), FALSE, array( 'disabled' => ( !$data['editable'] OR in_array( 'inline', $data['disabled'] ) ), 'tooltip' => ( !$data['editable'] OR in_array( 'inline', $data['disabled'] ) ) ? \IPS\Member::loggedIn()->language()->addToStack('admin_notification_disabled') : NULL ) );
},
'member_notifications_email' => function( $key, $value, $data )
{
return new \IPS\Helpers\Form\YesNo( $key, in_array( 'email', ( is_array( $data['selected'] ) ) ? $data['selected'] : array() ), FALSE, array( 'disabled' => ( !$data['editable'] OR in_array( 'email', $data['disabled'] ) ), 'tooltip' => ( !$data['editable'] OR in_array( 'email', $data['disabled'] ) ) ? \IPS\Member::loggedIn()->language()->addToStack('admin_notification_disabled') : NULL ) );
},
);
/* Add rows */
$defaultConfiguration = static::defaultConfiguration();
$memberConfiguration = $member->notificationsConfiguration();
foreach( \IPS\Application::allExtensions( 'core', 'Notifications' ) as $group => $class )
{
$configuration = $class->getConfiguration( $member );
if ( !empty( $configuration ) )
{
$lang = "notifications__{$group}";
$header = \IPS\Member::loggedIn()->language()->addToStack( $lang );
$matrix->rows[] = $header;
foreach ( $configuration as $key => $data )
if ( !isset( static::$defaultConfiguration[ $key ] ) )
{
$matrix->rows[ $key ] = array( 'selected' => ( !empty( $memberConfiguration[ $key ] ) ) ? $memberConfiguration[ $key ] : NULL, 'disabled' => $defaultConfiguration[ $key ]['disabled'], 'icon' => isset( $data['icon'] ) ? $data['icon'] : NULL, 'editable' => $defaultConfiguration[ $key ]['editable'] );
/* Row isn't in DB, add it */
\IPS\Db::i()->insert( 'core_notification_defaults', array(
'notification_key' => $key,
'default' => implode( ',', $data['default'] ),
'disabled' => implode( ',', $data['disabled'] )
) );
static::$defaultConfiguration[ $key ] = array_merge( $data, array( 'editable' => TRUE ) );
}
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'] ) ) );
}
}
}
return $matrix;
return static::$defaultConfiguration;
}
/**
* Save Matrix
* Get available options for an extension
*
* @param \IPS\Member $member The member
* @param array $values Values from matrix
* @param \IPS\Member|NULL $member The member or NULL to get all
* @param object $extension The noticiations extension
* @return void
*/
public static function saveMatrix( \IPS\Member $member, array $values )
public static function availableOptions( $member, $extension )
{
/* Remove the current preferences */
\IPS\Db::i()->delete( 'core_notification_preferences', array( 'member_id=?', $member->member_id ) );
/* Get the default configuration so we know what is forced enabled by the admin */
$defaults = static::defaultConfiguration();
/* Now loop over the notifications and set preferences */
$insert = array();
foreach ( $values['notifications'] as $k => $v )
/* First of all, get the settings from the extension, maintaining backwards compatibility with older extensions */
if ( method_exists( $extension, 'configurationOptions' ) )
{
$pref = array();
if ( $v['member_notifications_inline'] )
$settings = $extension::configurationOptions( $member );
}
else
{
$configuration = $extension->getConfiguration( $member );
$settings = array();
foreach ( $configuration as $key => $details )
{
$pref[] = 'inline';
$settings[ $key ] = array(
'type' => 'standard',
'notificationTypes' => array( $key ),
'title' => "notifications__{$key}",
'showTitle' => TRUE,
'description' => NULL,
);
}
else if( !$defaults[ $k ]['editable'] AND in_array( 'inline', $defaults[ $k ]['default'] ) )
}
/* Now loop through each of those and get the value for each */
if ( $member )
{
$defaultConfiguration = \IPS\Notification::defaultConfiguration();
$memberConfiguration = $member->notificationsConfiguration();
$finalSettings = array();
foreach ( $settings as $key => $details )
{
$pref[] = 'inline';
}
if ( $v['member_notifications_email'] )
{
$pref[] = 'email';
}
else if( !$defaults[ $k ]['editable'] AND in_array( 'email', $defaults[ $k ]['default'] ) )
{
$pref[] = 'email';
if ( $details['type'] === 'standard' )
{
$options = array();
$availableTypes = array('inline');
if ( \IPS\Settings::i()->mobile_app_id and $member->members_bitoptions['mobile_notifications'] )
{
$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'] ) ) )
{
$enabled = array();
$haveMemberPreferences = FALSE;
if ( $defaultConfiguration[ $key ]['editable'] )
{
foreach ( $details['notificationTypes'] as $notificationType )
{
if ( isset( $memberConfiguration[ $notificationType ] ) )
{
$haveMemberPreferences = TRUE;
$enabled += $memberConfiguration[ $notificationType ];
}
}
}
if ( !$haveMemberPreferences )
{
$enabled = $defaultConfiguration[ $key ]['default'];
}
$options[ $type ] = array(
'title' => "member_notifications_{$type}",
'value' => \in_array( $type, $enabled ),
'editable' => $defaultConfiguration[ $key ]['editable']
);
}
}
if ( \count( $options ) or ( isset( $details['extra'] ) and \count( $details['extra'] ) ) )
{
$details['options'] = $options;
$finalSettings[ $key ] = $details;
}
}
else
{
$finalSettings[ $key ] = $details;
}
}
$insert[] = array(
'member_id' => $member->member_id,
'notification_key' => $k,
'preference' => implode( ',', $pref )
);
/* And return */
return $finalSettings;
}
else
{
return $settings;
}
\IPS\Db::i()->insert( 'core_notification_preferences', $insert );
}
/**
* 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)
* @return array
*/
public static function membersOptionCategories( \IPS\Member $member, $extensions )
{
$categories = array();
foreach( $extensions as $group => $extension )
{
$options = static::availableOptions( $member, $extension );
if ( !empty( $options ) )
{
if ( !isset( $categories[ $group ] ) )
{
$categories[ $group ] = array();
}
foreach ( $options as $key => $details )
{
if ( isset( $details['extra'] ) )
{
foreach ( $details['extra'] as $extraKey => $extraDetails )
{
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 );
}
}
}
if ( $details['type'] === 'standard' )
{
foreach ( $details['options'] as $type => $typeDetails )
{
if ( $typeDetails['value'] )
{
switch ( $type )
{
case 'inline':
if ( isset( $details['options']['push'] ) and $details['options']['push']['value'] )
{
continue 2;
}
$icon = 'bell-o';
break;
case 'push':
$icon = 'mobile';
break;
case 'email':
$icon = 'envelope-o';
break;
}
$categories[ $group ][ $type ] = array( 'title' => \IPS\Member::loggedIn()->language()->get( 'member_notifications_' . $type ), 'icon' => $icon );
}
}
}
}
}
}
return $categories;
}
/**
* 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
*/
public static function membersTypeForm( \IPS\Member $member, $extension )
{
$form = NULL;
$formIsEditable = FALSE;
if ( $options = \IPS\Notification::availableOptions( $member, $extension ) )
{
$form = new \IPS\Helpers\Form;
foreach ( $options as $key => $details )
{
if ( $details['type'] === 'separator' and \IPS\Dispatcher::i()->controllerLocation === 'front' )
{
$form->addSeparator();
}
elseif ( $details['type'] === 'header' )
{
$form->addHeader( $details['header'] );
}
elseif ( $details['type'] === 'custom' and ( !isset( $details['adminOnly'] ) ) )
{
$form->add( $details['field'] );
$formIsEditable = TRUE;
}
elseif ( $details['type'] === 'standard' )
{
if ( !$formIsEditable )
{
if ( isset( $details['extra'] ) and \count( $details['extra'] ) )
{
$formIsEditable = TRUE;
}
else
{
foreach ( $details['options'] as $option )
{
if ( $option['editable'] )
{
$formIsEditable = TRUE;
break;
}
}
}
}
$form->add( new \IPS\Helpers\Form\Custom( "notifications_{$key}", NULL, TRUE, array(
'rowHtml' => function( $field ) use ( $details, $options ) {
return \IPS\Theme::i()->getTemplate( 'members', 'core', 'global' )->notificationsSettingsRow( $field, $details );
}
) ) );
}
}
if ( $values = $form->values() )
{
foreach ( $options as $key => $details )
{
if ( $details['type'] === 'standard' )
{
$value = array();
if ( isset( $values["notifications_{$key}"] ) )
{
foreach ( $values["notifications_{$key}"] as $k => $v )
{
if ( $v === 'push' )
{
$value['inline'] = 'inline';
$value['push'] = 'push';
}
elseif ( \in_array( $k, array( 'inline', 'push', 'email' ) ) and $v )
{
$value[ $k ] = $k;
}
}
}
foreach ( $details['notificationTypes'] as $notificationKey )
{
\IPS\Db::i()->insert( 'core_notification_preferences', array(
'member_id' => $member->member_id,
'notification_key' => $notificationKey,
'preference' => implode( ',', $value )
), TRUE );
}
}
elseif ( $details['type'] === 'custom' and ( !isset( $details['adminOnly'] ) or !$details['adminOnly'] ) )
{
$extension::saveExtra( $member, $key, $values[ $key ] );
}
if ( isset( $details['extra'] ) )
{
foreach ( $details['extra'] as $extraKey => $extraDetails )
{
$extension::saveExtra( $member, $extraKey, array_key_exists( $extraKey, $values["notifications_{$key}"] ?? [] ) );
}
}
}
$member->save();
return TRUE;
}
}
if ( $form and !$formIsEditable )
{
$form->actionButtons = array();
}
return $form;
}
/**
* @brief Application
*/
@@ -265,6 +469,60 @@ class _Notification
$emails = array();
$emailRecipients = array();
$thingsBeingFollowed = array();
$pushNotifications = array();
/* First, loop over the members so we can load their notification preferences en-masse */
$membersForNotifications = array();
foreach ( $this->recipients as $member )
{
/* Let's not send notifications to deleted members, banned members or spammers */
if ( $member === NULL or !$member->member_id or $member->isBanned() or $member->members_bitoptions['bw_is_spammer'] )
{
continue;
}
$membersForNotifications[ $member->member_id ] = $member;
}
if( \count( $membersForNotifications ) )
{
/* Fill in any that may not have customized their preferences */
foreach( $membersForNotifications as $member )
{
if( $member->notificationsConfiguration === NULL )
{
$member->notificationsConfiguration = array();
}
}
/* Get all preferences at once */
$preferenceSet = array();
foreach (
\IPS\Db::i()->select(
'd.*, p.preference, p.member_id',
array( 'core_notification_defaults', 'd' )
)->join(
array( 'core_notification_preferences', 'p' ),
array( 'd.notification_key=p.notification_key AND p.member_id IN(' . implode( ',', array_keys( $membersForNotifications ) ) . ')' )
)
as $row
) {
if( !\in_array( $row['notification_key'], $preferenceSet ) )
{
foreach( $membersForNotifications as $member )
{
$member->notificationsConfiguration[ $row['notification_key'] ] = explode( ',', $row['default'] );
}
$preferenceSet[] = $row['notification_key'];
}
if ( $row['preference'] !== NULL AND $row['editable'] )
{
$membersForNotifications[ $row['member_id'] ]->notificationsConfiguration[ $row['notification_key'] ] = array_diff( explode( ',', $row['preference'] ), explode( ',', $row['disabled'] ) );
}
}
}
/* Loop recipients */
foreach ( $this->recipients as $member )
@@ -320,7 +578,7 @@ class _Notification
/* Work out how the user wants to receive this notification */
$notificationPreferences = $member->notificationsConfiguration();
$info = $this->recipients->getInfo();
if ( $info['follow_app'] === 'core' and $info['follow_area'] === 'member' )
if ( $info !== NULL and $info['follow_app'] === 'core' and $info['follow_area'] === 'member' )
{
$keyToCheck = 'follower_content';
}
@@ -336,9 +594,9 @@ class _Notification
$keyToCheck = 'unapproved_content';
}
}
/* 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;
@@ -374,26 +632,38 @@ class _Notification
{
$classname = 'IPS\\' . $info['follow_app'] . '\\' . 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 */
$email->setAdvertisementParameters( $classname, $info['follow_rel_id'] );
}
}
$thingBeingFollowed = $thingsBeingFollowed[ $info['follow_app'] ][ $info['follow_area'] ][ $info['follow_rel_id'] ];
if ( $thingBeingFollowed instanceof \IPS\Member )
{
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow_member', FALSE, array( 'sprintf' => array( $thingBeingFollowed->name ) ) );
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow_member', FALSE, array( 'htmlsprintf' => array( $thingBeingFollowed->name ) ) );
}
elseif ( $thingBeingFollowed instanceof \IPS\Node\Model )
{
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow', FALSE, array( 'sprintf' => array( $member->language()->addToStack( $thingBeingFollowed::$nodeTitle . '_sg' ), $thingBeingFollowed->_title ) ) );
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow', FALSE, array( 'htmlsprintf' => array( $member->language()->addToStack( $thingBeingFollowed::$nodeTitle . '_sg' ), $thingBeingFollowed->getTitleForLanguage( $member->language() ) ) ) );
}
else
{
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow', FALSE, array( 'sprintf' => array( $member->language()->addToStack( $thingBeingFollowed::$title ), $thingBeingFollowed->mapped('title') ) ) );
$unsubscribeBlurb = $member->language()->addToStack( 'unsubscribe_blurb_follow', FALSE, array( 'htmlsprintf' => array( $member->language()->addToStack( $thingBeingFollowed::$title ), $thingBeingFollowed->mapped('title') ) ) );
}
$unfollowLink = (string) \IPS\Http\Url::internal( "app=core&module=system&section=notifications&do=follow&follow_app={$info['follow_app']}&follow_area={$info['follow_area']}&follow_id={$info['follow_rel_id']}", 'front' );
$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' );
/* 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 )
{
$unfollowLink = $unfollowLink->setQueryString( array( 'email' => 1, 'type' => $this->emailKey ) );
}
$unfollowLink = (string) $unfollowLink;
if ( $member->members_bitoptions['email_notifications_once'] and $member->last_activity < $info['follow_notify_sent'] )
if ( $member->members_bitoptions['email_notifications_once'] and max( $member->last_activity, $member->last_visit ) < $info['follow_notify_sent'] )
{
$okToEmail = FALSE;
}
@@ -413,7 +683,8 @@ 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) */
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 ] ) ) )
$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->item AND $this->allowMerging )
{
@@ -421,7 +692,7 @@ class _Notification
{
$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 = \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->member = $member;
$notification->updated_time = time();
@@ -448,13 +719,13 @@ class _Notification
if ( $param instanceof \IPS\Content )
{
$subIdColumn = $param::$databaseColumnId;
$notification->item_sub_class = get_class( $param );
$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 ( ( $param instanceof \IPS\Content\Comment OR $param instanceof \IPS\Content\Review ) && \in_array( $this->key, array( 'new_comment', 'new_review', 'quote', 'new_likes' ) ) )
{
if ( $this->key === 'new_likes' and $this->emailParams[1] instanceof \IPS\Member )
{
@@ -482,10 +753,78 @@ class _Notification
$sentTo[ $member->member_id ][] = 'inline';
}
/* They want to receive a mobile app push notification (we don't send until the end once we've collated all the notifications to send) */
if ( \IPS\Settings::i()->mobile_app_id and $member->members_bitoptions['mobile_notifications'] and isset( $notificationPreferences[ $keyToCheck ] ) AND \in_array( 'push', $notificationPreferences[ $keyToCheck ] ) and ( !isset( $sentTo[ $member->member_id ] ) or !\in_array( 'push', $sentTo[ $member->member_id ] ) ) and $licenseKey = \IPS\IPS::licenseKey() and $licenseKey['active'] )
{
$language = $member->language();
if ( !isset( $pushNotifications[ $language->id ] ) )
{
$method = "parse_mobile_{$this->key}";
foreach ( $this->app->extensions( 'core', 'Notifications' ) as $class )
{
if ( method_exists( $class, $method ) or $this->key === 'follower_content' )
{
$data = $class::$method( $language, ...$this->emailParams );
$language->parseOutputForDisplay( $data );
if ( !isset( $data['data'] ) )
{
$data['data'] = array();
}
$data['data']['type'] = $this->app->directory . '/' . $this->key;
if ( isset( $data['data']['url'] ) )
{
$url = \IPS\Http\Url::createFromString( $data['data']['url'] );
if ( $url instanceof \IPS\Http\Url\Internal )
{
foreach ( array( 'app', 'module', 'controller', 'id', 'comment', 'review' ) as $k )
{
if ( isset( $url->hiddenQueryString[ $k ] ) )
{
$data['data'][ $k ] = $url->hiddenQueryString[ $k ];
}
elseif ( isset( $url->queryString[ $k ] ) )
{
$data['data'][ $k ] = $url->queryString[ $k ];
}
}
}
}
if ( isset( $data['data']['author'] ) )
{
$data['data']['image'] = $data['data']['author']->photo;
unset( $data['data']['author'] );
}
$pushNotifications[ $language->id ] = $data;
break;
}
}
}
if ( isset( $pushNotifications[ $language->id ] ) )
{
$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;
}
$sentTo[ $member->member_id ][] = 'push';
}
}
/* Send any emails */
$this->sendEmails( $emails, $emailRecipients );
/* Send any emails and push notifications */
if ( \count( $pushNotifications ) )
{
$this->sendPushNotifications( $pushRecipients );
}
if ( \count( $emails ) )
{
$this->sendEmails( $emails, $emailRecipients );
}
/* And return */
return $sentTo;
@@ -508,4 +847,59 @@ class _Notification
}
}
}
}
/**
* Send push notifications
*
* @param array $pushNotifications Push notifications to send
* @return void
*/
protected function sendPushNotifications( $pushNotifications )
{
$limit = 100;
$offset = 0;
while ( $slice = \array_slice( $pushNotifications, $offset, $limit ) )
{
try
{
/* Send to remoteservices */
$response = \IPS\Http\Url::ips('mobile/notify')->request()->login( \IPS\Settings::i()->mobile_app_id, \IPS\Settings::i()->mobile_app_secret )->post( array( 'data' => $slice ) );
if ( $response->httpResponseCode != 200 )
{
throw new \DomainException( $response );
}
$response = $response->decodeJson();
/* If any users have turned off notifications on all of their devices, we can remove them */
if ( isset( $response['unsubscribe'] ) and $response['unsubscribe'] )
{
\IPS\Db::i()->update( 'core_members', "members_bitoptions2 = members_bitoptions2 & ~" . \IPS\Member::$bitOptions['members_bitoptions']['members_bitoptions2']['mobile_notifications'], \IPS\Db::i()->in( 'member_id', $response['unsubscribe'] ) );
}
/* Log any errors */
if ( isset( $response['errors'] ) and $response['errors'] )
{
\IPS\Log::log( json_encode( $slice, JSON_PRETTY_PRINT ) . "\n\n==============================================================================\n\n" . json_encode( $response, JSON_PRETTY_PRINT ), 'mobile-push-notifications' );
}
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'mobile-push-notifications' );
}
$offset += $limit;
}
}
/**
* Convert HTML to plaintext for use in notifications
*
* @param string $html HTML Text
* @return string
* @todo Almost certainly will need to make this more thorough
*/
public static function textForPushNotification( $html )
{
return preg_replace( "/\n\s+/", "\n", trim( html_entity_decode( strip_tags( $html ) ) ) );
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
+7 -5
View File
@@ -11,7 +11,7 @@
namespace IPS\Notification;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -60,7 +60,7 @@ class _Table extends \IPS\Helpers\Table\Table
*/
public function setMember( \IPS\Member $member )
{
$this->where[] = array( 'member=?', $member->member_id );
$this->where[] = array( '`member`=?', $member->member_id );
}
/**
@@ -73,21 +73,23 @@ class _Table extends \IPS\Helpers\Table\Table
{
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' );
/* 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() ) ) . "')" );
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 */