Version 4.7.13

This commit is contained in:
Neo committed 2025-12-19 16:14:56 -08:00
1 parent fd22d99e69
commit 7124a02564
954 files changed
+29277 -59308

No files matched your search

@@ -254,7 +254,17 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
{
if ( \IPS\Application::appIsEnabled( $notification['app'] ) )
{
$notifications[ $notification['id'] ] = \IPS\core\AdminNotification::constructFromData( $notification );
$notificationObject = \IPS\core\AdminNotification::constructFromData( $notification );
if( $notificationObject )
{
$notifications[ $notificationObject->id ] = $notificationObject;
}
else
{
/* Remove orphan entry */
\IPS\Db::i()->delete( 'core_acp_notifications', array( 'id=?', $notification['id'] ) );
}
}
}
return $notifications;
@@ -388,12 +398,17 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
*
* @param array $data Row from database table
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
* @return static
* @return static|false
*/
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE ): static|false
{
$classname = 'IPS\\' . $data['app'] . '\extensions\core\AdminNotifications\\' . mb_ucfirst( $data['ext'] );
if( !class_exists( $classname ) )
{
return false;
}
/* Initiate an object */
$obj = new $classname;
$obj->_new = FALSE;
+3 -2
View File
@@ -118,7 +118,7 @@ class _Alert extends \IPS\Content\Item
$today = new \IPS\DateTime;
$form->add( new \IPS\Helpers\Form\Date( 'alert_start', ( $alert ) ? \IPS\DateTime::ts( $alert->start ) : new \IPS\DateTime, TRUE, array( 'time' => TRUE ) ) );
$form->add( new \IPS\Helpers\Form\Date( 'alert_end', ( $alert AND $alert->end ) ? \IPS\DateTime::ts( $alert->end ) : 0, FALSE, array( 'min' => $today->setTime( 0, 0, 1 ), 'unlimited' => 0, 'unlimitedLang' => 'none' ) ) );
$form->add( new \IPS\Helpers\Form\Date( 'alert_end', ( $alert AND $alert->end ) ? \IPS\DateTime::ts( $alert->end ) : 0, FALSE, array( 'min' => $today->setTime( 0, 0, 1 )->add( new \DateInterval( 'P1D' ) ), 'unlimited' => 0, 'unlimitedLang' => 'none' ) ) );
$form->add( new \IPS\Helpers\Form\Editor( 'alert_content', ( $alert ) ? $alert->content : NULL, TRUE, array( 'app' => 'core', 'key' => 'Alert', 'autoSaveKey' => ( $alert ? 'editAlert__' . $alert->id : 'createAlert' ), 'attachIds' => $alert ? array( $alert->id, NULL, 'alert' ) : NULL ), NULL, NULL, NULL, 'alert_content' ) );
$options = array( 'user' => 'alert_type_user', 'group' => 'alert_type_group' );
@@ -146,10 +146,11 @@ class _Alert extends \IPS\Content\Item
'minAjaxLength' => 3,
'disallowedCharacters' => array(),
'lang' => 'mem_optional',
'maxItems' => 1,
'minimized' => FALSE
];
$form->add( new \IPS\Helpers\Form\Member( 'alert_recipient_user', ( $alert and $alert->recipient_user ) ? \IPS\Member::load( $alert->recipient_user ) : ( isset( \IPS\Request::i()->user ) ? \IPS\Member::load( \IPS\Request::i()->user ) : NULL ), FALSE, array( 'autocomplete' => $autocomplete ), function( $member ) use ( $form )
$form->add( new \IPS\Helpers\Form\Member( 'alert_recipient_user', ( $alert and $alert->recipient_user ) ? \IPS\Member::load( $alert->recipient_user ) : ( isset( \IPS\Request::i()->user ) ? \IPS\Member::load( \IPS\Request::i()->user ) : NULL ), FALSE, array( 'multiple' => 1, 'autocomplete' => $autocomplete ), function( $member ) use ( $form )
{
if ( \IPS\Request::i()->alert_recipient_type === 'user' )
{
@@ -1661,4 +1661,25 @@ HTML,
}
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=dataLayer&tab=handlers' ) );
}
/**
* Get the data layer properties used when viewing a member's profile
*
* @param \IPS\Member $member The member
* @param array $additionalProperties Optional: Any additional properties to set (e.g. view location)
* @return array
*/
public function getMemberProfileEventProperties( \IPS\Member $member, array $additionalProperties=array() ) : array
{
$profileId = $member->members_bitoptions['datalayer_pii_optout'] ? '' : \IPS\core\DataLayer::i()->getSsoId($member->member_id);
$properties = array(
'profile_group' => \IPS\Member\Group::load( $member->member_group_id )->formattedName,
'profile_group_id' => $member->member_group_id ?: null,
'profile_name' => $profileId ? $member->real_name : null,
'profile_id' => $profileId ?: null,
);
array_replace( $properties, $additionalProperties );
return $properties;
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
/**
* @brief Community Hive Helpers
* @author <a href='https://communityhive.com'>CommunityHive.com</a>
* @copyright (c) CommunityHive.com
* @license https://www.invisioncommunity.com/legal/standards/
* @package Community Hive
* @since 16 May 2023
*/
namespace IPS\core;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Application;
use IPS\Content;
use IPS\Content\Item;
use IPS\Content\Searchable;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Settings;
use Jose\Component\Core\JWK;
use IPS\Db;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Community Hive Helpers
*/
class _Hive extends \IPS\Patterns\Singleton
{
/**
* Framework coding standards....
*/
public function __construct()
{
}
/**
* Make an API request with a JWT
*
* @param string $endpoint
* @param array $payload
* @return array
*/
public static function api( string $endpoint, array $payload ): array
{
$hostname = \defined( 'COMMUNITYHIVE_URL' ) ? COMMUNITYHIVE_URL : 'https://communityhive.com';
$request = Url::external( $hostname . '/api/v1/' . $endpoint )
->request(3)
->put( static::_assembleJwt( $payload ) );
if( (int) $request->httpResponseCode !== 200 )
{
\IPS\Log::log( [ 'endpoint' => $hostname . '/api/v1/' . $endpoint, 'payload' => $payload, 'response' => $request ], 'hive_error' );
throw new \LogicException( $request->decodeJson()['error'] ?? 'an unknown error occurred.' );
}
return $request->decodeJson();
}
/**
* Generate group hash in an ordered way
*
* @param Member $member
* @return string
*/
public static function groupHash( Member $member ): string
{
if( $member->isBanned() === FALSE )
{
$hash = array_filter( array_merge( [ $member->member_group_id ], explode( ',', $member->mgroup_others ) ) );
natcasesort( $hash );
return implode( ',', $hash );
}
else
{
return 'guest';
}
}
/**
* @brief store any keys for removal in __destruct()
*/
protected static array $_deleteQueue = [];
/**
* Remove content from Hive - Used if something is moved or content is hidden
*
* @param Content $content
* @return void
*/
public function removeContent( Content $content )
{
/* Wrap it all because we don't want it to stop the moderation action */
try
{
if ( !( $content instanceof Searchable ) or empty( Settings::i()->hive_key ) )
{
return;
}
$idColumn = $content::$databaseColumnId;
if ( $content instanceof Item )
{
$key1 = \get_class( $content ) . '/' . $content->$idColumn;
}
else
{
$itemIdColumn = $content->item()::$databaseColumnId;
$key1 = \get_class( $content->item() ) . '/' . $content->item()->$itemIdColumn;
$key2 = \get_class( $content ) . '/' . $content->$idColumn;
}
/* Store IDs for removal */
static::$_deleteQueue[] = [ 'key1' => $key1, 'key2' => $key2 ?? NULL ];
}
catch( \Exception | \Throwable $e )
{
\IPS\Log::log( $e, 'hive_content_removal' );
}
}
/**
* Assemble a JWT with payload
*
* @param array $payload
* @return string
*/
protected static function _assembleJwt( array $payload ): string
{
IPS::$PSR0Namespaces['Jose'] = \IPS\ROOT_PATH . '/system/3rd_party/JwtFramework/src';
IPS::$PSR0Namespaces['Base64Url'] = \IPS\ROOT_PATH . '/system/3rd_party/Base64Url';
$jwk = new JWK([
'kty' => 'oct',
'k' => base64_encode( Settings::i()->hive_site_key )
]);
$jwsCompactSerializer = new \Jose\Component\Signature\Serializer\CompactSerializer();
$jwsBuilder = new \Jose\Component\Signature\JWSBuilder( new \Jose\Component\Core\AlgorithmManager( [ new \Jose\Component\Signature\Algorithm\HS256() ] ) );
$defaultPayload = [
'sub' => Settings::i()->hive_site_id,
'iss' => rtrim( Settings::i()->base_url, '/' ),
'iat' => time(),
'nbf' => time(),
'exp' => time() + 60,
'aud' => 'communityhive',
'hive_system_version' => Application::load('core')->long_version
];
$jws = $jwsBuilder
->create()
->withPayload( json_encode( array_merge( $defaultPayload, $payload ) ) )
->addSignature( $jwk, [
'typ' => 'JWT',
'alg' => 'HS256',
])
->build();
return $jwsCompactSerializer->serialize( $jws, 0 );
}
/**
* @brief Cache whether the member is subscribed
*/
protected static ?bool $_showPromotion = NULL;
/**
* Check if we should show the Hive Promotion for the viewing member
*
* @return bool
*/
public static function showPromotion(): bool
{
if( static::$_showPromotion !== NULL )
{
return static::$_showPromotion;
}
if ( !Settings::i()->hive_enabled )
{
return static::$_showPromotion = FALSE;
}
try
{
Db::i()->select('member_id', 'core_hive_subscribers', [ 'member_id=?', Member::loggedIn()->member_id ] )->first();
return static::$_showPromotion = FALSE;
}
catch ( \UnderflowException $e ){}
return static::$_showPromotion = TRUE;
}
/**
* Send API request to remove any content IDs that have been queued
*/
public function __destruct()
{
/* Wrap it all because we don't want it to stop the moderation action */
try
{
if( \count( static::$_deleteQueue ) )
{
static::api( 'contentremove', [ 'keys' => static::$_deleteQueue ] );
}
}
catch( \Exception | \Throwable $e )
{
\IPS\Log::log( $e, 'hive_content_removal' );
}
}
}
@@ -53,6 +53,13 @@ class _Conversation extends \IPS\Content\Item
* @brief [Content\Comment] Icon
*/
public static $icon = 'envelope';
/**
* Should IndexNow be skipped for this item? Can be used to prevent that Private Messages,
* Reports and other content which is never going to be visible to guests is triggering the requests.
* @var bool
*/
public static bool $skipIndexNow = TRUE;
/**
* Delete Record
@@ -146,6 +153,22 @@ class _Conversation extends \IPS\Content\Item
return FALSE;
}
/* If this conversation is associated with an alert, skip the
rest of the permission checks, the user should be able to reply */
if ( isset( \IPS\Request::i()->alert ) )
{
try
{
$alert = \IPS\core\Alerts\Alert::load( \IPS\Request::i()->alert );
if( $alert->forMember( \IPS\Member::loggedIn() ) AND $alert->reply == 2 )
{
return TRUE;
}
}
catch ( \OutOfRangeException $e ) {}
}
/* Have we exceeded our limit for the day/minute? */
if ( $member->group['g_pm_perday'] !== -1 )
@@ -375,7 +375,6 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
bypass the system. */
$content = preg_replace( "#<blockquote\s+?class=\"ipsQuote\"[^>]+?>#", "<blockquote>", $content );
$content = preg_replace( "#<div\s+?class=\"ipsQuote_citation\"([^>]+?)?>(.+?)</div>#s", "", $content );
foreach( static::getProfanity() AS $profanity )
{
if ( $profanity->action == 'moderate' )
@@ -437,6 +436,79 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
return FALSE;
}
/**
* Check Profanity Blocks
*
* @param string $content The content to check
* @return bool|string
*/
public static function checkProfanityBlocks( $content )
{
$looseProfanity = array();
$exactProfanity = array();
/* Clean up profanity a little to remove citation details and inline data attributes.
We don't want to remove <blockquotes> entirely as users can then hide profanity in a quote box to
bypass the system. */
$content = preg_replace( "#<blockquote\s+?class=\"ipsQuote\"[^>]+?>#", "<blockquote>", $content );
$content = preg_replace( "#<div\s+?class=\"ipsQuote_citation\"([^>]+?)?>(.+?)</div>#s", "", $content );
$blocked = [];
foreach( static::getProfanity() AS $profanity )
{
if ( $profanity->action == 'block' )
{
if ( $profanity->m_exact )
{
$exactProfanity[] = $profanity->type;
}
else
{
$looseProfanity[] = $profanity->type;
}
}
}
/* Loose is easy - if any of the words are present, then mod queue */
if ( \count( $looseProfanity ) )
{
foreach( $looseProfanity AS $word )
{
if ( mb_stristr( $content, $word ) )
{
return $word;
break;
}
}
}
/* Still here? Check exact - this gets a bit more complicated. */
if ( \count( $exactProfanity ) )
{
$words = array();
foreach( $exactProfanity AS $word )
{
$words[] = preg_quote( $word, '/' );
}
$split = preg_split( '/((?=<^|\b)(?:' . implode( '|', $words ) . ')(?=\b|$))/iu', $content, null, PREG_SPLIT_DELIM_CAPTURE );
if ( \is_array( $split ) )
{
foreach( $split AS $section )
{
if ( \in_array( mb_strtolower( $section ), array_map( 'mb_strtolower', $exactProfanity ) ) )
{
return $section;
break;
}
}
}
}
/* Still here? All good */
return FALSE;
}
/**
* Check URL Filters
*
@@ -511,20 +583,6 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
break;
}
/* IF we are moderating all URL's and it's not an attachment, return here. */
if ( $good !== TRUE )
{
if ( \IPS\Settings::i()->url_filter_any_action == 'moderate' )
{
return $urlToCheck;
}
if ( \IPS\Settings::i()->ipb_url_filter_option == 'none' )
{
return $good;
}
}
$url = preg_quote( $url, '/' );
$url = str_replace( '\*', "(.*?)", $url );
@@ -552,6 +610,21 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
}
}
}
/* Check the URL after we have cycled through the black/white list. */
if ( $good !== TRUE )
{
/* If we are moderating links, only return if the URL was caught by a rule above. */
if ( \IPS\Settings::i()->url_filter_any_action == 'moderate' AND ( \IPS\Settings::i()->ipb_url_filter_option == 'none' OR $good !== NULL ) )
{
return $urlToCheck;
}
if ( \IPS\Settings::i()->ipb_url_filter_option == 'none' )
{
return $good;
}
}
}
}
@@ -37,6 +37,8 @@ class _Field extends \IPS\CustomField
const SEARCH = 4;
const EDIT = 5;
const PROFILE_COMPLETION = 6;
const PII_DATA_EXPORT = 7;
/**
* @brief [ActiveRecord] Database Table
@@ -268,7 +270,8 @@ class _Field extends \IPS\CustomField
( $location === static::PROFILE and $row['pf_member_hide'] == 'hide' ) or
( $location === static::SEARCH and ( !$row['pf_search_type'] or $row['pf_member_hide'] == 'hide' ) ) or
( $location === static::REG and !$row['pf_show_on_reg'] ) or
( ( $location === static::EDIT OR $location === static::PROFILE_COMPLETION ) and !$row['pf_member_edit'] )
( ( $location === static::EDIT OR $location === static::PROFILE_COMPLETION ) and !$row['pf_member_edit'] ) or
( $location === static::PII_DATA_EXPORT and !$row['pf_contains_pii'] )
)
{
continue;
@@ -355,6 +358,11 @@ class _Field extends \IPS\CustomField
continue 2;
}
break;
case ( static::PII_DATA_EXPORT ):
if( !$row['pf_contains_pii'] )
{
continue 2;
}
}
/* Next to user content we perform a couple of extra checks */
@@ -404,10 +412,11 @@ class _Field extends \IPS\CustomField
/* Quick register is off, so show normal 'reg' field */
$form->add( new \IPS\Helpers\Form\YesNo( 'pf_show_on_reg', $this->id ? $this->show_on_reg : TRUE, FALSE, array(), NULL, NULL, NULL, 'pf_show_on_reg' ) );
}
$form->add( new \IPS\Helpers\Form\YesNo( 'pf_member_edit', $this->id ? $this->member_edit : TRUE, FALSE, array(), NULL, NULL, NULL, 'pf_member_edit' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'pf_member_hide', $this->id ? $this->member_hide : 'all', TRUE, array( 'options' => array( 'hide' => 'custom_fields_hide', 'staff' => 'custom_fields_staff', 'owner' => 'custom_fields_staff_owner', 'all' => 'custom_fields_all' ), 'toggles' => array( 'staff' => array( 'pf_profile_format' ), 'all' => array( 'pf_profile_format' ), 'owner' => array( 'pf_profile_format' ) ) ), NULL, NULL, NULL, 'pf_member_hide' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'pf_topic_hide', $this->id ? $this->topic_hide : 'hide', TRUE, array( 'options' => array( 'hide' => 'custom_fields_hide', 'staff' => 'custom_fields_staff', 'all' => 'custom_fields_all' ), 'toggles' => array( 'staff' => array( 'pf_format' ), 'all' => array( 'pf_format' ) ) ), NULL, NULL, NULL, 'pf_topic_hide' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'pf_contains_pii', $this->id ? $this->contains_pii : FALSE, FALSE ) );
$form->addHeader( 'pfield_displayoptions' );
$form->add( new \IPS\Helpers\Form\Select( 'pf_search_type', $this->id ? $this->search_type : 'loose', FALSE, array( 'options' => array( 'exact' => 'pf_search_type_exact', 'loose' => 'pf_search_type_loose', '' => 'pf_search_type_none' ) ), NULL, NULL, NULL, 'pf_search_type' ) );
@@ -525,4 +534,5 @@ const STAFF = _Field::STAFF;
const CONTENT = _Field::CONTENT;
const SEARCH = _Field::SEARCH;
const EDIT = _Field::EDIT;
const PROFILE_COMPLETION = _Field::PROFILE_COMPLETION;
@@ -103,6 +103,13 @@ class _Report extends \IPS\Content\Item implements \IPS\Content\ReadMarkers
* @brief [Content\Item] Include these items in trending content
*/
public static $includeInTrending = FALSE;
/**
* Should IndexNow be skipped for this item? Can be used to prevent that Private Messages,
* Reports and other content which is never going to be visible to guests is triggering the requests.
* @var bool
*/
public static bool $skipIndexNow = TRUE;
/**
* Should posting this increment the poster's post count?
@@ -0,0 +1,189 @@
<?php
/**
* @brief Statistics Charts
* @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 1 Jul 2015
*/
namespace IPS\core\Statistics;
/* 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;
}
/**
* Statistics Charts
*/
abstract class _Chart
{
/**
* @brief Controller
*/
public $controller = NULL;
/**
* Get Chart
*
* @param \IPS\Http\Url $url URL the chart is being shown on.
*
* @return \IPS\Helpers\Chart
*/
abstract public function getChart( \IPS\Http\Url $url ): \IPS\Helpers\Chart;
/**
* Load from Extension
*
* @return static
* @throws \OutOfRangeException
*/
public static function loadFromExtension( string $app, string $extension ): static
{
$extensions = \IPS\Application::load( $app )->extensions( 'core', 'Statistics' );
if ( isset( $extensions[ $extension ] ) )
{
return $extensions[ $extension ];
}
throw new \OutOfRangeException;
}
/**
* Load from Controller
*
* @return static
* @throws \OutOfRangeException
*/
public static function loadFromController( string $controller ): static
{
foreach( \IPS\Application::allExtensions( 'core', 'Statistics', FALSE ) AS $extension )
{
if ( $extension->controller AND $extension->controller === $controller )
{
return $extension;
}
}
throw new \OutOfRangeException;
}
/**
* Construct a saved chart from data
*
* @param array|int $data Chart ID or pre-loaded chart data.
* @param \IPS\Http\Url $url URL chart is shown on
* @param \IPS\Member|bool $check Check chart is owned by a specific member, or the currently logged in member if TRUE. If FALSE, no permission checking.
*
* @return \IPS\Helpers\Chart
* @throws \OutOfRangeException
* @throws \InvalidArgumentException
*/
public static function constructMemberChartFromData( array|int $data, \IPS\Http\Url $url, \IPS\Member|bool $check = TRUE ): \IPS\Helpers\Chart
{
try
{
if ( $check === FALSE )
{
if ( !\is_array( $data ) )
{
$data = \IPS\Db::i()->select( '*', 'core_saved_charts', array( "id=?", $data ) )->first();
}
}
else if ( ( $check instanceof \IPS\Member ) AND $check->member_id )
{
if ( !\is_array( $data ) )
{
$data = \IPS\Db::i()->select( '*', 'core_saved_charts', array( "chart_id=? AND chart_member=?", $data, $check->member_id ) )->first();
}
else if ( $data['chart_member'] !== $check->member_id )
{
throw new \UnderflowException;
}
}
else if ( $check === TRUE AND \IPS\Member::loggedIn()->member_id )
{
if ( !\is_array( $data ) )
{
$data = \IPS\Db::i()->select( '*','core_saved_charts', array( "chart_id=? AND chart_member=?", $data, \IPS\Member::loggedIn()->member_id ) )->first();
}
else if ( $data['chart_member'] !== \IPS\Member::loggedIn()->member_id )
{
throw new \UnderflowException;
}
}
else
{
/* If we're here, we were passed a guest object which isn't going to work. Throw a different exception as this should lead directly to a bugfix since this should never happen. */
throw new \InvalidArgumentException;
}
$extension = static::loadFromController( $data['chart_controller'] );
$chart = $extension->getChart( $url );
$chart->currentFilters = json_decode( $data['chart_configuration'], true );
$chart->timescale = $data['chart_timescale'] ?? $chart->timescale;
$chart->title = $data['chart_title'];
$chart->showFilterTabs = FALSE;
$chart->showIntervals = FALSE;
$chart->showDateRange = FALSE;
return $chart;
}
catch( \UnderflowException $e )
{
/* Chart doesn't exist, or isn't owned by $member */
throw new \OutOfRangeException;
}
}
/**
* Get charts for Member
*
* @param \IPS\Http\Url $url URL
* @param bool $idsOnly Return only ID's belonging to the user for lazyloading.
* @param \IPS\Member|null $member Member, or NULL for currently logged in member.
*
* @return array
* @throws \InvalidArgumentException
*/
public static function getChartsForMember( \IPS\Http\Url $url, bool $idsOnly = FALSE, ?\IPS\Member $member = NULL ): array
{
$member ??= \IPS\Member::loggedIn();
if ( !$member->member_id )
{
throw new \InvalidArgumentException;
}
$return = [];
foreach( \IPS\Db::i()->select( '*', 'core_saved_charts', array( "chart_member=?", $member->member_id ) ) AS $chart )
{
if ( $idsOnly )
{
$return[] = $chart['chart_id'];
}
else
{
try
{
$controller = explode( '_', $chart['chart_controller'] );
$return[$chart['chart_id']] = array(
'chart' => static::constructMemberChartFromData( $chart, $url, $member ),
'data' => $chart
);
}
catch( \OutOfRangeException )
{
continue;
}
}
}
return $return;
}
}
@@ -777,7 +777,7 @@ class _Warning extends \IPS\Content\Item
public function sendNotifications()
{
/* Queue if there's lots, or just send them */
if ( $this->notificationsCount( $this->getModerators() ) > static::NOTIFICATIONS_PER_BATCH )
if ( $this->notificationsCount( $this->getModerators() ) > \IPS\NOTIFICATION_BACKGROUND_THRESHOLD )
{
\IPS\Task::queue( 'core', 'WarnNotifications', array( 'class' => \get_class( $this ), 'item' => $this->id, 'sentTo' => array() ), 1 );
}