Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -13,9 +13,14 @@ namespace IPS\core\Achievements\Actions;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Application;
|
||||
use IPS\cloud\Quest;
|
||||
use IPS\core\Achievements\Rule;
|
||||
use IPS\Helpers\Form\Node;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
use function get_called_class;
|
||||
|
||||
@@ -42,6 +47,17 @@ abstract class AchievementActionAbstract
|
||||
return Application::appIsEnabled( $exploded[1] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this should be displayed in the ACP list.
|
||||
* We need to override this for some rules that should remain hidden
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function showInAcp() : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter form elements
|
||||
*
|
||||
@@ -51,7 +67,22 @@ abstract class AchievementActionAbstract
|
||||
*/
|
||||
public function filters( ?array $filters, Url $url ): array
|
||||
{
|
||||
return [];
|
||||
$return = [];
|
||||
|
||||
if( Bridge::i()->featureIsEnabled( 'quests' ) )
|
||||
{
|
||||
$classKey = explode( '\\', get_called_class() )[5];
|
||||
$questField = new Node( 'achievement_filter_quest_' . $classKey, $filters['quest'] ?? null, false, [
|
||||
'class' => Quest::class,
|
||||
'subnodes' => false,
|
||||
'multiple' => false,
|
||||
'showAllNodes' => true
|
||||
], null, Member::loggedIn()->language()->addToStack( 'achievement_filter_quest_prefix' ), null, 'achievement_filter_quest_' . $classKey );
|
||||
$questField->label = Member::loggedIn()->language()->addToStack( 'achievement_filter_quest' );
|
||||
$return['quest'] = $questField;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +93,17 @@ abstract class AchievementActionAbstract
|
||||
*/
|
||||
public function formatFilterValues( array $values ): array
|
||||
{
|
||||
if( Bridge::i()->featureIsEnabled( 'quests' ) )
|
||||
{
|
||||
$classKey = explode( '\\', get_called_class() )[5];
|
||||
if( isset( $values['achievement_filter_quest_' . $classKey ] ) and $values['achievement_filter_quest_' . $classKey ] instanceof Quest )
|
||||
{
|
||||
return [
|
||||
'quest' => $values['achievement_filter_quest_' . $classKey ]->_id
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -81,6 +123,31 @@ abstract class AchievementActionAbstract
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get "description" for rule (usually a description of the rule's filters)
|
||||
*
|
||||
* @param Rule $rule The rule
|
||||
* @return string|NULL
|
||||
*/
|
||||
protected function _questFilterDescription( Rule $rule ): ?string
|
||||
{
|
||||
if ( isset( $rule->filters['quest'] ) and Bridge::i()->featureIsEnabled( 'quests' ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$quest = Quest::load( $rule->filters['quest'] );
|
||||
return Member::loggedIn()->language()->addToStack( 'achievements_title_filter_quest', false, [
|
||||
'htmlsprintf' => [
|
||||
Theme::i()->getTemplate( 'achievements', 'core' )->ruleDescriptionBadge( 'quest', $quest->_title )
|
||||
]
|
||||
]);
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the labels for the people this action might give awards to
|
||||
@@ -107,6 +174,21 @@ abstract class AchievementActionAbstract
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the member has already completed this rule.
|
||||
* Used for retroactive rule completion.
|
||||
* So far, this is only used in Quests, but may be used elsewhere at a later point.
|
||||
*
|
||||
* @param Member $member
|
||||
* @param array $filters
|
||||
* @return bool
|
||||
*/
|
||||
public function isRuleCompleted( Member $member, array $filters ) : bool
|
||||
{
|
||||
/* Default to false, so that any other rules would have to be completed again. */
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identifier to prevent the member being awarded points for the same action twice
|
||||
|
||||
@@ -141,7 +141,8 @@ abstract class ContentAchievementActionAbstract extends AchievementActionAbstrac
|
||||
$nthFilter = new Number( 'achievement_filter_' . $classKey . '_nth', ( $filters and isset( $filters['milestone'] ) and $filters['milestone'] ) ? $filters['milestone'] : 0, FALSE, [], NULL, Member::loggedIn()->language()->addToStack('achievement_filter_nth_their'), Member::loggedIn()->language()->addToStack('achievement_filter_' . $classKey . '_nth_suffix') );
|
||||
$nthFilter->label = Member::loggedIn()->language()->addToStack('achievement_filter_NewContentItem_nth');
|
||||
|
||||
$return = [ 'type' => $typeFilter ];
|
||||
$return = parent::filters( $filters, $url );
|
||||
$return['type'] = $typeFilter;
|
||||
foreach ( $_return as $k => $v )
|
||||
{
|
||||
$return[ $k ] = $v;
|
||||
@@ -159,9 +160,10 @@ abstract class ContentAchievementActionAbstract extends AchievementActionAbstrac
|
||||
*/
|
||||
public function formatFilterValues( array $values ): array
|
||||
{
|
||||
$return = parent::formatFilterValues( $values );
|
||||
|
||||
$classKey = explode( '\\', get_called_class() )[5];
|
||||
|
||||
$return = [];
|
||||
|
||||
if ( isset( $values['achievement_filter_' . $classKey . '_type'] ) )
|
||||
{
|
||||
$return['type'] = $values['achievement_filter_' . $classKey . '_type'];
|
||||
@@ -293,7 +295,7 @@ abstract class ContentAchievementActionAbstract extends AchievementActionAbstrac
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( 'achievements_title_filter_location', FALSE, [
|
||||
'htmlsprintf' => [
|
||||
Theme::i()->getTemplate( 'achievements' )->ruleDescriptionBadge( 'location',
|
||||
Theme::i()->getTemplate( 'achievements', 'core' )->ruleDescriptionBadge( 'location',
|
||||
count( $nodeNames ) === 1 ? $nodeNames[0] : Member::loggedIn()->language()->addToStack( 'achievements_title_filter_location_val', FALSE, [ 'sprintf' => [
|
||||
count( $nodeNames ),
|
||||
Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle, FALSE )
|
||||
|
||||
@@ -87,7 +87,8 @@ abstract class NodeAchievementActionAbstract extends AchievementActionAbstract
|
||||
$nthFilter = new Number( 'achievement_filter_Node_nth', ( $filters and isset( $filters['milestone'] ) and $filters['milestone'] ) ? $filters['milestone'] : 0, FALSE, [], NULL, Member::loggedIn()->language()->addToStack('achievement_filter_nth_their'), Member::loggedIn()->language()->addToStack('achievement_filter_Node_nth_suffix') );
|
||||
$nthFilter->label = Member::loggedIn()->language()->addToStack('achievement_filter_Node_nth');
|
||||
|
||||
$return = [ 'type' => $typeFilter ];
|
||||
$return = parent::filters( $filters, $url );
|
||||
$return['type'] = $typeFilter;
|
||||
foreach ( $_return as $k => $v )
|
||||
{
|
||||
$return[ $k ] = $v;
|
||||
@@ -105,7 +106,7 @@ abstract class NodeAchievementActionAbstract extends AchievementActionAbstract
|
||||
*/
|
||||
public function formatFilterValues( array $values ): array
|
||||
{
|
||||
$return = [];
|
||||
$return = parent::formatFilterValues( $values );
|
||||
if ( isset( $values['achievement_filter_Node_type'] ) )
|
||||
{
|
||||
$return['type'] = $values['achievement_filter_Node_type'];
|
||||
@@ -197,7 +198,7 @@ abstract class NodeAchievementActionAbstract extends AchievementActionAbstract
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( 'achievements_title_filter_location', FALSE, [
|
||||
'htmlsprintf' => [
|
||||
Theme::i()->getTemplate( 'achievements' )->ruleDescriptionBadge( 'location',
|
||||
Theme::i()->getTemplate( 'achievements', 'core' )->ruleDescriptionBadge( 'location',
|
||||
count( $nodeNames ) === 1 ? $nodeNames[0] : Member::loggedIn()->language()->addToStack( 'achievements_title_filter_location_val', FALSE, [ 'sprintf' => [
|
||||
count( $nodeNames ),
|
||||
Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle, FALSE, [ 'strtolower' => TRUE ] )
|
||||
|
||||
@@ -268,6 +268,11 @@ class Badge extends Model
|
||||
{
|
||||
if ( !$this->badge_use_image and $badge = $this->getRecordBadge() )
|
||||
{
|
||||
if( !$badge->file() )
|
||||
{
|
||||
$badge->save();
|
||||
}
|
||||
|
||||
return $badge->file()->url;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,12 +128,12 @@ class Rule extends Model
|
||||
/**
|
||||
* Get the extension
|
||||
*
|
||||
* @return AchievementActionAbstract
|
||||
* @return AchievementActionAbstract|null
|
||||
*/
|
||||
public function extension(): AchievementActionAbstract
|
||||
public function extension(): ?AchievementActionAbstract
|
||||
{
|
||||
$exploded = explode( '_', $this->action );
|
||||
return Application::load( $exploded[0] )->extensions( 'core', 'AchievementAction' )[ $exploded[1] ];
|
||||
return Application::load( $exploded[0] )->extensions( 'core', 'AchievementAction' )[ $exploded[1] ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,19 +14,27 @@ namespace IPS\core;
|
||||
|
||||
use Exception;
|
||||
use IPS\Application;
|
||||
use IPS\Content;
|
||||
use IPS\Db;
|
||||
use IPS\Dispatcher;
|
||||
use IPS\Email;
|
||||
use IPS\Extensions\AdvertisementLocationsAbstract;
|
||||
use IPS\File;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
use IPS\Helpers\Form\Node;
|
||||
use IPS\Helpers\Form\Number;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\Output;
|
||||
use IPS\Patterns\ActiveRecord;
|
||||
use IPS\Redis;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use function count;
|
||||
use function defined;
|
||||
|
||||
@@ -102,11 +110,245 @@ class Advertisement extends ActiveRecord
|
||||
* @brief List all default location codes
|
||||
*/
|
||||
public static array $defaultLocations = [
|
||||
'ad_global_header',
|
||||
'ad_global_footer',
|
||||
'ad_sidebar'
|
||||
'ad_global_header' => array( 'ad_apps' ),
|
||||
'ad_global_footer' => array( 'ad_apps' ),
|
||||
'ad_sidebar' => array( 'ad_apps' )
|
||||
];
|
||||
|
||||
/**
|
||||
* Return an array of form elements that will handle location-based settings
|
||||
*
|
||||
* @param array $currentValues
|
||||
* @param array $currentLocations
|
||||
* @return array
|
||||
*/
|
||||
public static function locationFields( array $currentValues, array $currentLocations ) : array
|
||||
{
|
||||
$settingFields = [];
|
||||
$applications = [];
|
||||
$appToggles = [];
|
||||
$nodeFields = [];
|
||||
$defaultLocations = static::$defaultLocations;
|
||||
foreach( Application::enabledApplications() as $app )
|
||||
{
|
||||
$applications[ $app->directory ] = $app->_title;
|
||||
foreach( $app->extensions( 'core', 'ContentRouter' ) as $router )
|
||||
{
|
||||
if( !isset( $appToggles[ $app->directory ] ) )
|
||||
{
|
||||
$appToggles[ $app->directory ] = [];
|
||||
}
|
||||
|
||||
foreach( $router->classes as $itemClass )
|
||||
{
|
||||
if( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$nodeClass = $itemClass::$containerNodeClass;
|
||||
|
||||
/* Skip nodes that can be created by users, such as blogs */
|
||||
if( !empty( $nodeClass::$ownerTypes ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = str_replace( '\\', '_', $nodeClass );
|
||||
$field = new Node( $key, $currentValues[ $key ] ?? 0, false, [
|
||||
'class' => $nodeClass,
|
||||
'multiple' => true,
|
||||
'zeroVal' => 'all',
|
||||
'nodeGroups' => true
|
||||
], null, null, null, $key );
|
||||
$field->label = Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle );
|
||||
$nodeFields[] = $field;
|
||||
$appToggles[ $app->directory ][] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Add a field to restrict by app */
|
||||
$settingFields['ad_apps'] = new Select( 'ad_apps', $currentValues['ad_apps'] ?? -1, false, array(
|
||||
'options' => $applications,
|
||||
'multiple' => true,
|
||||
'unlimitedLang' => 'all',
|
||||
'toggles' => $appToggles
|
||||
), null, null, null, 'ad_apps' );
|
||||
|
||||
foreach( $nodeFields as $field )
|
||||
{
|
||||
$settingFields[ $field->htmlId ] = $field;
|
||||
}
|
||||
|
||||
/* Now grab ad location extensions */
|
||||
foreach ( Application::allExtensions( 'core', 'AdvertisementLocations', FALSE, 'core' ) as $key => $extension )
|
||||
{
|
||||
/* @var AdvertisementLocationsAbstract $extension */
|
||||
$result = $extension->getSettings( $currentValues );
|
||||
|
||||
$defaultLocations = array_merge( $defaultLocations, $result['locations'] );
|
||||
|
||||
/* If this is a list view, add setting fields for interaval and repeat */
|
||||
if( $extension::$listView )
|
||||
{
|
||||
$settingFields['ad_view_type'] = new Radio( 'ad_view_type', $currentValues['ad_view_type'] ?? null, true, [
|
||||
'options' => [
|
||||
'interval' => 'ad_view_type__interval',
|
||||
'fixed' => 'ad_view_type__fixed'
|
||||
],
|
||||
'toggles' => [
|
||||
'interval' => [ 'ad_view_number', 'ad_view_repeat' ],
|
||||
'fixed' => [ 'ad_view_position' ]
|
||||
]
|
||||
], null, null, null, 'ad_view_type' );
|
||||
|
||||
$settingFields['ad_view_number'] = new Number( 'ad_view_number', ( isset( $currentValues['ad_view_number'] ) AND $currentValues['ad_view_number'] ) ? $currentValues['ad_view_number'] : 1, null, array( 'min' => 1 ), NULL, NULL, Member::loggedIn()->language()->addToStack('ad_view_number_suffix'), 'ad_view_number' );
|
||||
|
||||
$settingFields['ad_view_repeat'] = new Number( 'ad_view_repeat', $currentValues['ad_view_repeat'] ?? 1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, Member::loggedIn()->language()->addToStack('ad_view_repeat_suffix'), 'ad_view_repeat' );
|
||||
|
||||
$settingFields['ad_view_position'] = new Number( 'ad_view_position', $currentValues['ad_view_position'] ?? 1, null, id: 'ad_view_position' );
|
||||
|
||||
foreach( $result['locations'] as $location => $toggles )
|
||||
{
|
||||
$defaultLocations[ $location ][] = 'ad_view_type';
|
||||
}
|
||||
}
|
||||
|
||||
if( isset( $result['settings'] ) )
|
||||
{
|
||||
$settingFields = $settingFields + $result['settings'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Make location optional so that you can have an active ad for widgets only */
|
||||
$locationField = new CheckboxSet( 'ad_location',
|
||||
array_intersect( array_keys( $defaultLocations ), $currentLocations ),
|
||||
false,
|
||||
array(
|
||||
'options' => array_combine( array_keys( $defaultLocations ), array_keys( $defaultLocations ) ),
|
||||
'toggles' => $defaultLocations,
|
||||
'noDefault' => true
|
||||
),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
'ad_location'
|
||||
);
|
||||
|
||||
return [ 'ad_location' => $locationField ] + $settingFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the location settings for saving
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public static function processLocationFields( array $values ) : array
|
||||
{
|
||||
/* Any additional settings to save? */
|
||||
$additionalSettings = array(
|
||||
'ad_apps' => ( isset( $values['ad_apps'] ) AND is_array( $values['ad_apps'] ) ) ? array_values( $values['ad_apps'] ) : 0
|
||||
);
|
||||
|
||||
if( is_array( $additionalSettings['ad_apps'] ) )
|
||||
{
|
||||
foreach( $additionalSettings['ad_apps'] as $app )
|
||||
{
|
||||
foreach( Application::load( $app )->extensions( 'core', 'ContentRouter' ) as $router )
|
||||
{
|
||||
foreach( $router->classes as $itemClass )
|
||||
{
|
||||
if( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$key = str_replace( '\\', '_', $itemClass::$containerNodeClass );
|
||||
if( !empty( $values[ $key ] ) and is_array( $values[ $key ] ) )
|
||||
{
|
||||
$additionalSettings[ $key ] = array_keys( $values[ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( Application::allExtensions( 'core', 'AdvertisementLocations', FALSE, 'core' ) as $key => $extension )
|
||||
{
|
||||
if( $extension::$listView )
|
||||
{
|
||||
$additionalSettings['ad_view_type'] = $values['ad_view_type'] ?? 'interval';
|
||||
if( $additionalSettings['ad_view_type'] == 'fixed' )
|
||||
{
|
||||
$additionalSettings['ad_view_position'] = $values['ad_view_position'] ?? 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$additionalSettings['ad_view_number'] = $values['ad_view_number'] ?? 1;
|
||||
$additionalSettings['ad_view_repeat'] = $values['ad_view_repeat'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
$settings = $extension->parseSettings( $values );
|
||||
|
||||
$additionalSettings = array_merge( $additionalSettings, $settings );
|
||||
}
|
||||
|
||||
return $additionalSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of form elements for email advertisement configuration
|
||||
*
|
||||
* @param array $currentValues
|
||||
* @return array
|
||||
*/
|
||||
public static function emailFields( array $currentValues ) : array
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
/* Container restrictions for email ads */
|
||||
$containerClasses = array();
|
||||
$containerToggles = array();
|
||||
|
||||
foreach( Content::routedClasses( FALSE, FALSE, TRUE ) as $contentItemClass )
|
||||
{
|
||||
if( isset( $contentItemClass::$containerNodeClass ) AND $contentItemClass::$containerNodeClass AND !isset( $containerClasses[ $contentItemClass::$containerNodeClass ] ) )
|
||||
{
|
||||
$containerClass = $contentItemClass::$containerNodeClass;
|
||||
|
||||
$containerClasses[ $containerClass ] = $contentItemClass::$title . '_pl';
|
||||
$containerToggles[ $containerClass ] = array( 'node_' . md5( $containerClass ) );
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $containerClasses ) )
|
||||
{
|
||||
$fields['_ad_email_container'] = new Select( '_ad_email_container', $currentValues['email_container'] ?? '*', FALSE, array( 'options' => $containerClasses, 'multiple' => FALSE, 'unlimited' => '*', 'unlimitedLang' => 'unrestricted', 'toggles' => $containerToggles ), NULL, NULL, NULL, '_ad_email_container' );
|
||||
|
||||
foreach( $containerClasses as $classname => $lang )
|
||||
{
|
||||
$containerField = new Node( 'ad_node_' . md5( $classname ), $currentValues['email_node'] ?? 0, FALSE, array( 'class' => $classname, 'zeroVal' => 'any', 'multiple' => TRUE, 'forceOwner' => FALSE ), NULL, NULL, NULL, 'node_' . md5( $classname ) );
|
||||
$containerField->label = Member::loggedIn()->language()->addToStack( $classname::$nodeTitle );
|
||||
$fields[ 'node_' . md5( $classname ) ] = $containerField;
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format email fields for saving
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public static function processEmailFields( array $values ) : array
|
||||
{
|
||||
return [
|
||||
'email_container' => $values['_ad_email_container'],
|
||||
'email_node' => ( $values['_ad_email_container'] != '*' ) ? ( ( $values['ad_node_' . md5( $values['_ad_email_container'] ) ] == 0 ) ? 0 : array_keys( $values['ad_node_' . md5( $values['_ad_email_container'] ) ] ) ) : 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch advertisements and return the appropriate one to display
|
||||
*
|
||||
@@ -162,17 +404,55 @@ class Advertisement extends ActiveRecord
|
||||
}
|
||||
|
||||
/* Weed out any ads that we don't see due to the application settings */
|
||||
if ( in_array( $adLocation, static::$defaultLocations ) and isset( $ad->_additional_settings['ad_apps'] ) and is_array( $ad->_additional_settings['ad_apps'] ) and $ad->_additional_settings != 0 )
|
||||
if ( array_key_exists( $adLocation, static::$defaultLocations ) and isset( $ad->_additional_settings['ad_apps'] ) and ( is_array( $ad->_additional_settings['ad_apps'] ) and count( $ad->_additional_settings['ad_apps'] ) ) and $ad->_additional_settings != 0 )
|
||||
{
|
||||
if ( !in_array( Dispatcher::i()->application->directory, $ad->_additional_settings['ad_apps'] ) )
|
||||
{
|
||||
unset( static::$advertisements[ $adLocation ][ $index ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if we have a content class defined */
|
||||
if( isset( Output::i()->bodyAttributes['contentClass'] ) )
|
||||
{
|
||||
$contentId = Request::i()->id ?? 0;
|
||||
$class = Output::i()->bodyAttributes['contentClass'];
|
||||
if( isset( $class::$containerNodeClass ) )
|
||||
{
|
||||
if( isset( Request::i()->id ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$item = $class::load( Request::i()->id );
|
||||
$contentId = $item->mapped( 'container' );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
$class = $class::$containerNodeClass;
|
||||
}
|
||||
|
||||
$key = str_replace( '\\', '_', $class );
|
||||
if( isset( $ad->_additional_settings[ $key ] ) and is_array( $ad->_additional_settings[ $key ] ) and !in_array( $contentId, $ad->_additional_settings[ $key ] ) )
|
||||
{
|
||||
unset( static::$advertisements[ $adLocation ][ $index ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If we defined specific nodes, and we are on a general page (e.g. an index page), skip this ad */
|
||||
foreach( $ad->_additional_settings as $k => $v )
|
||||
{
|
||||
if( str_starts_with( $k, 'IPS_' . Dispatcher::i()->application->directory ) )
|
||||
{
|
||||
unset( static::$advertisements[ $adLocation ][ $index ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !in_array( $adLocation, static::$defaultLocations ) )
|
||||
if( !array_key_exists( $adLocation, static::$defaultLocations ) )
|
||||
{
|
||||
foreach ( $ads as $index => $ad )
|
||||
{
|
||||
@@ -461,8 +741,23 @@ class Advertisement extends ActiveRecord
|
||||
break;
|
||||
}
|
||||
|
||||
$indexNumber = $ad->_additional_settings[ 'ad_view_number' ] ?? false;
|
||||
$repeat = $ad->_additional_settings[ 'ad_view_repeat' ] ?? false;
|
||||
/* Fixed position ads have a set position and do not repeat */
|
||||
if( isset( $ad->_additional_settings['ad_view_type'] ) and $ad->_additional_settings['ad_view_type'] == 'fixed' )
|
||||
{
|
||||
$indexNumber = $ad->_additional_settings['ad_view_position'] ?? 1;
|
||||
$repeat = 1;
|
||||
|
||||
/* Ignore this entirely if we are not in the correct position */
|
||||
if( $position != $indexNumber )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$indexNumber = $ad->_additional_settings[ 'ad_view_number' ] ?? false;
|
||||
$repeat = $ad->_additional_settings[ 'ad_view_repeat' ] ?? false;
|
||||
}
|
||||
|
||||
if( $indexNumber !== false )
|
||||
{
|
||||
@@ -724,6 +1019,4 @@ class Advertisement extends ActiveRecord
|
||||
Db::i()->update( 'core_advertisements', "ad_impressions=ad_impressions+" . $by . ", ad_daily_impressions=ad_daily_impressions+" . $by, "ad_id IN(" . implode( ',', $ids ) . ")" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -615,7 +615,7 @@ class Alert extends Item
|
||||
$member = $member?: Member::loggedIn();
|
||||
if ( !$this->anonymous and $this->reply and $this->author()->member_id and $member->canUseMessenger())
|
||||
{
|
||||
$form = \IPS\core\Messenger\Conversation::create();
|
||||
$form = \IPS\core\Messenger\Conversation::create( showError: false );
|
||||
$form->class = 'ipsForm ipsForm--fullWidth';
|
||||
$form->elements['']['messenger_content']->defaultValue = '<blockquote class="ipsQuote">' . $this->content . "</blockquote><br>";
|
||||
$form->elements['']['messenger_content']->setValue(TRUE , FALSE );
|
||||
|
||||
@@ -20,6 +20,7 @@ use IPS\Data\Store;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Dispatcher;
|
||||
use IPS\Extensions\AnnouncementsAbstract;
|
||||
use IPS\File;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\CheckboxSet;
|
||||
@@ -207,10 +208,13 @@ class Announcement extends Item
|
||||
/* App and container specific announcements */
|
||||
foreach ( $extensions as $key => $extension )
|
||||
{
|
||||
/* @var AnnouncementsAbstract $extension */
|
||||
$id = $extension::$idField;
|
||||
|
||||
if ( $announcement->ids AND isset( Request::i()->$id ) )
|
||||
{
|
||||
$idsToCheck = $extension->getAnnouncementIds( $announcement->ids );
|
||||
|
||||
/* Are we viewing a content item */
|
||||
if ( Dispatcher::i()->dispatcherController instanceof Controller )
|
||||
{
|
||||
@@ -220,7 +224,7 @@ class Announcement extends Item
|
||||
{
|
||||
try
|
||||
{
|
||||
if( $announcement->location == $key AND in_array( $class::load( Request::i()->$id )->mapped('container'), explode( ',', $announcement->ids ) ) )
|
||||
if( $announcement->location == $key AND in_array( $class::load( Request::i()->$id )->mapped('container'), $idsToCheck ) )
|
||||
{
|
||||
$return[] = $announcement;
|
||||
}
|
||||
@@ -234,7 +238,7 @@ class Announcement extends Item
|
||||
{
|
||||
try
|
||||
{
|
||||
if( $announcement->location == $key AND in_array( Request::i()->$id , explode( ',', $announcement->ids ) ) )
|
||||
if( $announcement->location == $key AND in_array( Request::i()->$id , $idsToCheck ) )
|
||||
{
|
||||
$return[] = $announcement;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ class Approval extends ActiveRecord
|
||||
if ( Application::appIsEnabled( 'cloud' ) )
|
||||
{
|
||||
$reasons[] = 'image';
|
||||
$reasons[] = 'spam';
|
||||
}
|
||||
|
||||
foreach( Application::allExtensions( 'core', 'ApprovalReason' ) AS $ext )
|
||||
|
||||
@@ -413,4 +413,64 @@ class Assignment extends ActiveRecord
|
||||
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @param bool $includeItem Should the assigned item be attached to the response?
|
||||
* @return array
|
||||
* @apiresponse int id ID
|
||||
* @apiresponse int date Date of assignment
|
||||
* @apiresponse \IPS\Member assigned_by Member who assigned the item
|
||||
* @apiresponse string time_first_reply Time for the first reply
|
||||
* @apiresponse [\IPS\Member] assigned_to Assigned Members
|
||||
* @apiresponse string item_class Content Class
|
||||
* @apiresponse int item_id Content ID
|
||||
* @apiresponse [\IPS\Content\Item|\IPS\Content\Comment] content Content Object
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL, bool $includeItem = true ): array
|
||||
{
|
||||
$item = $this->item;
|
||||
$response = [];
|
||||
$response['id'] = $this->id;
|
||||
$response['date'] = DateTime::ts( $this->date )->rfc3339();
|
||||
$response['assigned_by'] = Member::load($this->by )->apiOutput( $authorizedMember);
|
||||
$response['time_first_reply'] = $this->timeToFirstReply();
|
||||
$assignedTo = $this->assignedTo();
|
||||
|
||||
if( $assignedTo )
|
||||
{
|
||||
$members = [];
|
||||
if( $assignedTo instanceof Team )
|
||||
{
|
||||
|
||||
foreach( $assignedTo->members() as $member )
|
||||
{
|
||||
$members[] = $member->apiOutput( $authorizedMember );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$members[] = $assignedTo->apiOutput( $authorizedMember );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$members = [];
|
||||
}
|
||||
$response['assigned_to'] = $members;
|
||||
$response['item_class'] = $this->item_class;
|
||||
$response['item_id'] = $this->item_id;
|
||||
if( $includeItem and $item )
|
||||
{
|
||||
$response['content'] = $item->apiOutput( $authorizedMember );
|
||||
}
|
||||
else
|
||||
{
|
||||
$response['content'] = NULL;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -49,13 +49,13 @@ class Table extends \IPS\Helpers\Table\Db
|
||||
*/
|
||||
public function __construct( $table, Url $baseUrl, $where=NULL, $forceIndex=NULL )
|
||||
{
|
||||
parent::__construct( $table, $baseUrl, $where, $forceIndex );
|
||||
|
||||
/* Do any multi-mod */
|
||||
if ( isset( Request::i()->modaction ) )
|
||||
{
|
||||
$this->multiMod();
|
||||
}
|
||||
|
||||
parent::__construct( $table, $baseUrl, $where, $forceIndex );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +154,6 @@ class Table extends \IPS\Helpers\Table\Db
|
||||
}
|
||||
}
|
||||
|
||||
Output::i()->redirect( Url::internal( 'app=core&module=system&controller=attachments', 'front', 'attachments' ), 'deleted' );
|
||||
Output::i()->redirect( $this->baseUrl, 'deleted' );
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@ class CustomBadge extends ActiveRecord
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
File::$safeFileExtensions[] = 'svg';
|
||||
$this->file = (string) File::create( 'core_CustomBadges', 'custombadge-' . $this->id . '.svg', $this->raw );
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ class CustomBadge extends ActiveRecord
|
||||
{
|
||||
$foreground = $this->foreground ?: 'currentColor';
|
||||
$icon = $this->compileIconElement();
|
||||
$numberOverlay = static::getBadgeNumberOverlay( $this->number_overlay );
|
||||
$numberOverlay = $this->getBadgeNumberOverlay( $this->number_overlay );
|
||||
|
||||
return <<<SVG
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" height="48">
|
||||
@@ -170,7 +171,7 @@ SVG;
|
||||
{
|
||||
$points = static::getRadialPoints( $this->sides, ( $this->sides === 4 and $this->rotation % 90 === 45) ? sqrt( 47.4*47.4*2 ) : 47.4 );
|
||||
$icon = $this->compileIconElement();
|
||||
$numberOverlay = static::getBadgeNumberOverlay( $this->number_overlay );
|
||||
$numberOverlay = $this->getBadgeNumberOverlay( $this->number_overlay );
|
||||
$foreground = $this->foreground ?: 'currentColor';
|
||||
|
||||
$path = "";
|
||||
@@ -198,7 +199,7 @@ SVG;
|
||||
protected function generateSVG_circle() : string
|
||||
{
|
||||
$icon = $this->compileIconElement();
|
||||
$numberOverlay = static::getBadgeNumberOverlay( $this->number_overlay );
|
||||
$numberOverlay = $this->getBadgeNumberOverlay( $this->number_overlay );
|
||||
$foreground = $this->foreground ?: 'currentColor';
|
||||
|
||||
return <<<SVG
|
||||
@@ -221,7 +222,7 @@ SVG;
|
||||
protected function generateSVG_star() : string
|
||||
{
|
||||
$icon = $this->compileIconElement();
|
||||
$numberOverlay = static::getBadgeNumberOverlay( $this->number_overlay );
|
||||
$numberOverlay = $this->getBadgeNumberOverlay( $this->number_overlay );
|
||||
$foreground = $this->foreground ?: 'currentColor';
|
||||
$innerPoints = static::getRadialPoints( $this->sides, (0.75 + (0.095 * ( $this->sides - 4 ) / 8)) * ( 45 * ( cos( pi() / $this->sides ) ) ), 0.5 );
|
||||
$outerPoints = static::getRadialPoints( $this->sides, 47.4 );
|
||||
@@ -250,7 +251,7 @@ SVG;
|
||||
protected function generateSVG_flower() : string
|
||||
{
|
||||
$icon = $this->compileIconElement();
|
||||
$numberOverlay = static::getBadgeNumberOverlay( $this->number_overlay );
|
||||
$numberOverlay = $this->getBadgeNumberOverlay( $this->number_overlay );
|
||||
$foreground = $this->foreground ?: 'currentColor';
|
||||
|
||||
$r = 45 * ((sin(pi() / $this->sides)) / (sin(pi() / $this->sides) + 1));
|
||||
@@ -354,16 +355,10 @@ SVG;
|
||||
{
|
||||
// complicated, but emojis don't center well. This was tested on MacOS using Apple Emoji font and accounts for whether the badge uses a background shape or not. Might be worth adding a note that emojis might not look correct on all devices (as the font is device-specific)
|
||||
$size = 50 + round( $sizeOffset * 2, 2 );
|
||||
$offset = 50 + round( ($this->shape === "no_shape" ? 24 : 18) + (($this->iconSize - 1) * ($this->shape === 'no_shape' ? 12.5 : 8.75) / 4), 2 );
|
||||
$offset = 50 + round( $size / 10, 2 );// - round( $size / 2, 2 );//round( ($this->shape === "no_shape" ? 24 : 18) + (($this->iconSize - 1) * ($this->shape === 'no_shape' ? 12.5 : 8.75) / 4), 2 );
|
||||
|
||||
return <<<SVG
|
||||
<style>
|
||||
svg text.svg__text_icon:not(#x) {
|
||||
line-height: {$size}px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
<text text-anchor="middle" font-size="{$size}" x="50" y="{$offset}" class="svg__text_icon">{$iconData['raw']}</text>
|
||||
<text text-anchor="middle" dominant-baseline="middle" font-size="{$size}" x="50" y="{$offset}" style="line-height: {$size}px;" class="svg__text_icon">{$iconData['raw']}</text>
|
||||
SVG;
|
||||
}
|
||||
}
|
||||
@@ -375,8 +370,24 @@ SVG;
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getBadgeNumberOverlay( int $number ) : string
|
||||
protected function getBadgeNumberOverlay( int $number ) : string
|
||||
{
|
||||
/* Do nothing if the number is empty */
|
||||
if( $number == 0 )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/* Check if the class using this badge uses the overlay */
|
||||
$class = $this->class;
|
||||
if( $class )
|
||||
{
|
||||
if( isset( $class::$customBadgeNumberOverlay ) and !$class::$customBadgeNumberOverlay )
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return <<<SVG
|
||||
<style>
|
||||
svg text.svg__text_overlay:not(#x) {
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace IPS\core;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\core\modules\admin\settings\dataLayer as DataLayerController;
|
||||
use IPS\Data\Store;
|
||||
@@ -23,6 +24,7 @@ use IPS\Member;
|
||||
use IPS\Member\Group;
|
||||
use IPS\Output;
|
||||
use IPS\Patterns\Singleton;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
@@ -284,6 +286,16 @@ HTML,
|
||||
'type' => 'string',
|
||||
'page_level' => true,
|
||||
],
|
||||
'content_is_followed' => [
|
||||
'event_keys' => ['content_*', 'filter_*', 'sort', 'file_download'],
|
||||
'pii' => false,
|
||||
'formatted_name' => 'content_is_followed',
|
||||
'description' => 'Whether the user if following the content item.',
|
||||
'short' => 'Content item followed',
|
||||
'enabled' => true,
|
||||
'type' => 'boolean',
|
||||
'page_level' => true,
|
||||
],
|
||||
'comment_type' => [
|
||||
'event_keys' => ['content_comment', 'content_react', 'content_quote'],
|
||||
'pii' => false,
|
||||
@@ -294,6 +306,17 @@ HTML,
|
||||
'type' => 'string',
|
||||
'page_level' => false,
|
||||
],
|
||||
'content_anonymous' => [
|
||||
'event_keys' => ['content_*', 'filter_*', 'sort', 'file_download'],
|
||||
'pii' => false,
|
||||
'formatted_name' => 'content_anonymous',
|
||||
'description' => 'Whether the relevant content (item/comment/review) was created anonymously. This attribute is only possible for certain content types, and when true the author ID and Name will be obfuscated',
|
||||
'short' => 'Content is anonymous',
|
||||
'enabled' => true,
|
||||
'type' => 'boolean',
|
||||
'page_level' => true,
|
||||
'default' => false,
|
||||
],
|
||||
'content_container_id' => [
|
||||
'event_keys' => ['content_*', 'filter_*', 'sort', 'file_download'],
|
||||
'pii' => false,
|
||||
@@ -334,6 +357,17 @@ HTML,
|
||||
'type' => 'string',
|
||||
'page_level' => true,
|
||||
],
|
||||
'message_recipient_count' => [
|
||||
'event_keys' => ['content_*', 'filter_*', 'sort', 'file_download'],
|
||||
'pii' => false,
|
||||
'formatted_name' => 'message_recipient_count',
|
||||
'description' => 'The number of recipients a message was sent to. If the content event or page context is not relevant to a message, this will be <code>null</code>',
|
||||
'short' => 'PM Recipient Count',
|
||||
'enabled' => true,
|
||||
'type' => 'number | null',
|
||||
'default' => null,
|
||||
'page_level' => true,
|
||||
],
|
||||
'ips_key' => [
|
||||
'event_keys' => ['*'],
|
||||
'pii' => false,
|
||||
@@ -700,7 +734,7 @@ HTML,
|
||||
$piiProperties = array_merge( $piiProperties, $this->relatedPiiProperties( $propertyKey ) );
|
||||
continue;
|
||||
}
|
||||
$value = $this->getSsoId( $value );
|
||||
$value = $value ? $this->getSsoId( $value ) : $value;
|
||||
}
|
||||
|
||||
$_values[$propertyKey] = $value;
|
||||
@@ -728,31 +762,28 @@ HTML,
|
||||
{
|
||||
$output = array();
|
||||
$piiExcludes = array();
|
||||
$pii = Settings::i()->core_datalayer_include_pii;
|
||||
foreach ( $this->propertiesConfiguration as $key => $property )
|
||||
{
|
||||
if ( $key === 'content_anonymous' and @$properties[$key] )
|
||||
{
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( 'author_id' ) );
|
||||
}
|
||||
|
||||
if ( !$property['enabled'] OR !isset( $properties[$key] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !$pii AND ( $property['pii'] ?? 0 ) )
|
||||
if ( !Settings::i()->core_datalayer_include_pii AND ( $property['pii'] ?? 0 ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check the member's pii preference. If we can verify the member's PII should NOT be exposed, don't allow it to be added */
|
||||
if ( Settings::i()->core_datalayer_member_pii_choice AND
|
||||
@$property['replace_with_sso'] AND
|
||||
( $originalID = array_search( $properties[$key], $this->ssoIds ) )
|
||||
)
|
||||
if ( @$property['replace_with_sso'] and ( $originalID = array_search( $properties[$key], $this->ssoIds ) ) and !$this->includeSSOForMember( $originalID ) )
|
||||
{
|
||||
$member = Member::load( $originalID );
|
||||
if ( $member->member_id AND $member->members_bitoptions['datalayer_pii_optout'] )
|
||||
{
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( $key ) );
|
||||
continue;
|
||||
}
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( $key ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
$output[$key] = $properties[$key];
|
||||
@@ -832,13 +863,13 @@ HTML,
|
||||
}
|
||||
|
||||
/* If this has PII, is PII allowed? */
|
||||
if ( ( $properties[$key]['pii'] ?? 0 ) AND ! Settings::i()->core_datalayer_include_pii )
|
||||
if ( @$properties[$key]['pii'] AND !Settings::i()->core_datalayer_include_pii )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Replace with sso id if needed */
|
||||
if ( $properties[$key]['replace_with_sso'] ?? 0 )
|
||||
if ( @$properties[$key]['replace_with_sso'] )
|
||||
{
|
||||
$value = $this->getSsoId( $value );
|
||||
}
|
||||
@@ -942,7 +973,7 @@ HTML,
|
||||
}
|
||||
|
||||
/* Logged In Member PII */
|
||||
if ( Settings::i()->core_datalayer_include_pii AND ( !Settings::i()->core_datalayer_member_pii_choice OR !Member::loggedIn()->members_bitoptions['datalayer_pii_optout'] ) )
|
||||
if ( $this->includeSSOForMember() )
|
||||
{
|
||||
if ( $properties['member_id']['enabled'] )
|
||||
{
|
||||
@@ -1002,6 +1033,11 @@ HTML,
|
||||
$piiExcludes = array();
|
||||
foreach ( $properties as $key => $property )
|
||||
{
|
||||
if ( $key === 'content_anonymous' and @$_context[$key] )
|
||||
{
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( 'author_id' ) );
|
||||
}
|
||||
|
||||
/* Make sure all are page level, enabled, and valid */
|
||||
if ( !$property['enabled'] or !$property['page_level'] )
|
||||
{
|
||||
@@ -1010,20 +1046,11 @@ HTML,
|
||||
$fkey = $property['formatted_name'];
|
||||
|
||||
/* If we can determine a member based on this property's PII, only include the related PII if we know that member hasn't opted out? */
|
||||
if (
|
||||
( $property['replace_with_sso'] ?? 0 ) AND
|
||||
Settings::i()->core_datalayer_include_pii AND
|
||||
Settings::i()->core_datalayer_member_pii_choice AND
|
||||
isset( $_context[ $key ] ) AND
|
||||
( $originalId = array_search( $_context[ $key ], $this->ssoIds ) )
|
||||
if ( @$property['replace_with_sso'] AND isset( $_context[ $key ] ) AND ( $originalId = array_search( $_context[ $key ], $this->ssoIds ) AND !( $this->includeSSOForMember( $originalId ) ) )
|
||||
)
|
||||
{
|
||||
$member = Member::load( $originalId );
|
||||
if ( !( $member->member_id AND !$member->members_bitoptions['datalayer_pii_optout'] ) )
|
||||
{
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( $key ) );
|
||||
continue;
|
||||
}
|
||||
$piiExcludes = array_merge( $piiExcludes, $this->relatedPiiProperties( $key ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Add context properties set by the system */
|
||||
@@ -1063,6 +1090,52 @@ HTML,
|
||||
return str_replace( ["\u{2028}", "\u{2029}"], ['', ''], $out );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Member|int|null $member
|
||||
* @return bool
|
||||
*/
|
||||
public function includeSSOForMember( Member|int|null $member=null ) :bool
|
||||
{
|
||||
/* Can we determine using settings? */
|
||||
if ( !Settings::i()->core_datalayer_include_pii )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Settings::i()->core_datalayer_include_pii and !Settings::i()->core_datalayer_member_pii_choice )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( is_null( $member ) )
|
||||
{
|
||||
$member = Member::loggedIn();
|
||||
}
|
||||
|
||||
static $res = [];
|
||||
$memberId = ( is_int( $member ) ? $member : $member->member_id ) ?: 0;
|
||||
if ( array_key_exists( 'member_' . $memberId, $res ) )
|
||||
{
|
||||
return $res['member_'.$memberId];
|
||||
}
|
||||
|
||||
if ( is_int( $member ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$member = Member::load( $memberId );
|
||||
}
|
||||
catch ( OutOfRangeException )
|
||||
{
|
||||
$res['member_'.$memberId] = false;
|
||||
return false; // edge case, but if the member doesn't exist, let's not expose PII on the grounds that there technically shouldn't really be PII to expose
|
||||
}
|
||||
}
|
||||
|
||||
$res['member_' . $memberId] = !$member->members_bitoptions['datalayer_pii_optout'];
|
||||
return $res['member_' . $memberId];
|
||||
}
|
||||
|
||||
|
||||
public static string $configCacheKey = 'dataLayerConfig';
|
||||
|
||||
@@ -1731,4 +1804,59 @@ HTML,
|
||||
|
||||
return array_merge( $properties, $additionalProperties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to determine whether
|
||||
*
|
||||
* @param string[]|string $features Features that must be available for this site's package for the data layer to be enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function enabled( array|string $features=[] ) : bool
|
||||
{
|
||||
if ( !Settings::i()->core_datalayer_enabled or Output::i()->bypassDataLayer )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if ( empty( $features ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Now let's determine if the data layer is enabled for the provided features. We'll cache the result */
|
||||
static $results = [];
|
||||
if ( is_string( $features ) )
|
||||
{
|
||||
$cacheKey = $features;
|
||||
$features = [ $features];
|
||||
}
|
||||
else
|
||||
{
|
||||
$cacheKey = implode( ',', sort( $features ) );
|
||||
}
|
||||
|
||||
if ( !array_key_exists( $cacheKey, $results ) )
|
||||
{
|
||||
/* assume it's enabled till we find one that's not */
|
||||
$results[$cacheKey] = true;
|
||||
foreach ( $features as $feature )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( !Bridge::i()->featureIsEnabled( $feature ) )
|
||||
{
|
||||
$results[$cacheKey] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
$results[$cacheKey] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results[$cacheKey];
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,14 @@ namespace IPS\core\Feature;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use Exception;
|
||||
use IPS\Content;
|
||||
use IPS\Content\Featurable;
|
||||
use IPS\core\Feature;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Table\Table;
|
||||
use IPS\Content\Tag as SystemTag;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Theme;
|
||||
use function defined;
|
||||
@@ -125,6 +128,18 @@ class PublicTable extends Table
|
||||
}
|
||||
|
||||
$where[] = array( '( promote_hide=0 )' );
|
||||
|
||||
/* Get only the valid classes. We may have disabled applications or legacy content that doesn't use the right trait */
|
||||
$classes = [];
|
||||
foreach( Content::routedClasses() as $_class )
|
||||
{
|
||||
if( IPS::classUsesTrait( $_class, Featurable::class ) )
|
||||
{
|
||||
$classes[] = $_class;
|
||||
}
|
||||
}
|
||||
|
||||
$where[] = [ Db::i()->in( 'promote_class', $classes ) ];
|
||||
|
||||
/* Get Count */
|
||||
$count = Db::i()->select( 'COUNT(*) as cnt', 'core_content_promote', $where )->first();
|
||||
|
||||
@@ -107,11 +107,9 @@ class Table extends TableHelper
|
||||
$it = Db::i()->select( '*', 'core_content_promote', $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ) );
|
||||
$rows = iterator_to_array( $it );
|
||||
|
||||
if ( ! count( $rows ) )
|
||||
{
|
||||
static::$rows = [];
|
||||
}
|
||||
else
|
||||
static::$rows = [];
|
||||
|
||||
if ( count( $rows ) )
|
||||
{
|
||||
foreach ( $rows as $index => $row )
|
||||
{
|
||||
|
||||
@@ -14,13 +14,19 @@ namespace IPS\core\Followed;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use InvalidArgumentException;
|
||||
use IPS\Api\Webhook;
|
||||
use IPS\Application;
|
||||
use IPS\Content\Tag;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Events\Event;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Patterns\ActiveRecord;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -59,6 +65,17 @@ class Follow extends ActiveRecord
|
||||
*/
|
||||
protected static array $multitonMap = array();
|
||||
|
||||
/**
|
||||
* Set Default Values (overriding $defaultValues)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setDefaultValues() : void
|
||||
{
|
||||
$this->notify_meta = '';
|
||||
$this->visible = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save
|
||||
*
|
||||
@@ -66,16 +83,24 @@ class Follow extends ActiveRecord
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
$sendWebhook = false;
|
||||
|
||||
if( $this->_new )
|
||||
{
|
||||
$this->visible = 1;
|
||||
$this->notify_sent = 0;
|
||||
$this->added = time();
|
||||
$sendWebhook = true;
|
||||
}
|
||||
|
||||
$this->id = md5( $this->app . ';' . $this->area . ';' . $this->rel_id . ';' . $this->member_id );
|
||||
|
||||
parent::save();
|
||||
|
||||
if ( $sendWebhook )
|
||||
{
|
||||
Webhook::fire( 'content_followed', $this);
|
||||
}
|
||||
|
||||
$this->clearFollowCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +118,7 @@ class Follow extends ActiveRecord
|
||||
* @apiresponse datetime|null followSent Date and time the last notification was sent, or NULL if none has been sent
|
||||
* @apiresponse string followName Textual representation of the content that was followed (title, name, etc.)
|
||||
* @apiresponse string followUrl URL to the content that was followed
|
||||
* @apiresponse \IPS\Member followMember Member following the item
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array|null
|
||||
{
|
||||
@@ -186,6 +212,7 @@ class Follow extends ActiveRecord
|
||||
'followSent' => $this->notify_sent ? DateTime::ts( $this->notify_sent )->rfc3339() : NULL,
|
||||
'followName' => $name ?? '',
|
||||
'followUrl' => ( isset( $url ) ? (string) $url : '' ),
|
||||
'followMember' => Member::load($this->member_id)->apiOutput($authorizedMember)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,9 +224,116 @@ class Follow extends ActiveRecord
|
||||
'core' => [
|
||||
'member' => Member::class,
|
||||
'club' => Club::class,
|
||||
'tag' => Tag::class
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the app and area parameters
|
||||
*
|
||||
* @param object $object
|
||||
* @return array
|
||||
*/
|
||||
public static function getFollowParameters( object $object ) : array
|
||||
{
|
||||
$objectClass = get_class( $object );
|
||||
foreach( static::$specialFollowClasses as $app => $areas )
|
||||
{
|
||||
foreach( $areas as $area => $class )
|
||||
{
|
||||
if( $class == $objectClass )
|
||||
{
|
||||
return [
|
||||
'app' => $app,
|
||||
'area' => $area
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$itemClass = ( $object instanceof Model ) ? $object::$contentItemClass : get_class( $object );
|
||||
return [
|
||||
'app' => $itemClass::$application,
|
||||
'area' => mb_strtolower( mb_substr( $objectClass, mb_strrpos( $objectClass, '\\' ) + 1 ) )
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a record based on an object and a member
|
||||
*
|
||||
* @param object $object
|
||||
* @param Member|null $member
|
||||
* @return static|null
|
||||
* @throws OutOfRangeException
|
||||
*/
|
||||
public static function loadByObject( object $object, ?Member $member=null ) : static|null
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Guests can never follow anything */
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
|
||||
$params = static::getFollowParameters( $object );
|
||||
$idColumn = $object::$databaseColumnId;
|
||||
try
|
||||
{
|
||||
$row = Db::i()->select( '*', 'core_follow', [ 'follow_app=? and follow_area=? and follow_rel_id=? and follow_member_id=?', $params['app'], $params['area'], $object->$idColumn, $member->member_id ] )->first();
|
||||
return static::constructFromData( $row );
|
||||
}
|
||||
catch( UnderflowException )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get__class() : string
|
||||
{
|
||||
if( isset( static::$specialFollowClasses[ $this->app ][ $this->area ] ) )
|
||||
{
|
||||
return static::$specialFollowClasses[ $this->app ][ $this->area ];
|
||||
}
|
||||
|
||||
return 'IPS\\' . $this->app . '\\' . IPS::mb_ucfirst( $this->area );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActiveRecord|null
|
||||
*/
|
||||
public function get__item() : ActiveRecord|null
|
||||
{
|
||||
$class = $this->_class;
|
||||
try
|
||||
{
|
||||
return $class::load( $this->rel_id );
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set as many properties as we can get from the object
|
||||
*
|
||||
* @param object $object
|
||||
* @return void
|
||||
*/
|
||||
public function set_object( object $object ) : void
|
||||
{
|
||||
$params = static::getFollowParameters( $object );
|
||||
$this->app = $params['app'];
|
||||
$this->area = $params['area'];
|
||||
|
||||
$idColumn = $object::$databaseColumnId;
|
||||
$this->rel_id = $object->$idColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class to follow
|
||||
*
|
||||
@@ -225,4 +359,25 @@ class Follow extends ActiveRecord
|
||||
return $classToFollow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete corresponding record from the follow cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clearFollowCache() : void
|
||||
{
|
||||
Db::i()->delete( 'core_follow_count_cache', array( 'id=? AND class=?', $this->rel_id, $this->_class ) );
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
Webhook::fire( 'content_unfollowed', $this );
|
||||
|
||||
Event::fire( 'onUnfollow', Member::load( $this->member_id ), [ $this->_item ] );
|
||||
|
||||
parent::delete();
|
||||
|
||||
/* Delete from the cache */
|
||||
$this->clearFollowCache();
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,12 @@ class SocialProfiles extends KeyValue
|
||||
*
|
||||
* @param string $name Form helper name
|
||||
* @param mixed|null $defaultValue Default value for the helper
|
||||
* @param bool $required Helper is required (TRUE) or not (FALSE)
|
||||
* @param bool|null $required Helper is required (TRUE) or not (FALSE)
|
||||
* @param array $options Options for the helper instance
|
||||
* @return void
|
||||
*@see \IPS\Helpers\Form\Abstract::__construct
|
||||
*/
|
||||
public function __construct(string $name, mixed $defaultValue=NULL, bool $required=FALSE, array $options=array() )
|
||||
public function __construct(string $name, mixed $defaultValue=NULL, ?bool $required=FALSE, array $options=array() )
|
||||
{
|
||||
$options = array_merge( $this->defaultOptions, $options );
|
||||
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Mailchimp
|
||||
* @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
|
||||
* @subpackage
|
||||
* @since 8/13/2025
|
||||
*/
|
||||
|
||||
namespace IPS\core;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DomainException;
|
||||
use IPS\Http\Request\Exception as RequestException;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Settings;
|
||||
use function str_replace;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class Mailchimp
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $apiKey = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $prefix = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static string $apiEndpoint = 'https://<prefix>.api.mailchimp.com/3.0';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/* Are we testing a new API key? */
|
||||
$this->apiKey = Settings::i()->mailchimp_api_key;
|
||||
$this->prefix = Settings::i()->mailchimp_server_prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Mailchimp|null
|
||||
*/
|
||||
protected static ?Mailchimp $instance = null;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function i() : static
|
||||
{
|
||||
if( static::$instance === null )
|
||||
{
|
||||
static::$instance = new static();
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection
|
||||
*
|
||||
* @param string|null $apiKey
|
||||
* @param string|null $prefix
|
||||
* @return bool
|
||||
*/
|
||||
public function test( ?string $apiKey=null, ?string $prefix=null ) : bool
|
||||
{
|
||||
/* Are we overriding the settings? */
|
||||
if( $apiKey !== null )
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
if( $prefix !== null )
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
if( $result = $this->makeRequest( 'ping', [], 'GET' ) )
|
||||
{
|
||||
if( isset( $result['health_status'] ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new DomainException( $result['title'] );
|
||||
}
|
||||
|
||||
throw new DomainException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all available lists
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLists() : array
|
||||
{
|
||||
$lists = [];
|
||||
if( $result = $this->makeRequest( "lists", [], 'GET' ) )
|
||||
{
|
||||
if( !empty( $result['lists'] ) )
|
||||
{
|
||||
foreach( $result['lists'] as $list )
|
||||
{
|
||||
$lists[ $list['id'] ] = $list['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a member to a list
|
||||
*
|
||||
* @param Member|null $member
|
||||
* @param bool $subscribe
|
||||
* @return bool
|
||||
*/
|
||||
public function subscribeMember( ?Member $member=null, bool $subscribe=true ) : bool
|
||||
{
|
||||
if( !$this->getListId() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$member = $member ?? Member::loggedIn();
|
||||
|
||||
/* Did we unsubscribe emails? */
|
||||
if( !$member->allow_admin_mails and $subscribe )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( $result = $this->makeRequest( "lists/" . $this->getListId(), [
|
||||
'members' => [
|
||||
[
|
||||
'email_address' => $member->email,
|
||||
'status' => ( $subscribe ? 'subscribed' : 'unsubscribed' )
|
||||
]
|
||||
],
|
||||
'update_existing' => true
|
||||
] ) )
|
||||
{
|
||||
/* Did we have any errors? */
|
||||
if( !empty( $result['errors'] ) )
|
||||
{
|
||||
/* Make sure it's for this email address */
|
||||
foreach( $result['errors'] as $error )
|
||||
{
|
||||
if( $error['email_address'] == $member->email )
|
||||
{
|
||||
$member->logHistory( 'core', 'mailchimp_subscribed', [
|
||||
'error_code' => $error['error_code'],
|
||||
'error_message' => $error['error']
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Log a successful subscription */
|
||||
$member->logHistory( 'core', 'mailchimp_subscribed', [ 'list' => $this->getListName(), 'action' => ( $subscribe ? 'subscribed' : 'unsubscribed' ) ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's email address
|
||||
*
|
||||
* @param Member|null $member
|
||||
* @param string $oldEmail
|
||||
* @return bool
|
||||
*/
|
||||
public function updateMemberEmail( ?Member $member , string $oldEmail ) : bool
|
||||
{
|
||||
if( !$this->getListId() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Did we unsubscribe emails? */
|
||||
if( !$member->allow_admin_mails )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( $result = $this->makeRequest( "lists/" . $this->getListId() . "/members/" . $this->subscriberHash( $oldEmail ), [
|
||||
'email_address' => $member->email,
|
||||
'status_if_new' => 'subscribed'
|
||||
], 'PUT' ) )
|
||||
{
|
||||
/* Did we have any errors? */
|
||||
if( !empty( $result['errors'] ) )
|
||||
{
|
||||
/* Make sure it's for either the old or new email address */
|
||||
foreach( $result['errors'] as $error )
|
||||
{
|
||||
if( $error['email_address'] == $member->email or $error['email_address'] == $oldEmail )
|
||||
{
|
||||
$member->logHistory( 'core', 'mailchimp_subscribed', [
|
||||
'error_code' => $error['error_code'],
|
||||
'error_message' => $error['error']
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Log a successful subscription */
|
||||
$member->logHistory( 'core', 'mailchimp_subscribed', [ 'list' => $this->getListName(), 'action' => 'email_update' ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the list to which we are subscribing
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getListId() : string
|
||||
{
|
||||
if( $value = json_decode( Settings::i()->mailchimp_lists, true ) )
|
||||
{
|
||||
return $value['id'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the list to which we are subscribing
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getListName() : string
|
||||
{
|
||||
if( $value = json_decode( Settings::i()->mailchimp_lists, true ) )
|
||||
{
|
||||
return $value['name'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash that mailchimp will recognize
|
||||
*
|
||||
* @param string $email
|
||||
* @return string
|
||||
*/
|
||||
protected function subscriberHash( string $email ) : string
|
||||
{
|
||||
return md5( strtolower( $email ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to Mailchimp
|
||||
*
|
||||
* @param string $endpoint
|
||||
* @param array $params
|
||||
* @param string $method
|
||||
* @return array|null
|
||||
*/
|
||||
protected function makeRequest( string $endpoint, array $params=array(), string $method='POST' ) : ?array
|
||||
{
|
||||
/* Make sure we have an API key */
|
||||
if( empty( $this->apiKey ) or empty( $this->prefix ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$method = strtolower( $method );
|
||||
$url = Url::external( str_replace( "<prefix>", $this->prefix, static::$apiEndpoint ) . '/' . ltrim( $endpoint, '/' ) );
|
||||
if( $method == 'get' and !empty( $params ) )
|
||||
{
|
||||
$url = $url->setQueryString( $params );
|
||||
$params = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$response = $url->request()
|
||||
->login( 'apiKey', $this->apiKey )
|
||||
->setHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json'
|
||||
])->$method( !empty( $params ) ? json_encode( $params ) : null );
|
||||
|
||||
return $response->decodeJson();
|
||||
}
|
||||
catch( RequestException $e )
|
||||
{
|
||||
Log::log( $e->getMessage(), 'mailchimp' );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ use InvalidArgumentException;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Content\Comment;
|
||||
use IPS\Content\Item;
|
||||
use IPS\Content\Permissions;
|
||||
use IPS\core\Alerts\Alert;
|
||||
use IPS\core\DataLayer;
|
||||
use IPS\core\Reports\Report;
|
||||
use IPS\core\Warnings\Warning;
|
||||
use IPS\DateTime;
|
||||
@@ -26,6 +28,7 @@ use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Member as FormMember;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Output;
|
||||
@@ -180,10 +183,28 @@ class Conversation extends Item
|
||||
* @param Member $member The member
|
||||
* @param Model|null $container Container (e.g. forum) ID, if appropriate
|
||||
* @param bool $showError If TRUE, rather than returning a boolean value, will display an error
|
||||
* @param Alert|null $alert Is this a reply to an alert?
|
||||
* @return bool
|
||||
*/
|
||||
public static function canCreate( Member $member, ?Model $container=NULL, bool $showError=FALSE ) : bool
|
||||
public static function canCreate( Member $member, ?Model $container=NULL, bool $showError=FALSE, Alert $alert = null ) : bool
|
||||
{
|
||||
/* If this conversation is associated with an alert, skip the rest of the permission checks, the user should be able to reply */
|
||||
if ( ( $alert and $alert instanceof Alert ) or isset( Request::i()->alert ) )
|
||||
{
|
||||
if( isset( Request::i()->alert ) )
|
||||
{
|
||||
$alert = Alert::load( Request::i()->alert );
|
||||
}
|
||||
try
|
||||
{
|
||||
if( $alert->forMember( Member::loggedIn() ) AND $alert->reply == Alert::REPLY_REQUIRED )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( OutOfRangeException $e ) {}
|
||||
}
|
||||
|
||||
/* Can we access the module? */
|
||||
if ( !parent::canCreate( $member, $container, $showError ) )
|
||||
{
|
||||
@@ -200,22 +221,6 @@ class Conversation extends 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( Request::i()->alert ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$alert = Alert::load( Request::i()->alert );
|
||||
|
||||
if( $alert->forMember( Member::loggedIn() ) AND $alert->reply == Alert::REPLY_REQUIRED )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch ( OutOfRangeException $e ) {}
|
||||
}
|
||||
|
||||
/* Have we exceeded our limit for the day/minute? */
|
||||
if ( $member->group['g_pm_perday'] !== -1 )
|
||||
@@ -401,8 +406,13 @@ class Conversation extends Item
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Extensions are last because all of the above needs to be honored */
|
||||
if( $permCheck = Permissions::can( 'receive', new static, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -475,6 +485,8 @@ class Conversation extends Item
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Does a member have permission to access?
|
||||
*
|
||||
@@ -485,6 +497,12 @@ class Conversation extends Item
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Extensions check */
|
||||
if( $permCheck = Permissions::can( 'view', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
/* Is the user part of the conversation? */
|
||||
foreach ( $this->maps() as $map )
|
||||
{
|
||||
@@ -993,4 +1011,59 @@ class Conversation extends Item
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse string|null title Title ( if available )
|
||||
* @apiresponse \IPS\Member author The member that created the item
|
||||
* @apiresponse datetime date Date
|
||||
* @apiresponse string content Content
|
||||
* @apiresponse string url URL
|
||||
* @apiresponse [\IPS\Member] participants Active users in this conversation
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
{
|
||||
$return = [
|
||||
'id' => $this->id,
|
||||
'title' => $this->mapped( 'title' ),
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => DateTime::ts( $this->mapped('date') )->rfc3339(),
|
||||
'content' => $this->content(),
|
||||
'url' => (string) $this->url(),
|
||||
'participants' => []
|
||||
];
|
||||
|
||||
foreach( $this->maps() as $map )
|
||||
{
|
||||
if( $map['map_user_active'] )
|
||||
{
|
||||
$return['participants'][] = Member::load( $map['map_user_id'] )->apiOutput( $authorizedMember );
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Comment|null $comment
|
||||
* @param array $createOrEditValues=[]
|
||||
* @param bool $clearCache=false
|
||||
* @return array
|
||||
*/
|
||||
public function getDataLayerProperties ( ?Comment $comment = null, array $createOrEditValues = [], bool $clearCache=false ): array
|
||||
{
|
||||
$properties = parent::getDataLayerProperties( $comment, $createOrEditValues, $clearCache );
|
||||
if ( isset( $properties['content_area'] ) )
|
||||
{
|
||||
$properties['content_area'] = Lang::load( Lang::defaultLanguage() )->addToStack( 'personal_conversations' );
|
||||
}
|
||||
|
||||
$updates = DataLayer::i()->filterProperties([ 'message_recipient_count' => $this->activeParticipants ]);
|
||||
|
||||
return array_replace( $properties, $updates );
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ use IPS\Content\Item;
|
||||
use IPS\Content\Reportable;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Notification;
|
||||
@@ -229,4 +230,32 @@ class Message extends Comment
|
||||
|
||||
parent::move( $item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse int item_id The ID number of the item this belongs to
|
||||
* @apiresponse \IPS\Member author Author
|
||||
* @apiresponse datetime date Date
|
||||
* @apiresponse string content The content
|
||||
* @apiresponse string url URL to content
|
||||
*/
|
||||
public function apiOutput( Member|null $authorizedMember = NULL ): array
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$itemColumn = static::$databaseColumnMap['item'];
|
||||
$return = array(
|
||||
'id' => $this->$idColumn,
|
||||
'item_id' => $this->$itemColumn,
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => DateTime::ts( $this->mapped('date') )->rfc3339(),
|
||||
'content' => $this->content(),
|
||||
'url' => (string) $this->url()
|
||||
);
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -669,8 +669,14 @@ class Profanity extends ActiveRecord implements JsonSerializable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have tags, and it's still null, that means none of the tags were worth checking (e.g. local attachments) */
|
||||
if( count( $tags ) and $good === null )
|
||||
{
|
||||
$good = true;
|
||||
}
|
||||
|
||||
if ( $good !== TRUE AND Settings::i()->ipb_url_filter_option == 'white' )
|
||||
if ( count($tags) and $good !== TRUE AND Settings::i()->ipb_url_filter_option == 'white' )
|
||||
{
|
||||
return $good;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use IPS\core\Reports\Comment as ReportComment;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Email;
|
||||
use IPS\Events\Event;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Log;
|
||||
@@ -149,6 +150,8 @@ class Report extends Item
|
||||
*/
|
||||
public static bool $skipIndexNow = TRUE;
|
||||
|
||||
protected static array|bool $_bypassDataLayerEvents = true;
|
||||
|
||||
|
||||
/**
|
||||
* Get all the report types for this report index
|
||||
@@ -189,6 +192,9 @@ class Report extends Item
|
||||
$this->status = $status;
|
||||
$this->save();
|
||||
|
||||
/* Fire an event */
|
||||
Event::fire( 'onCreateOrEdit', $this, [ array(), false ] );
|
||||
|
||||
/* We only need to notify if there is an outcome */
|
||||
if ( $status == static::STATUS_CLOSED or static::STATUS_REJECTED )
|
||||
{
|
||||
@@ -311,6 +317,9 @@ class Report extends Item
|
||||
$comment = ReportComment::create( $this, $content, TRUE, NULL, NULL, $member, new DateTime );
|
||||
$comment->save();
|
||||
|
||||
/* And fire an event for the comment */
|
||||
Event::fire( 'onCreateOrEdit', $comment, [ array(), true ] );
|
||||
|
||||
/* And add to the moderator log */
|
||||
Session::i()->modLog( 'modlog__action_update_report_status', array( $this->url()->__toString() => FALSE ) );
|
||||
}
|
||||
@@ -908,8 +917,6 @@ class Report extends Item
|
||||
}
|
||||
}
|
||||
|
||||
$extensionWhereClause = '';
|
||||
|
||||
/* Skip this if we have global permissions */
|
||||
if( is_array( $perms ) and !$perms['can_view_reports'] )
|
||||
{
|
||||
@@ -1001,4 +1008,43 @@ class Report extends Item
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @param bool $includeItem Should the assigned item be attached to the response?
|
||||
* @return array
|
||||
* @apiresponse int id ID
|
||||
* @apiresponse int date Date of report
|
||||
* @apiresponse \IPS\Member reported_by Member who reported the item
|
||||
* @apiresponse string item_class Content Class
|
||||
* @apiresponse int item_id Content ID
|
||||
* @apiresponse [\IPS\Content\Item|\IPS\Content\Comment] content Content Object
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
{
|
||||
$className = $this->class;
|
||||
try
|
||||
{
|
||||
$reportedContent = $className::load( $this->content_id );
|
||||
}
|
||||
catch ( OutOfRangeException $ex )
|
||||
{
|
||||
/* No content, no moderation, no cry */
|
||||
$reportedContent = FALSE;
|
||||
}
|
||||
|
||||
$response = [];
|
||||
$response['id'] = $this->id;
|
||||
$response['date'] = $this->mapped('date');
|
||||
$response['reported_by'] = $this->author()->apiOutput($authorizedMember);
|
||||
$response['item_class'] = $this->class;
|
||||
$response['item_id'] = $this->content_id;
|
||||
$response['content'] = $reportedContent ? $reportedContent->apiOutput($authorizedMember) : NULL;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ use IPS\Http\Request\Exception;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Http\Url\Exception as UrlException;
|
||||
use IPS\Image;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Settings;
|
||||
@@ -602,20 +603,9 @@ class Import extends Model
|
||||
'rss_imported_import_id' => $this->id
|
||||
), TRUE );
|
||||
|
||||
if ( $this->auto_follow )
|
||||
if ( $this->auto_follow and IPS::classUsesTrait( $item, Content\Followable::class ) )
|
||||
{
|
||||
Db::i()->insert( 'core_follow', array(
|
||||
'follow_id' => md5( $class::$application . ';' . $followArea . ';' . $item->$idColumn . ';' . $member->member_id ),
|
||||
'follow_app' => $class::$application,
|
||||
'follow_area' => $followArea,
|
||||
'follow_rel_id' => $item->$idColumn,
|
||||
'follow_member_id' => $member->member_id,
|
||||
'follow_is_anon' => 0,
|
||||
'follow_added' => time(),
|
||||
'follow_notify_do' => 1,
|
||||
'follow_notify_freq' => 'immediate',
|
||||
'follow_visible' => 1,
|
||||
) );
|
||||
$item->follow( 'immediate', true, $member );
|
||||
}
|
||||
|
||||
/* Re-index to pick up any changes to hidden, etc */
|
||||
|
||||
@@ -12,16 +12,31 @@ namespace IPS\core\Statistics;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DateInterval;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\DateRange;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function array_keys;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_numeric;
|
||||
use function strval;
|
||||
use function time;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -37,7 +52,14 @@ abstract class Chart
|
||||
/**
|
||||
* @brief Controller
|
||||
*/
|
||||
public ?string $controller = NULL;
|
||||
public ?string $controller = 'NULL';
|
||||
|
||||
/**
|
||||
* Whether this chart can be saved so that extra tabs exist. When this is true, the chart can only be saved to reports
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $noTabs = false;
|
||||
|
||||
/**
|
||||
* Get Chart
|
||||
@@ -112,9 +134,9 @@ abstract class Chart
|
||||
{
|
||||
if ( !is_array( $data ) )
|
||||
{
|
||||
$data = Db::i()->select( '*', 'core_saved_charts', array( "chart_id=? AND chart_member=?", $data, $check->member_id ) )->first();
|
||||
$data = Db::i()->select( '*', 'core_saved_charts', array( "chart_id=? AND (chart_member=? OR chart_member IS NULL)", $data, $check->member_id ) )->first();
|
||||
}
|
||||
else if ( $data['chart_member'] !== $check->member_id )
|
||||
else if ( $data['chart_member'] !== $check->member_id and $data['chart_member'] !== null and $data['chart_member'] !== 0 )
|
||||
{
|
||||
throw new UnderflowException;
|
||||
}
|
||||
@@ -123,9 +145,9 @@ abstract class Chart
|
||||
{
|
||||
if ( !is_array( $data ) )
|
||||
{
|
||||
$data = Db::i()->select( '*','core_saved_charts', array( "chart_id=? AND chart_member=?", $data, Member::loggedIn()->member_id ) )->first();
|
||||
$data = Db::i()->select( '*', 'core_saved_charts', array( "chart_id=? AND (chart_member=? OR chart_member IS NULL OR chart_member=0)", $data, Member::loggedIn()->member_id ) )->first();
|
||||
}
|
||||
else if ( $data['chart_member'] !== Member::loggedIn()->member_id )
|
||||
else if ( $data['chart_member'] !== Member::loggedIn()->member_id and $data['chart_member'] !== null and $data['chart_member'] !== 0 )
|
||||
{
|
||||
throw new UnderflowException;
|
||||
}
|
||||
@@ -157,10 +179,14 @@ abstract class Chart
|
||||
$chart->showFilterTabs = FALSE;
|
||||
$chart->showIntervals = FALSE;
|
||||
$chart->showDateRange = FALSE;
|
||||
|
||||
$dateRange = static::getChartDateRanges( is_numeric( Request::i()->report_id ) ? (int) Request::i()->report_id : null );
|
||||
$chart->start = $dateRange['start'];
|
||||
$chart->end = $dateRange['end'];
|
||||
|
||||
return $chart;
|
||||
}
|
||||
catch( UnderflowException $e )
|
||||
catch( UnderflowException )
|
||||
{
|
||||
/* Chart doesn't exist, or isn't owned by $member */
|
||||
throw new OutOfRangeException;
|
||||
@@ -180,15 +206,34 @@ abstract class Chart
|
||||
public static function getChartsForMember( Url $url, bool $idsOnly = FALSE, ?Member $member = NULL ): array
|
||||
{
|
||||
$member ??= Member::loggedIn();
|
||||
|
||||
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
throw new InvalidArgumentException;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach( Db::i()->select( '*', 'core_saved_charts', array( "chart_member=?", $member->member_id ) ) AS $chart )
|
||||
|
||||
if ( Bridge::i()->featureIsEnabled( "community_health_stats" ) and !empty( $url->queryString['communityhealth'] ) )
|
||||
{
|
||||
$charts = Bridge::i()->getCommunityHealthCharts();
|
||||
}
|
||||
else
|
||||
{
|
||||
$return = [];
|
||||
$reportID = is_numeric( Request::i()->report_id ) ? (int) Request::i()->report_id : 0;
|
||||
$where = [];
|
||||
if ( $reportID > 0 )
|
||||
{
|
||||
$where[] = array( "(chart_member IS NULL OR chart_member=? OR chart_member=0) AND chart_report_id=?", $member->member_id, $reportID );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( "(chart_member IS NULL OR chart_member=0) AND chart_report_id IS NULL", $member->member_id );
|
||||
}
|
||||
|
||||
$charts = Db::i()->select( '*', 'core_saved_charts', $where );
|
||||
}
|
||||
|
||||
foreach( $charts AS $chart )
|
||||
{
|
||||
if ( $idsOnly )
|
||||
{
|
||||
@@ -198,13 +243,12 @@ abstract class Chart
|
||||
{
|
||||
try
|
||||
{
|
||||
$controller = explode( '_', $chart['chart_controller'] );
|
||||
$return[$chart['chart_id']] = array(
|
||||
'chart' => static::constructMemberChartFromData( $chart, $url, $member ),
|
||||
'data' => $chart
|
||||
);
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
catch( OutOfRangeException $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -213,4 +257,222 @@ abstract class Chart
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the saved blocks for the current page.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getSavedBlocks() : array
|
||||
{
|
||||
static $toDisplay = null;
|
||||
if ( !is_array( $toDisplay ) )
|
||||
{
|
||||
$where = [[ "(stat_member IS NULL OR stat_member=?)", Member::loggedIn()->member_id ]];
|
||||
if ( Request::i()->report_id )
|
||||
{
|
||||
$where[] = [ "stat_report_id=?", Request::i()->report_id ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = [ "stat_report_id IS NULL" ];
|
||||
}
|
||||
|
||||
$allExtensions = Application::allExtensions( 'core', 'OverviewStatistics', TRUE );
|
||||
$toDisplay = [];
|
||||
foreach ( Db::i()->select( "*", "core_saved_overview_stats", $where ) as $savedStat )
|
||||
{
|
||||
$key = $savedStat['stat_app'] . "_" . $savedStat['stat_extension'];
|
||||
if ( isset( $allExtensions[ $key ] ) and in_array( $savedStat['stat_subblock'], $allExtensions[ $key ]->getBlocks() ) )
|
||||
{
|
||||
$details = $allExtensions[ $key ]->getBlockDetails( $savedStat['stat_subblock'] );
|
||||
$details['title'] = $savedStat['stat_title'];
|
||||
|
||||
$toDisplay[] = [
|
||||
"blockKey" => $key,
|
||||
"subblock" => $savedStat['stat_subblock'],
|
||||
"saved_stat_id" => $savedStat['id'],
|
||||
"details" => $details,
|
||||
'page' => $allExtensions[ $key ]->page,
|
||||
'extension' => $allExtensions[$key],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $toDisplay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date range selector that is at the top of the saved reports acp page
|
||||
* @return string
|
||||
*/
|
||||
public static function getBlockDateRangeForm( ?int $reportId=null ) : string
|
||||
{
|
||||
$options = array(
|
||||
'7' => 'last_week',
|
||||
'30' => 'last_month',
|
||||
'90' => 'last_three_months',
|
||||
'180' => 'last_six_months',
|
||||
'365' => 'last_year',
|
||||
'0' => 'alltime',
|
||||
'-1' => 'custom'
|
||||
);
|
||||
|
||||
$defaultRange = '7';
|
||||
$dateRange = null;
|
||||
if ( ( is_numeric( Request::i()->predate ) and isset( $options[Request::i()->predate] ) ) or ( is_numeric( @Request::i()->date['start'] ) and is_numeric( @Request::i()->date['end'] ) ) )
|
||||
{
|
||||
if ( is_numeric( @Request::i()->date['start'] ) and is_numeric( @Request::i()->date['end'] ) )
|
||||
{
|
||||
$dateRange = [
|
||||
'start' => Request::i()->date['start'],
|
||||
'end' => Request::i()->date['end']
|
||||
];
|
||||
|
||||
$defaultRange = '-1';
|
||||
}
|
||||
else
|
||||
{
|
||||
$defaultRange = Request::i()->predate;
|
||||
}
|
||||
}
|
||||
else if ( is_int( $reportId ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$currentReport = Db::i()->select( '*', 'core_reports', ['id=?', $reportId] )->first();
|
||||
$defaultRange = strval($currentReport['report_range_timescale'] ?? '7');
|
||||
if ( isset( $currentReport['report_range_start'] ) or isset( $currentReport['report_range_end'] ) )
|
||||
{
|
||||
$dateRange = [
|
||||
'start'=> $currentReport['report_range_start'],
|
||||
'end' => $currentReport['report_range_end'],
|
||||
];
|
||||
}
|
||||
}
|
||||
catch ( \UnderflowException ) {}
|
||||
}
|
||||
|
||||
$form = new Form( 'posts', 'update' );
|
||||
$form->add( new Select( 'predate', $defaultRange, FALSE, array( 'options' => $options, 'toggles' => array( '-1' => array( 'dateFilterInputs' ) ) ) ) );
|
||||
$form->add( new DateRange( 'date', $dateRange, FALSE, array(), NULL, NULL, NULL, 'dateFilterInputs' ) );
|
||||
$form->class .= " ipsBox ipsPull i-padding_2";
|
||||
|
||||
if ( is_int( $reportId ) and $values = $form->values() )
|
||||
{
|
||||
$update = [
|
||||
'report_range_timescale' => (int) $values['predate'],
|
||||
'report_range_start' => @$values['date']['start']?->getTimestamp() ?: null,
|
||||
'report_range_end' => @$values['date']['end']?->getTimestamp() ?: null
|
||||
];
|
||||
Db::i()->update( 'core_reports', $update, ['id=?', $reportId] );
|
||||
if ( Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->json( ['message' => 'ok'], 200 );
|
||||
}
|
||||
Output::i()->redirect( (string) Url::internal( 'app=core&module=overview&controller=mycharts' ) );
|
||||
}
|
||||
|
||||
return $form->customTemplate( array( Theme::i()->getTemplate( 'stats' ), 'filtersOverviewForm' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getAppsToFilterBy()
|
||||
{
|
||||
static $apps = null;
|
||||
if ( !is_array( $apps ) )
|
||||
{
|
||||
$blocks = static::getSavedBlocks();
|
||||
$apps = [];
|
||||
foreach ( $blocks as $block )
|
||||
{
|
||||
if ( isset( $block['details']['app'] ) and !isset( $apps[ $block['details']['app'] ] ) )
|
||||
{
|
||||
$apps[ $block['details']['app'] ] = Application::load( $block['details']['app'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $apps;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int|null $reportId The ID of the report, or leave null to use the default saved charts function
|
||||
* @return array{start: DateTime|null, end: DateTime|null}
|
||||
*/
|
||||
public static function getChartDateRanges( ?int $reportId=null )
|
||||
{
|
||||
// It is possible this is included right in the request
|
||||
if ( isset( Request::i()->predate ) and is_numeric( Request::i()->predate ) and intval( Request::i()->predate ) >= 0 )
|
||||
{
|
||||
$days = (int) Request::i()->predate;
|
||||
if ( $days <= 0 )
|
||||
{
|
||||
return [
|
||||
'start' => null,
|
||||
'end' => null,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'end' => DateTime::ts( time() )->setTime( 23, 59, 59 ),
|
||||
'start' => DateTime::ts( time() )->setTime( 0, 0, 0 )->sub( new DateInterval( "P{$days}D" ) )
|
||||
];
|
||||
}
|
||||
else if ( isset( Request::i()->date['start'] ) and isset( Request::i()->date['end'] ) ) // the way the JS works, the start and end are sent even if it just relies on the predate.
|
||||
{
|
||||
$start = is_numeric( Request::i()->date['start'] ) ? DateTime::ts( (int) Request::i()->date['start'] ) : null;
|
||||
$end = is_numeric( Request::i()->date['end'] ) ? DateTime::ts( (int) Request::i()->date['end'] ) : null;
|
||||
|
||||
return [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
];
|
||||
}
|
||||
|
||||
if ( is_null( $reportId ) )
|
||||
{
|
||||
if ( isset( Settings::i()->core_saved_charts_range_timescale ) and is_numeric( Settings::i()->core_saved_charts_range_timescale ) and intval( Settings::i()->core_saved_charts_range_timescale ) >= 0 )
|
||||
{
|
||||
$days = (int) Settings::i()->core_saved_charts_range_timescale;
|
||||
if ( $days <= 0 )
|
||||
{
|
||||
return [
|
||||
'start' => null,
|
||||
'end' => null,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'end' => DateTime::ts( time() )->setTime( 23, 59, 59 ),
|
||||
'start' => DateTime::ts( time() )->setTime( 0, 0, 0 )->sub( new DateInterval( "P{$days}D" ) )
|
||||
];
|
||||
}
|
||||
|
||||
$start = is_numeric( Settings::i()->core_saved_charts_range_start ) ? DateTime::ts( (int) Settings::i()->core_saved_charts_range_start ) : null;
|
||||
$end = is_numeric( Settings::i()->core_saved_charts_range_end ) ? DateTime::ts( (int) Settings::i()->core_saved_charts_range_end ) : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$row = Db::i()->select( "*", "core_reports", [ "id=?", $reportId ?: 0 ] )->first();
|
||||
$start = is_int( $row['report_range_start'] ) ? DateTime::ts( $row['report_range_start'] ) : null;
|
||||
$end = is_int( $row['report_range_end'] ) ? DateTime::ts( $row['end'] ) : null;
|
||||
}
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
$start = null;
|
||||
$end = null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"start" => $start,
|
||||
"end" => $end
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -441,7 +441,15 @@ class Stream extends Model
|
||||
$value = $containers->$class ?? 0;
|
||||
|
||||
/* @var Item $class */
|
||||
$field = new Node( 'stream_containers_' . $class::$title, $value, FALSE, array( 'class' => $nodeClass, 'clubs' => TRUE, 'multiple' => TRUE, 'zeroVal' => 'all', 'forceOwner' => FALSE, 'subnodes' => FALSE ), NULL, NULL, NULL, 'stream_containers_' . $class::$title );
|
||||
$field = new Node( 'stream_containers_' . $class::$title, $value, FALSE, array(
|
||||
'class' => $nodeClass,
|
||||
'clubs' => TRUE,
|
||||
'multiple' => TRUE,
|
||||
'zeroVal' => 'all',
|
||||
'forceOwner' => FALSE,
|
||||
'subnodes' => FALSE,
|
||||
'nodeGroups' => TRUE,
|
||||
), NULL, NULL, NULL, 'stream_containers_' . $class::$title );
|
||||
$field->label = Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle );
|
||||
$form->add( $field );
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use IPS\Application;
|
||||
use IPS\Content;
|
||||
use IPS\Content\Comment;
|
||||
use IPS\Content\Item;
|
||||
use IPS\core\DataLayer;
|
||||
use IPS\core\Messenger\Conversation;
|
||||
use IPS\core\Messenger\Message;
|
||||
use IPS\DateTime;
|
||||
@@ -33,11 +34,13 @@ use IPS\Helpers\Form\Editor;
|
||||
use IPS\Helpers\Form\Number;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Lang;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Notification;
|
||||
use IPS\Output;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Settings;
|
||||
@@ -65,6 +68,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
class Warning extends Item
|
||||
{
|
||||
/* !\IPS\Patterns\ActiveRecord */
|
||||
protected static array|bool $_bypassDataLayerEvents = true;
|
||||
|
||||
/**
|
||||
* @brief Database Table
|
||||
@@ -371,7 +375,8 @@ class Warning extends Item
|
||||
{
|
||||
$reasons['other'] = 'core_warn_reason_other';
|
||||
}
|
||||
|
||||
|
||||
$first = null;
|
||||
foreach( $roots AS $root )
|
||||
{
|
||||
$first = $root;
|
||||
@@ -379,15 +384,15 @@ class Warning extends Item
|
||||
}
|
||||
|
||||
/* Build the form */
|
||||
$elements[] = new Select( 'warn_reason', NULL, TRUE, array( 'options' => $reasons ) );
|
||||
$elements[] = new Number( 'warn_points', 0, FALSE, array( 'valueToggles' => array( 'warn_remove' ), 'disabled' => ( !$first->points_override ) ) );
|
||||
$elements[] = new Select( 'warn_reason', NULL, !empty( $reasons ), array( 'options' => $reasons ) );
|
||||
$elements[] = new Number( 'warn_points', 0, FALSE, array( 'valueToggles' => array( 'warn_remove' ), 'disabled' => ( $first and !$first->points_override ) ) );
|
||||
$elements[] = new Date( 'warn_remove', -1, FALSE, array( 'time' => TRUE, 'unlimited' => -1, 'unlimitedLang' => 'never' ), NULL, NULL, NULL, 'warn_remove' );
|
||||
$elements[] = new Number( 'warn_cheeve_point_reduction', $first->cheev_point_reduction, FALSE, array( 'disabled' => ( !$first->cheev_override ) ) );
|
||||
$elements[] = new Number( 'warn_cheeve_point_reduction', $first?->cheev_point_reduction, FALSE, array( 'disabled' => ( !$first?->cheev_override ) ) );
|
||||
$elements[] = new Editor( 'warn_member_note', NULL, FALSE, array( 'app' => 'core', 'key' => 'Modcp', 'autoSaveKey' => "warn-member-" . Request::i()->id, 'attachIds' => ( $item === NULL ? NULL : array( $item->id, NULL, 'member' ) ), 'minimize' => 'warn_member_note_placeholder' ) );
|
||||
$elements[] = new Editor( 'warn_mod_note', NULL, FALSE, array( 'app' => 'core', 'key' => 'Modcp', 'autoSaveKey' => "warn-mod-" . Request::i()->id, 'attachIds' => ( $item === NULL ? NULL : array( $item->id, NULL, 'mod' ) ), 'minimize' => 'warn_mod_note_placeholder' ) );
|
||||
$elements[] = new CheckboxSet( 'warn_punishment', array(), FALSE, array(
|
||||
'options' => array( 'mq' => 'warn_mq', 'rpa' => 'warn_rpa', 'suspend' => 'warn_suspend' ),
|
||||
'toggles' => array( 'mq' => array( 'form_warn_mq' ), 'rpa' => array( 'form_warn_rpa' ), 'suspend' => array( 'form_warn_suspend' ) ),
|
||||
'toggles' => array( 'mq' => array( 'warn_mq' ), 'rpa' => array( 'warn_rpa' ), 'suspend' => array( 'warn_suspend' ) ),
|
||||
) );
|
||||
$elements[] = new Date( 'warn_mq', -1, FALSE, array( 'time' => TRUE, 'unlimited' => -1, 'unlimitedLang' => 'indefinitely' ), function( $val )
|
||||
{
|
||||
@@ -523,6 +528,12 @@ class Warning extends Item
|
||||
$member->save();
|
||||
$member->logHistory( 'core', 'warning', array( 'wid' => $this->id, 'points' => $this->points, 'reason' => $this->reason, 'consequences' => $consequences ) );
|
||||
|
||||
/* Data Layer */
|
||||
if ( DataLayer::enabled( 'analytics_full' ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'warning_created', $this->getDataLayerProperties() );
|
||||
}
|
||||
|
||||
/* Moderator log */
|
||||
Session::i()->modLog( 'modlog__action_warn_user', array( $member->name => FALSE ) );
|
||||
|
||||
@@ -531,6 +542,36 @@ class Warning extends Item
|
||||
parent::processAfterCreate( $comment, $values );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Comment|null $comment
|
||||
* @param array $createOrEditValues
|
||||
* @param bool $clearCache=false
|
||||
* @return array
|
||||
*/
|
||||
public function getDataLayerProperties( ?Comment $comment = null, array $createOrEditValues = [], bool $clearCache=false ): array
|
||||
{
|
||||
if ( $comment !== null )
|
||||
{
|
||||
return parent::getDataLayerProperties( $comment, $createOrEditValues, $clearCache );
|
||||
}
|
||||
|
||||
static $properties = null;
|
||||
if ( $clearCache or $properties === null )
|
||||
{
|
||||
$lang = Lang::load( Lang::defaultLanguage() );
|
||||
$reasonString = $lang->addToStack( ( $this->reason and $lang->checkKeyExists( Reason::$titleLangPrefix . $this->reason ) ) ? ( Reason::$titleLangPrefix . $this->reason ) : 'core_warn_reason_other' );
|
||||
$lang->parseOutputForDisplay( $reasonString );
|
||||
$properties = DataLayer::i()->filterProperties([
|
||||
'warn_reason' => $reasonString,
|
||||
'warn_reason_id' => $this->reason,
|
||||
'warn_points' => $this->points,
|
||||
'warn_member_id' => DataLayer::i()->getSsoId( $this->member ),
|
||||
'warn_mod_id' => DataLayer::i()->getSsoId( $this->moderator ),
|
||||
]);
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the warning values
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user