Version 4.7.13
This commit is contained in:
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user