Version 4.7.0
This commit is contained in:
1 parent
517a5e1f70
commit
8ba7d43898
1933 files changed
+65613
-11332
No files matched your search
@@ -77,22 +77,25 @@ abstract class _AbstractAchievementAction
|
||||
/**
|
||||
* Get the labels for the people this action might give awards to
|
||||
*
|
||||
* @param array|NULL $filters Current filter values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function awardOptions(): array
|
||||
public function awardOptions( ?array $filters ): array
|
||||
{
|
||||
return [
|
||||
'subject' => '',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the "other" people we need to award =stuff to
|
||||
*
|
||||
* @param mixed $extra Any additional information about what is happening (e.g. if a post is being made: the post object)
|
||||
* @param mixed $extra Any additional information about what is happening (e.g. if a post is being made: the post object)
|
||||
* @param array|NULL $filters Current filter values
|
||||
* @return array
|
||||
*/
|
||||
public function awardOther( $extra = NULL ): array
|
||||
public function awardOther( $extra = NULL, ?array $filters = NULL ): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
+31
-4
@@ -122,7 +122,7 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
$return['milestone'] = $nthFilter;
|
||||
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
{
|
||||
$item = $extra->item();
|
||||
}
|
||||
|
||||
|
||||
if ( isset( $filters[ 'nodes_' . str_replace( '\\', '-', \get_class( $item ) ) ] ) )
|
||||
{
|
||||
if ( !\in_array( $item->container()->_id, $filters[ 'nodes_' . str_replace( '\\', '-', \get_class( $item ) ) ] ) )
|
||||
@@ -193,9 +193,36 @@ abstract class _AbstractContentAchievementAction extends AbstractAchievementActi
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
else
|
||||
{
|
||||
/* Make sure the class is a contentRouter approved class, mostly to stop private messages being awarded points */
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $extension )
|
||||
{
|
||||
foreach ( $extension->classes as $class )
|
||||
{
|
||||
if ( \get_class( $extra ) == $class )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
elseif ( isset( $class::$commentClass ) and \get_class( $extra ) == $class::$commentClass )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
elseif ( isset( $class::$reviewClass ) and \get_class( $extra ) == $class::$reviewClass )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Stop PHPStorm from complaining about incorrect return types >_> */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,7 @@ class _Badge extends \IPS\Node\Model
|
||||
'app' => 'core',
|
||||
'module' => 'achievements',
|
||||
'prefix' => 'badges_',
|
||||
'all' => 'badges_manage',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -135,8 +136,11 @@ class _Badge extends \IPS\Node\Model
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
/* Allow SVGs without the obscure hash removing the file extension */
|
||||
\IPS\File::$safeFileExtensions[] = 'svg';
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'badge_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_badges_{$this->id}" : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'badge_image', $this->image ? \IPS\File::get( 'core_Badges', $this->image ) : NULL, TRUE, array( 'obscure' => FALSE, 'checkImage' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::$imageExtensions, ['svg'] ), 'storageExtension' => 'core_Badges', 'allowStockPhotos' => FALSE ), function( $val ) {
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'badge_image', $this->image ? \IPS\File::get( 'core_Badges', $this->image ) : NULL, TRUE, array( 'obscure' => TRUE, 'checkImage' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::supportedExtensions(), ['svg'] ), 'storageExtension' => 'core_Badges', 'allowStockPhotos' => FALSE ), function( $val ) {
|
||||
if( !$val )
|
||||
{
|
||||
throw new \DomainException('achievements_bad_image');
|
||||
@@ -228,12 +232,47 @@ class _Badge extends \IPS\Node\Model
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$stats = $this->getBadgeStats();
|
||||
|
||||
return ( 100 / $stats['memberCount'] * $stats['badgeCount'][ $this->id ] ) < \IPS\Settings::i()->rare_badge_percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of people who have this badge
|
||||
*
|
||||
* @param bool|null $roundUp Round number to nearest 100 to be less specific
|
||||
* @return int
|
||||
*/
|
||||
public function ownedByCount( bool $roundUp=FALSE )
|
||||
{
|
||||
if ( $stats = $this->getBadgeStats() and isset( $stats['badgeCount'][ $this->id ] ) )
|
||||
{
|
||||
if ( $roundUp )
|
||||
{
|
||||
return ceil( $stats['badgeCount'][$this->id] / 100 ) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $stats['badgeCount'][ $this->id ];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for this badge
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getBadgeStats(): ?array
|
||||
{
|
||||
/* Fetch the counts we need from the datastore. If they are old or non-existent, refresh and store in datastore for next time. */
|
||||
$stats = NULL;
|
||||
|
||||
if( isset( \IPS\Data\Store::i()->badgeRarityStats ) )
|
||||
if( isset( \IPS\Data\Store::i()->badgeStats ) )
|
||||
{
|
||||
$stats = json_decode( \IPS\Data\Store::i()->badgeRarityStats, true );
|
||||
$stats = json_decode( \IPS\Data\Store::i()->badgeStats, true );
|
||||
|
||||
if( !isset( $stats['expiration'] ) OR time() > $stats['expiration'] OR !isset( $stats['badgeCount'][ $this->id ] ) )
|
||||
{
|
||||
@@ -243,21 +282,38 @@ class _Badge extends \IPS\Node\Model
|
||||
|
||||
if( $stats === NULL )
|
||||
{
|
||||
$exclude = json_decode( \IPS\Settings::i()->rules_exclude_groups, TRUE );
|
||||
$where = [ [ 'completed=?', true ] ];
|
||||
$subQuery = NULL;
|
||||
|
||||
if ( \is_array( $exclude ) and \count( $exclude ) )
|
||||
{
|
||||
$subQuery = \IPS\Db::i()->select( 'member_id', 'core_members', [ \IPS\Db::i()->in( 'member_group_id', $exclude ) ] );
|
||||
$where[] = [ \IPS\Db::i()->in( 'member_group_id', $exclude, TRUE ) ];
|
||||
}
|
||||
|
||||
$stats = array(
|
||||
'expiration' => time() + 3600, // Cache for 60 minutes
|
||||
'memberCount' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members', [ 'completed=?', true ] )->first(),
|
||||
'expiration' => time() + ( 3600 * 6 ), // Cache for 6 hours
|
||||
'memberCount' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members', $where )->first(),
|
||||
'badgeCount' => array(),
|
||||
);
|
||||
|
||||
foreach( \IPS\Db::i()->select( 'id', 'core_badges' ) as $badgeId )
|
||||
{
|
||||
$stats['badgeCount'][ $badgeId ] = \IPS\Db::i()->select( 'COUNT(*)', 'core_member_badges', [ 'badge=?', $badgeId ] )->first();
|
||||
$where = [ [ 'badge=?', $badgeId ] ];
|
||||
|
||||
if ( \is_array( $exclude ) and \count( $exclude ) )
|
||||
{
|
||||
$where[] = [ \IPS\Db::i()->in( 'core_member_badges.member', $subQuery, TRUE ) ];
|
||||
}
|
||||
|
||||
$stats['badgeCount'][ $badgeId ] = \IPS\Db::i()->select( 'COUNT(*)', 'core_member_badges', $where )->first();
|
||||
}
|
||||
|
||||
\IPS\Data\Store::i()->badgeRarityStats = json_encode( $stats );
|
||||
\IPS\Data\Store::i()->badgeStats = json_encode( $stats );
|
||||
}
|
||||
|
||||
return ( 100 / $stats['memberCount'] * $stats['badgeCount'][ $this->id ] ) < \IPS\Settings::i()->rare_badge_percent;
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,4 +500,24 @@ class _Badge extends \IPS\Node\Model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse string name Name
|
||||
* @apiresponse array statistcs Badge Statistics
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
$return = array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->_title,
|
||||
'statistics' => $this->getBadgeStats(),
|
||||
);
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
if ( $this->icon )
|
||||
{
|
||||
return \IPS\File::get( 'core_Badges', $this->icon );
|
||||
return \IPS\File::get( 'core_Ranks', $this->icon );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -197,9 +197,12 @@ class _Rank extends \IPS\Node\Model
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
/* Allow SVGs without the obscure hash removing the file extension */
|
||||
\IPS\File::$safeFileExtensions[] = 'svg';
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'member_ranks_word_custom', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_member_rank_{$this->id}" : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'member_ranks_points', $this->points ?: 0, TRUE, array( 'min' => 0 ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'member_ranks_icon', $this->icon ? \IPS\File::get( 'core_Badges', $this->icon ) : NULL, TRUE, array( 'obscure' => FALSE, 'allowedFileTypes' => array_merge( \IPS\Image::$imageExtensions, ['svg'] ), 'checkImage' => TRUE, 'storageExtension' => 'core_Badges' ), function( $val ) {
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'member_ranks_icon', $this->icon ? \IPS\File::get( 'core_Ranks', $this->icon ) : NULL, TRUE, array( 'obscure' => TRUE, 'allowedFileTypes' => array_merge( \IPS\Image::supportedExtensions(), ['svg'] ), 'checkImage' => TRUE, 'storageExtension' => 'core_Ranks' ), function( $val ) {
|
||||
if( !$val )
|
||||
{
|
||||
throw new \DomainException('achievements_bad_image');
|
||||
@@ -285,8 +288,8 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
try
|
||||
{
|
||||
$image = \IPS\File::get( 'core_Badges', $oldImage );
|
||||
$newImage = \IPS\File::create( 'core_Badges', $image->originalFilename, $image->contents() );
|
||||
$image = \IPS\File::get( 'core_Ranks', $oldImage );
|
||||
$newImage = \IPS\File::create( 'core_Ranks', $image->originalFilename, $image->contents() );
|
||||
$this->icon = (string) $newImage;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
@@ -309,7 +312,7 @@ class _Rank extends \IPS\Node\Model
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\File::get( 'core_Badges', $this->icon )->delete();
|
||||
\IPS\File::get( 'core_Ranks', $this->icon )->delete();
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
}
|
||||
@@ -332,7 +335,7 @@ class _Rank extends \IPS\Node\Model
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->_title,
|
||||
'icon' => $this->icon ? $this->_icon->url : $this->_icon,
|
||||
'icon' => $this->icon ? (string) $this->_icon->url : $this->_icon,
|
||||
'points' => $this->points
|
||||
);
|
||||
}
|
||||
@@ -419,7 +422,7 @@ class _Rank extends \IPS\Node\Model
|
||||
|
||||
if ( ! empty( $insert['icon_name'] ) and ! empty( $insert['icon_data'] ) )
|
||||
{
|
||||
$insert['icon'] = (string) \IPS\File::create( 'core_Badges', $insert['icon_name'], $insert['icon_data'], NULL, TRUE, NULL, FALSE );
|
||||
$insert['icon'] = (string) \IPS\File::create( 'core_Ranks', $insert['icon_name'], $insert['icon_data'], NULL, TRUE, NULL, FALSE );
|
||||
|
||||
unset( $insert['icon_name'] );
|
||||
unset( $insert['icon_data'] );
|
||||
|
||||
@@ -102,7 +102,7 @@ class _Recognize extends \IPS\Patterns\ActiveRecord
|
||||
$member->awardPoints( $points, 0, [], ['subject'], $recognize->id );
|
||||
}
|
||||
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_recognize', $member, [ $recognize ], [ $recognize->id ] );
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_recognize', $member, [ $member, $recognize ], [ $recognize->id ] );
|
||||
$notification->recipients->attach( $member );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class _Rule extends \IPS\Node\Model
|
||||
$extraElements[] = $filterElement;
|
||||
}
|
||||
|
||||
$awardTo = $achievementAction->awardOptions();
|
||||
$awardTo = $achievementAction->awardOptions( $this->filters );
|
||||
$awardSubjectField = new \IPS\Helpers\Form\Custom( "{$exploded[1]}_{$exploded[5]}_award_subject", [ 'points' => $this->points_subject, 'badge' => $this->badge_subject ], FALSE, [
|
||||
'getHtml' => function( $field ) {
|
||||
return \IPS\Theme::i()->getTemplate('achievements')->awardField( $field->name, $field->value );
|
||||
@@ -228,7 +228,7 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
if ( isset( $values[$values['achievement_rule_action'] . '_award_' . $type . '_badge'] ) )
|
||||
{
|
||||
if ( $values[$values['achievement_rule_action'] . '_award_subject']['badge'] )
|
||||
if ( $values[$values['achievement_rule_action'] . '_award_' . $type]['badge'] )
|
||||
{
|
||||
\IPS\Lang::saveCustom( 'core', "core_award_" . $type . "_badge_{$this->id}", $values[$values['achievement_rule_action'] . '_award_' . $type . '_badge'] );
|
||||
}
|
||||
@@ -244,7 +244,7 @@ class _Rule extends \IPS\Node\Model
|
||||
$return = [
|
||||
'action' => $values['achievement_rule_action'],
|
||||
'filters' => $filters ? json_encode( $filters ) : NULL,
|
||||
'milestone' => isset( $filters['milestone'] ) ? $filters['milestone'] : NULL,
|
||||
'milestone' => ( isset( $filters['milestone'] ) and \is_numeric( $filters['milestone'] ) ) ? $filters['milestone'] : NULL,
|
||||
'points_subject'=> \intval( $values[ $values['achievement_rule_action'] . '_award_subject' ]['points'] ),
|
||||
'badge_subject' => $values[ $values['achievement_rule_action'] . '_award_subject' ]['badge'] ? \intval( $values[ $values['achievement_rule_action'] . '_award_subject' ]['badge'] ): NULL,
|
||||
'points_other' => \intval( isset( $values[ $values['achievement_rule_action'] . '_award_other' ] ) ? $values[ $values['achievement_rule_action'] . '_award_other' ]['points'] : 0 ),
|
||||
@@ -322,10 +322,33 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
parent::delete();
|
||||
|
||||
\IPS\Db::i()->delete( 'core_achievements_log_milestones', array( 'milestone_rule=?', $this->id ) );
|
||||
|
||||
\IPS\Lang::deleteCustom( 'core', "core_award_subject_badge_{$this->id}" );
|
||||
\IPS\Lang::deleteCustom( 'core', "core_award_other_badge_{$this->id}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Clone the rule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ( $this->skipCloneDuplication === TRUE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldId = $this->id;
|
||||
|
||||
parent::__clone();
|
||||
|
||||
foreach ( array( 'subject' => "core_award_subject_badge_{$this->id}", 'other' => "core_award_other_badge_{$this->id}") as $fieldKey => $langKey ) {
|
||||
$oldLangKey = str_replace( $this->id, $oldId, $langKey );
|
||||
\IPS\Lang::saveCustom( 'core', $langKey, iterator_to_array(\IPS\Db::i()->select( 'word_custom, lang_id', 'core_sys_lang_words', array('word_key=?', $oldLangKey ) )->setKeyField('lang_id')->setValueField('word_custom' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details about the rebuild progress so far
|
||||
*
|
||||
@@ -360,6 +383,7 @@ class _Rule extends \IPS\Node\Model
|
||||
public static function rebuildAllAchievements( ?\IPS\DateTime $time )
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_achievements_log' );
|
||||
\IPS\Db::i()->delete( 'core_achievements_log_milestones' );
|
||||
\IPS\Db::i()->delete( 'core_points_log' );
|
||||
\IPS\Db::i()->delete( 'core_member_badges', [ 'rule > 0' ] ); /* 0 is a manually awarded badge. Don't hate the coder, hate the player */
|
||||
\IPS\Db::i()->update( 'core_members', [ 'achievements_points' => 0 ] );
|
||||
@@ -410,7 +434,12 @@ class _Rule extends \IPS\Node\Model
|
||||
{
|
||||
foreach( $rules as $ruleId => $rule )
|
||||
{
|
||||
static::load( $ruleId )->delete();
|
||||
try
|
||||
{
|
||||
static::load( $ruleId )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ){}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,6 +479,12 @@ class _Rule extends \IPS\Node\Model
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Skip the forum related rule if we don't have forums installed */
|
||||
if ( $xml->name == 'action' AND !static::canImport( $xml->readString() ) )
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
switch ( $xml->name )
|
||||
{
|
||||
case 'action':
|
||||
@@ -566,4 +601,49 @@ class _Rule extends \IPS\Node\Model
|
||||
|
||||
unset( \IPS\Data\Store::i()->achievementRules );
|
||||
}
|
||||
|
||||
/**
|
||||
* Only import the node if the application is installed
|
||||
*
|
||||
* @param string $value Actionname
|
||||
* @return bool
|
||||
*/
|
||||
public static function canImport( string $value ) : bool
|
||||
{
|
||||
$appKey = explode( '_', $value );
|
||||
return \IPS\Application::appIsEnabled( $appKey[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this rule have a milestone set?
|
||||
*
|
||||
* @param array|null $filters Actionname
|
||||
* @return bool
|
||||
*/
|
||||
public static function ruleHasMilestone( ?array $filters ): bool
|
||||
{
|
||||
if ( \is_array( $filters ) and \count( $filters ) )
|
||||
{
|
||||
foreach( $filters as $key => $value )
|
||||
{
|
||||
if ( mb_substr( $key, 0, 9 ) == 'milestone' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get the title to store in the log
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function titleForLog()
|
||||
{
|
||||
$exploded = explode( '_', $this->action );
|
||||
return \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'AchievementAction__' . $exploded[1] . '_title' ) . " #" . $this->id;
|
||||
}
|
||||
}
|
||||
@@ -309,6 +309,8 @@ abstract class _AdminNotification extends \IPS\Patterns\ActiveRecord
|
||||
$notification->app = $app;
|
||||
$notification->ext = $extension;
|
||||
$notification->extra = $extra;
|
||||
|
||||
unset( \IPS\Data\Store::i()->acpNotifications );
|
||||
}
|
||||
|
||||
/* Is this a new notification? */
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Alert model
|
||||
* @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 12 May 2022
|
||||
* @todo js to hide modal when replying
|
||||
*/
|
||||
|
||||
namespace IPS\core\Alerts;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alerts Model
|
||||
*/
|
||||
class _Alert extends \IPS\Content\Item
|
||||
{
|
||||
/**
|
||||
* @brief Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_alerts';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Caches
|
||||
* @note Defined cache keys will be cleared automatically as needed
|
||||
*/
|
||||
protected $caches = array( 'alerts' );
|
||||
|
||||
/**
|
||||
* @brief Application
|
||||
*/
|
||||
public static $application = 'core';
|
||||
|
||||
/**
|
||||
* @brief Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'alert_';
|
||||
|
||||
/**
|
||||
* @brief Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Map
|
||||
*/
|
||||
protected static $multitonMap = array();
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databaseColumnId = 'id';
|
||||
|
||||
/**
|
||||
* @brief Database Column Map
|
||||
*/
|
||||
public static $databaseColumnMap = array(
|
||||
'title' => 'title',
|
||||
'date' => 'start',
|
||||
'author' => 'member_id',
|
||||
'views' => 'views',
|
||||
'content' => 'content'
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Title
|
||||
*/
|
||||
public static $title = 'alert';
|
||||
|
||||
/**
|
||||
* @brief Title
|
||||
*/
|
||||
public static $icon = 'bullhorn';
|
||||
|
||||
|
||||
/**
|
||||
* Get SEO name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_seo_title()
|
||||
{
|
||||
if( !$this->_data['seo_title'] )
|
||||
{
|
||||
$this->seo_title = \IPS\Http\Url\Friendly::seoTitle( $this->title );
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this->_data['seo_title'] ?: \IPS\Http\Url\Friendly::seoTitle( $this->title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display Form
|
||||
*
|
||||
* @param static|NULL $alert Existing alert (for edits)
|
||||
* @return \IPS\Helpers\Form
|
||||
*/
|
||||
public static function form( $alert )
|
||||
{
|
||||
/* Build the form */
|
||||
$form = new \IPS\Helpers\Form( NULL, 'save' );
|
||||
$form->class = 'ipsForm_vertical';
|
||||
|
||||
if ( \IPS\Member::loggedIn()->members_disable_pm == 2 )
|
||||
{
|
||||
$form->addMessage( 'alert_you_cannot_receive_messages', 'ipsMessage ipsMessage_info' );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'alert_title', ( $alert ) ? $alert->title : NULL, TRUE, array( 'maxLength' => 255 ) ) );
|
||||
|
||||
$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\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' );
|
||||
$toggles = array( 'user' => array( 'alert_recipient_user' ), 'group' => array( 'alert_recipient_group', 'alert_show_to' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'alert_recipient_type', ( $alert ) ? $alert->recipient_type : NULL, TRUE, array( 'options' => $options, 'toggles' => $toggles ) ) );
|
||||
|
||||
$formFields = array();
|
||||
|
||||
foreach( $formFields as $field )
|
||||
{
|
||||
$form->add( $field );
|
||||
}
|
||||
|
||||
$groups = array();
|
||||
foreach ( \IPS\Member\Group::groups( TRUE, FALSE ) as $group )
|
||||
{
|
||||
$groups[ $group->g_id ] = $group->name;
|
||||
}
|
||||
|
||||
$autocomplete = [
|
||||
'source' => 'app=core&module=system&controller=ajax&do=findMember',
|
||||
'resultItemTemplate' => 'core.autocomplete.memberItem',
|
||||
'commaTrigger' => false,
|
||||
'unique' => true,
|
||||
'minAjaxLength' => 3,
|
||||
'disallowedCharacters' => array(),
|
||||
'lang' => 'mem_optional',
|
||||
'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 )
|
||||
{
|
||||
if ( \IPS\Request::i()->alert_recipient_type === 'user' )
|
||||
{
|
||||
if( !\is_object( $member ) or !$member->member_id )
|
||||
{
|
||||
throw new \InvalidArgumentException( 'alert_no_recipient_selected' );
|
||||
}
|
||||
}
|
||||
},
|
||||
NULL, NULL, 'alert_recipient_user' ) );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'alert_recipient_group', ( $alert and $alert->recipient_group ) ? explode( ',', $alert->recipient_group ) : array(), FALSE, array( 'options' => $groups, 'multiple' => TRUE ), NULL, NULL, NULL, 'alert_recipient_group' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'alert_show_to', ( $alert and $alert->show_to ) ? $alert->show_to : 'all', FALSE, [
|
||||
'options' => [
|
||||
'all' => 'alert_show_to_all',
|
||||
'new' => 'alert_show_to_new'
|
||||
]
|
||||
], NULL, NULL, NULL, 'alert_show_to' ) );
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'alert_anonymous', ( $alert and $alert->anonymous ) ? TRUE : FALSE, TRUE, array( 'togglesOff' => array( 'alert_reply') ) ) );
|
||||
|
||||
if ( \IPS\Member::loggedIn()->members_disable_pm != 2 )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'alert_reply', $alert ? $alert->reply : 0, TRUE, array( 'options' => array( '0' => 'alert_no_reply', '1' => 'alert_can_reply', '2' => 'alert_must_reply' ) ), NULL, NULL, NULL, 'alert_reply' ) );
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data store
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getStore()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->alerts ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->alerts = iterator_to_array( \IPS\Db::i()->select( '*', static::$databaseTable, [
|
||||
[ 'alert_enabled=?', 1 ],
|
||||
[ 'alert_start < ?', time() ],
|
||||
[ '( alert_end = 0 or alert_end < ? )', time() ]
|
||||
], "alert_start ASC" )->setKeyField( 'alert_id' ) );
|
||||
}
|
||||
|
||||
return \IPS\Data\Store::i()->alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from form
|
||||
*
|
||||
* @param array $values Values from form
|
||||
* @param \IPS\core\Alerts\Alert|NULL $current Current alert
|
||||
* @return \IPS\core\Alerts\Alert
|
||||
*/
|
||||
public static function _createFromForm( $values, $current )
|
||||
{
|
||||
if( $current )
|
||||
{
|
||||
$obj = static::load( $current->id );
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = new static;
|
||||
$obj->member_id = \IPS\Member::loggedIn()->member_id;
|
||||
}
|
||||
|
||||
$obj->title = $values['alert_title'];
|
||||
$obj->seo_title = \IPS\Http\Url\Friendly::seoTitle( $values['alert_title'] );
|
||||
$obj->content = $values['alert_content'];
|
||||
$obj->start = $values['alert_start'] ? ( $values['alert_start']->getTimestamp() < time() ) ? time() : $values['alert_start']->getTimestamp() : time();
|
||||
$obj->end = $values['alert_end'] ? $values['alert_end']->getTimestamp() : 0;
|
||||
|
||||
$obj->recipient_type = $values['alert_recipient_type'];
|
||||
|
||||
if( $obj->recipient_type == 'user' )
|
||||
{
|
||||
if( \is_object( $values['alert_recipient_user'] ) )
|
||||
{
|
||||
$values['alert_recipient_user'] = $values['alert_recipient_user']->member_id;
|
||||
}
|
||||
$obj->recipient_user = $values['alert_recipient_user'];
|
||||
$obj->recipient_group = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj->recipient_user = NULL;
|
||||
$obj->recipient_group = implode( ",", $values['alert_recipient_group'] );
|
||||
$obj->show_to = $values['alert_show_to'];
|
||||
}
|
||||
|
||||
$obj->anonymous = $values['alert_anonymous'];
|
||||
$obj->reply = ( \IPS\Member::loggedIn()->members_disable_pm == 2 ) ? 0 : $values['alert_reply'];
|
||||
|
||||
$obj->save();
|
||||
|
||||
if( !$current )
|
||||
{
|
||||
\IPS\File::claimAttachments( 'createAlert', $obj->id, NULL, 'alert' );
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached URLs
|
||||
*/
|
||||
protected $_url = array();
|
||||
|
||||
/**
|
||||
* Get URL
|
||||
*
|
||||
* @param string|NULL $action Action
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
public function url( $action=NULL )
|
||||
{
|
||||
$_key = md5( $action );
|
||||
|
||||
if( !isset( $this->_url[ $_key ] ) )
|
||||
{
|
||||
$this->_url[ $_key ] = \IPS\Http\Url::internal( "app=core&module=modcp&controller=modcp&tab=alerts&id={$this->id}", 'front', 'modcp_alerts' );
|
||||
$this->_url[ $_key ] = $this->_url[ $_key ]->setQueryString( 'action', $action );
|
||||
}
|
||||
|
||||
return $this->_url[ $_key ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get owner
|
||||
*
|
||||
* @return \IPS\Member
|
||||
*/
|
||||
public function owner()
|
||||
{
|
||||
return \IPS\Member::load( $this->member_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unclaim attachments
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function unclaimAttachments()
|
||||
{
|
||||
\IPS\File::unclaimAttachments( 'core_Alert', $this->id, NULL, 'alert' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filters that are available for selecting table rows
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getTableFilters()
|
||||
{
|
||||
return array(
|
||||
'active', 'inactive'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \IPS\Member $member
|
||||
*
|
||||
* @return \IPS\core\Alerts\Alert|NULL
|
||||
*/
|
||||
public static function getNextAlertForMember( \IPS\Member $member ): ?Alert
|
||||
{
|
||||
$alerts = static::getStore();
|
||||
|
||||
if ( ! \count( $alerts ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Get possible alerts for this member (group alerts are checked in PHP) */
|
||||
$query = \IPS\Db::i()->select( '*', 'core_alerts', [
|
||||
[ 'alert_enabled=?', 1 ],
|
||||
[ 'alert_start < ?', time() ],
|
||||
[ '( alert_end = 0 or alert_end > ? )', time() ],
|
||||
[ '( alert_recipient_type=? OR ( alert_recipient_type=? AND alert_recipient_user=? ) )', 'group', 'user', $member->member_id ],
|
||||
[ 'alert_id NOT IN (?)', \IPS\Db::i()->select( 'seen_alert_id', 'core_alerts_seen', [ 'seen_member_id=?', $member->member_id ] ) ]
|
||||
], 'alert_start ASC' );
|
||||
|
||||
foreach( $query as $data )
|
||||
{
|
||||
$alert = static::constructFromData( $data );
|
||||
if ( $alert->forMember( $member ) )
|
||||
{
|
||||
return $alert;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this alert valid for the member?
|
||||
*
|
||||
* @return Bool
|
||||
*/
|
||||
public function forMember( $member )
|
||||
{
|
||||
/* Is it disabled? */
|
||||
if ( ! $this->enabled )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Are we the alert author? */
|
||||
if ( $this->member_id == \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Is it in the future? */
|
||||
if( $this->start > time() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Is it in the past? */
|
||||
if( $this->end and $this->end < time() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Show only if to this user or group */
|
||||
if( $this->recipient_type == 'user' and $this->recipient_user !== $member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if( $this->recipient_type == 'group' )
|
||||
{
|
||||
if( !$member->inGroup( explode( ",", $this->recipient_group ) ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $this->show_to == 'new' and ( \IPS\Member::loggedIn()->joined->getTimestamp() < $this->start ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the names of the groups for this alert.
|
||||
* Convenience method for use in templates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function groupNames(): array
|
||||
{
|
||||
$names = [];
|
||||
|
||||
if ( $this->recipient_type == 'group' and $this->recipient_group )
|
||||
{
|
||||
$groups = explode( ',', $this->recipient_group );
|
||||
|
||||
foreach ( \IPS\Member\Group::groups() as $group )
|
||||
{
|
||||
if ( \in_array( $group->g_id, $groups ) )
|
||||
{
|
||||
$names[] = $group->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the namee of the member for this alert
|
||||
* Convenience method for use in templates.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function memberName(): string
|
||||
{
|
||||
$member = \IPS\Member::load( $this->recipient_user );
|
||||
return ( $member->member_id ) ? $member->name :\IPS\Member::loggedIn()->language()->addToStack( 'deleted_member' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this alert as viewed
|
||||
*
|
||||
* @param \IPS\Member|null $member Member who viewed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function viewed( \IPS\Member $member=NULL )
|
||||
{
|
||||
$this->viewed++;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss alert
|
||||
*
|
||||
* @param \IPS\Member|null $member Member who viewed
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function dismiss( \IPS\Member $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if( !$this->forMember( $member ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->insert( 'core_alerts_seen', [
|
||||
'seen_alert_id' => $this->id,
|
||||
'seen_member_id' => $member->member_id,
|
||||
'seen_date' => time()
|
||||
], TRUE );
|
||||
|
||||
$member->latest_alert = $this->start;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
parent::delete();
|
||||
|
||||
\IPS\Db::i()->delete( 'core_alerts_seen', [ 'seen_alert_id=?', $this->id ] );
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -195,7 +195,7 @@ class _Announcement extends \IPS\Content\Item
|
||||
}
|
||||
}
|
||||
/* Or are we inside an allowed controller */
|
||||
else if ( \in_array( \get_class( \IPS\Dispatcher::i()->dispatcherController ), $extension::$controllers ) )
|
||||
else if (isset( \IPS\Dispatcher::i()->dispatcherController ) AND \in_array( \get_class( \IPS\Dispatcher::i()->dispatcherController ), $extension::$controllers ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief DataLayer Handler class
|
||||
* @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 Feb 2022
|
||||
*/
|
||||
|
||||
namespace IPS\core\DataLayer;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Layer Handler class
|
||||
*/
|
||||
class _Handler
|
||||
{
|
||||
public static $cicHandlerClass = '\IPS\cloud\DataLayer\Handler';
|
||||
|
||||
public static $handlerCacheKey = 'dataLayerHandlers';
|
||||
|
||||
public $id = 'gtm';
|
||||
|
||||
/**
|
||||
* Returns the enabled handlers to be used in the global template as an array of head and body HTML snippets.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function loadForTemplates()
|
||||
{
|
||||
$return = array();
|
||||
|
||||
/* try to get from cache */
|
||||
$key = static::$handlerCacheKey;
|
||||
try
|
||||
{
|
||||
$cached = json_decode( \IPS\Data\Store::i()->$key, true );
|
||||
if ( \is_array( $cached ) )
|
||||
{
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) {}
|
||||
|
||||
$handlers = static::loadEnabled();
|
||||
if ( \IPS\Settings::i()->core_datalayer_use_gtm )
|
||||
{
|
||||
$handlers[] = static::gtm();
|
||||
}
|
||||
|
||||
$headInserts = "<!-- Handlers -->\n\n<!-- Initializers -->\n";
|
||||
$bodyInserts = "<!-- Handlers -->\n";
|
||||
$headCodes = "";
|
||||
$eventCallbacks = array();
|
||||
$propertiesCallbacks = array();
|
||||
|
||||
foreach ( $handlers as $handler )
|
||||
{
|
||||
|
||||
if ( $handler->head_code )
|
||||
{
|
||||
$headCodes .= "\n{$handler->head_code}\n";
|
||||
}
|
||||
|
||||
if ( $handler->body_code )
|
||||
{
|
||||
$bodyInserts .= "\n{$handler->body_code}\n";
|
||||
}
|
||||
|
||||
if ( $handler->initializer_code AND $handler->use_js )
|
||||
{
|
||||
$formatted = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->initializer_code );
|
||||
$code = <<<HTML
|
||||
<script>
|
||||
let initcode{$handler->id} = context => {
|
||||
if ( !(context instanceof Object) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set the key and time */
|
||||
let ips_time = IpsDataLayerConfig._properties.ips_time.enabled ? IpsDataLayerConfig._properties.ips_time.formatted_name : false;
|
||||
let ips_key = IpsDataLayerConfig._properties.ips_key.enabled ? IpsDataLayerConfig._properties.ips_key.formatted_name : false;
|
||||
|
||||
if ( ips_time ) {
|
||||
context[ips_time] = Math.floor( Date.now() / 1000 );
|
||||
}
|
||||
|
||||
if ( ips_key ) {
|
||||
let s = i => {
|
||||
return Math.floor((1 + Math.random()) * Math.pow(16, i))
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
};
|
||||
|
||||
let mt = Date.now();
|
||||
let sec = Math.floor(mt / 1000);
|
||||
let secString = sec.toString(16);
|
||||
secString = secString.substring( secString.length - 8 );
|
||||
let ms = ( mt - ( sec * 1000 ) ) * 1000; // milliseconds
|
||||
let msString = (ms + 0x100000).toString(16).substring(1);
|
||||
let randomId = secString + msString + s(1) + '.' + s(4) + s(4);
|
||||
context[ips_key] = randomId;
|
||||
}
|
||||
|
||||
for ( let i in context ) {
|
||||
if ( context[i] === null ) {
|
||||
context[i] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$formatted
|
||||
} catch (e) {
|
||||
Debug.error('Bad Data Layer Initializer: Event initializer failed!');
|
||||
}
|
||||
};
|
||||
initcode{$handler->id}(IpsDataLayerContext || {});
|
||||
</script>
|
||||
HTML;
|
||||
$code = \preg_replace( '/\/\/(.*)[\n|\r]/', '/*$1*/', $code );
|
||||
$code = \preg_replace( '/\s+/', ' ', $code);
|
||||
$headInserts .= \str_replace("\n", ' ', $code ) . "\n";
|
||||
}
|
||||
|
||||
if ( $handler->event_handler AND $handler->use_js )
|
||||
{
|
||||
$formatted = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->event_handler );
|
||||
$code = <<<JS
|
||||
( () => _event => {
|
||||
try {
|
||||
$formatted
|
||||
} catch (e) {
|
||||
Debug.error( e );
|
||||
}
|
||||
} )
|
||||
JS;
|
||||
$code = \preg_replace( '/\/\/(.*)[\n|\r]?/', '/*$1*/', $code );
|
||||
$code = \preg_replace( '/\s+/', ' ', $code);
|
||||
$eventCallbacks[] = \str_replace( "\n", ' ', $code );
|
||||
}
|
||||
|
||||
if ( $handler->properties_handler AND $handler->use_js )
|
||||
{
|
||||
$formatted = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->properties_handler );
|
||||
$code = <<<JS
|
||||
( () => _properties => {
|
||||
try {
|
||||
$formatted
|
||||
} catch (e) {
|
||||
Debug.error( e );
|
||||
}
|
||||
} )
|
||||
JS;
|
||||
$code = \preg_replace( '/\/\/(.*)[\n|\r]?/', '/*$1*/', $code );
|
||||
$code = \preg_replace( '/\s+/', ' ', $code);
|
||||
$propertiesCallbacks[] = \str_replace( "\n", ' ', $code );
|
||||
}
|
||||
}
|
||||
|
||||
$headInserts .= "<!-- END Initializers -->\n";
|
||||
$headInserts .= "\n<!-- Head Snippets -->\n$headCodes\n<!-- END Head Snippets -->\n";
|
||||
$eventsJoined = implode( ",\n\t", $eventCallbacks );
|
||||
$headInserts .= <<<HTML
|
||||
|
||||
<!-- Event Callbacks -->
|
||||
<script>
|
||||
const IpsDataLayerEventHandlers = [
|
||||
$eventsJoined
|
||||
];
|
||||
</script>
|
||||
<!-- END Event Callbacks -->
|
||||
|
||||
HTML;
|
||||
|
||||
$propertiesJoined = implode( ",\n\t", $propertiesCallbacks );
|
||||
$headInserts .= <<<HTML
|
||||
|
||||
<!-- Properties Callbacks -->
|
||||
<script>
|
||||
const IpsDataLayerPropertiesHandlers = [
|
||||
$propertiesJoined
|
||||
];
|
||||
</script>
|
||||
<!-- END Properties Callbacks -->
|
||||
|
||||
HTML;
|
||||
|
||||
$headInserts .= "\n<!-- END Handlers -->";
|
||||
$bodyInserts .= "\n<!-- END Handlers -->";
|
||||
|
||||
$return = array(
|
||||
'headInserts' => $headInserts,
|
||||
'bodyInserts' => $bodyInserts,
|
||||
);
|
||||
|
||||
/* add to cache before returning */
|
||||
\IPS\Data\Store::i()->$key = json_encode( $return );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled handlers
|
||||
*
|
||||
* @return static[]
|
||||
*/
|
||||
public static function loadEnabled()
|
||||
{
|
||||
if ( class_exists( static::$cicHandlerClass ) )
|
||||
{
|
||||
$class = static::$cicHandlerClass;
|
||||
return $class::loadEnabled();
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a built in gtm handler
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function gtm()
|
||||
{
|
||||
$gtm = new static();
|
||||
$gtm->initializer_code = <<<JS
|
||||
|
||||
if (context instanceof Object) {
|
||||
{dataLayer} = {dataLayer} || [];
|
||||
{dataLayer}.push(context);
|
||||
return;
|
||||
}
|
||||
Debug.log( 'Invalid Data Layer Context: The IPS GTM Data Layer Initializer failed because the context wasn\'t an Object' );
|
||||
|
||||
JS;
|
||||
$gtm->head_code = \IPS\Settings::i()->googletag_head_code;
|
||||
$gtm->use_js = true;
|
||||
$gtm->body_code = \IPS\Settings::i()->googletag_noscript_code;
|
||||
$gtm->datalayer_key = \IPS\Settings::i()->core_datalayer_gtmkey ?: 'window.dataLayer';
|
||||
$gtm->event_handler = <<<JS
|
||||
|
||||
if ( (_event._properties instanceof Object) && (typeof _event._key === 'string')) {
|
||||
{dataLayer} = {dataLayer} || [];
|
||||
let properties = {};
|
||||
for ( let pKey in _event._properties ) {
|
||||
properties[_event._key + '.' + pKey] = _event._properties[pKey];
|
||||
}
|
||||
{dataLayer}.push( { ...properties, 'event': _event._key } );
|
||||
return;
|
||||
}
|
||||
Debug.log( 'Invalid Data Layer Event: An event wasn\'t processed by the IPS GTM Data Layer Handler. The event\'s _key has to be a string, and its _properties has to be an Object.' );
|
||||
|
||||
JS;
|
||||
$gtm->properties_handler = <<<JS
|
||||
|
||||
if ( _properties instanceof Object ) {
|
||||
delete _properties.event; // this cannot be set since this handler is NOT for adding GTM events
|
||||
{dataLayer} = {dataLayer} || [];
|
||||
{dataLayer}.push( _properties );
|
||||
}
|
||||
|
||||
JS;
|
||||
return $gtm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all handler objects according to the where clause
|
||||
*
|
||||
* @param array $where The where clause
|
||||
*
|
||||
* @return array
|
||||
* @throws \UnexpectedValueException If $where contains invalid fields and/or invalid SQL logic
|
||||
*/
|
||||
public static function loadWhere( array $where=array() )
|
||||
{
|
||||
if ( ( $class = static::$cicHandlerClass ) AND class_exists( $class ) )
|
||||
{
|
||||
return $class::loadWhere( $where );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock instance method since our autoloader will complain if there are only static methods in a class
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function mockFunction()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -230,7 +230,7 @@ class _Digest
|
||||
$htmlContent = str_replace( "___digest___", $this->output['html'], $this->mail->compileContent( 'html', $this->member ) );
|
||||
$plaintextContent = str_replace( "___digest___", $this->output['plain'], $this->mail->compileContent( 'plaintext', $this->member ) );
|
||||
|
||||
\IPS\Email::buildFromContent( $subject, $htmlContent, $plaintextContent, \IPS\Email::TYPE_LIST, FALSE, $this->frequency . '_digest' )->send( $this->member );
|
||||
\IPS\Email::buildFromContent( $subject, $htmlContent, $plaintextContent, \IPS\Email::TYPE_LIST, \IPS\Email::WRAPPER_NONE, $this->frequency . '_digest' )->send( $this->member );
|
||||
}
|
||||
|
||||
/* After sending digest update core_follows to set notify_sent (don't forget where clause for frequency) */
|
||||
@@ -262,7 +262,7 @@ class _Digest
|
||||
}
|
||||
|
||||
/* Fetch the member's follows so we can build their digest */
|
||||
$follows = \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_notify_do=1 AND follow_notify_freq=? AND follow_notify_sent < ? AND ' . \IPS\Db::i()->in( 'follow_member_id', $memberIDs ), $frequency, ( $frequency == 'daily' ) ? time() - 86400 : time() - 604800 ) );
|
||||
$follows = \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_notify_do=1 AND follow_notify_freq=? AND follow_notify_sent < ? AND ' . \IPS\Db::i()->in( 'follow_member_id', $memberIDs ), $frequency, ( $frequency == 'daily' ) ? time() - 86400 : time() - 604800 ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER );
|
||||
|
||||
$groupedFollows = array();
|
||||
foreach( $follows as $follow )
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Checkbox Set for Webhooks
|
||||
* @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 14 Dec 2021
|
||||
*/
|
||||
|
||||
namespace IPS\core\Form;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox Set for Webhooks
|
||||
*/
|
||||
class _WebhookSelector extends \IPS\Helpers\Form\CheckboxSet
|
||||
{
|
||||
|
||||
/**
|
||||
* Get HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html()
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'system/webhooks.css' ) );
|
||||
|
||||
/* Get descriptions */
|
||||
$descriptions = $this->options['descriptions'];
|
||||
|
||||
foreach ( $this->options['options'] as $k => $v )
|
||||
{
|
||||
if ( \IPS\Member::loggedIn()->language()->checkKeyExists( $v ) )
|
||||
{
|
||||
$descriptions[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( $v );
|
||||
}
|
||||
}
|
||||
|
||||
$value = $this->value;
|
||||
|
||||
/* If the value is NULL or an empty string, i.e. from a custom field, then we should not convert it into an array because the
|
||||
value will evaluate to 0 with an == check and the first option in the checkbox set will always be selected erroneously */
|
||||
if ( $this->options['unlimited'] === NULL and !\is_array( $value ) AND $value !== NULL AND $value !== '' )
|
||||
{
|
||||
$value = array( $value );
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'admin' )->webhookselector( $this->name, $value, $this->required, $this->parseOptions(), $this->options['multiple'], $this->options['class'], $this->options['disabled'], $this->options['toggles'], NULL, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $descriptions, $this->options['impliedUnlimited'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief IndexNow Submission Class
|
||||
* @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 11 Jan 2022
|
||||
*/
|
||||
|
||||
namespace IPS\core;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexNow class
|
||||
*
|
||||
* Submits the content to indexnow - https://www.indexnow.org/documentation
|
||||
*
|
||||
* The submission should always happen via the queue system!
|
||||
*/
|
||||
class _IndexNow extends \IPS\Patterns\Singleton
|
||||
{
|
||||
/**
|
||||
* @brief Singleton Instances
|
||||
*/
|
||||
protected static $instance = NULL;
|
||||
|
||||
/**
|
||||
* @brief Target URL for the API Request
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
static $apiUrl = "https://api.indexnow.org/indexnow/";
|
||||
|
||||
/**
|
||||
* Is the extension enabled?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) \IPS\Settings::i()->indexnow_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a URL to be sent to indexnow
|
||||
*
|
||||
* @param $url
|
||||
* @return void
|
||||
*/
|
||||
public static function addUrlToQueue( $url )
|
||||
{
|
||||
static::addUrlsToQueue([ $url ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an array with URLs to be sent to indexnow
|
||||
*
|
||||
* @param array $urls
|
||||
* @return void
|
||||
*/
|
||||
public static function addUrlsToQueue( array $urls )
|
||||
{
|
||||
$obj = static::i();
|
||||
if( $obj->isEnabled() )
|
||||
{
|
||||
\IPS\Task::queue( 'core', 'IndexNow', [ 'urls' => array_map('strval', $urls) ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends all the urls in the queue to indexnow
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function send( array $urls )
|
||||
{
|
||||
/* Move on if there's nothing to send */
|
||||
if( !\count( $urls ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$url = \IPS\Http\Url::internal('');
|
||||
$data = array(
|
||||
'host' => \IPS\Http\Url::baseUrl(\IPS\Http\Url::PROTOCOL_WITHOUT ),
|
||||
'key' => \IPS\Settings::i()->indexnow_key,
|
||||
'keyLocation' => (string) $url->setPath( $url->data[ \IPS\Http\Url::COMPONENT_PATH ] . $this->getKeyFileName() ),
|
||||
'urlList' => $urls,
|
||||
);
|
||||
|
||||
$response = \IPS\Http\Url::external( static::$apiUrl )->request()
|
||||
->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( $data ) );
|
||||
|
||||
if( !$response->isSuccessful() )
|
||||
{
|
||||
switch ( $response->httpResponseCode )
|
||||
{
|
||||
case 400:
|
||||
$error = 'invalid_request';
|
||||
break;
|
||||
case 403:
|
||||
$error = 'invalid_api_key';
|
||||
breaK;
|
||||
case 422:
|
||||
$error = 'invalid_url';
|
||||
break;
|
||||
case 429:
|
||||
default:
|
||||
$error = 'unknown_error';
|
||||
}
|
||||
\IPS\Log::log( $response->httpResponseCode . ' ' . $error . ' ' . print_r($data, TRUE ), 'IndexNow');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the indexnow verification file name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyFileName(): string
|
||||
{
|
||||
return \IPS\Settings::i()->indexnow_key . '.txt';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the indexnow verification file content, which is literally only the key!
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyfileContent(): string
|
||||
{
|
||||
return \IPS\Settings::i()->indexnow_key;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ class _Conversation extends \IPS\Content\Item
|
||||
*/
|
||||
public static $includeInSiteSearch = FALSE;
|
||||
|
||||
/**
|
||||
* @brief [Content\Item] Include these items in trending content
|
||||
*/
|
||||
public static $includeInTrending = FALSE;
|
||||
|
||||
/**
|
||||
* @brief [Content\Comment] Icon
|
||||
*/
|
||||
@@ -229,13 +234,13 @@ class _Conversation extends \IPS\Content\Item
|
||||
}
|
||||
}
|
||||
|
||||
$return['to'] = new \IPS\Helpers\Form\Member( 'messenger_to', $member, TRUE, array( 'multiple' => ( \IPS\Member::loggedIn()->group['g_max_mass_pm'] == -1 ) ? NULL : \IPS\Member::loggedIn()->group['g_max_mass_pm'] ), function ( $members )
|
||||
$return['to'] = new \IPS\Helpers\Form\Member( 'messenger_to', $member, TRUE, array( 'disabled' => ( \IPS\Request::i()->alert ) ? TRUE : FALSE, 'multiple' => ( \IPS\Member::loggedIn()->group['g_max_mass_pm'] == -1 ) ? NULL : \IPS\Member::loggedIn()->group['g_max_mass_pm'] ), function ( $members )
|
||||
{
|
||||
if ( \is_array( $members ) )
|
||||
{
|
||||
foreach ( $members as $m )
|
||||
{
|
||||
if ( !$m instanceof \IPS\Member OR !static::memberCanReceiveNewMessage( $m, \IPS\Member::loggedIn() ) )
|
||||
if ( !$m instanceof \IPS\Member OR !static::memberCanReceiveNewMessage( $m, \IPS\Member::loggedIn(), 'new' ) )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('meesnger_err_bad_recipient', FALSE, array( 'sprintf' => array( ( $m instanceof \IPS\Member ) ? $m->name : $m ) ) ) );
|
||||
}
|
||||
@@ -243,7 +248,7 @@ class _Conversation extends \IPS\Content\Item
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$members instanceof \IPS\Member OR !$members->member_id OR !static::memberCanReceiveNewMessage( $members, \IPS\Member::loggedIn() ) )
|
||||
if ( !$members instanceof \IPS\Member OR !$members->member_id OR !static::memberCanReceiveNewMessage( $members, \IPS\Member::loggedIn(), 'new' ) )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('meesnger_err_bad_recipient', FALSE, array( 'sprintf' => array( ( $members instanceof \IPS\Member ) ? $members->name : $members ) ) ) );
|
||||
}
|
||||
@@ -267,6 +272,11 @@ class _Conversation extends \IPS\Content\Item
|
||||
$return[ $k ] = $v;
|
||||
}
|
||||
|
||||
if( \IPS\Request::i()->alert )
|
||||
{
|
||||
unset( $return['title'] );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -276,15 +286,22 @@ class _Conversation extends \IPS\Content\Item
|
||||
*
|
||||
* @param \IPS\Member $member The member to check
|
||||
* @param \IPS\Member $sender The member sending the new message
|
||||
* @param string $type Type of message to check (new, reply)
|
||||
|
||||
* @return bool
|
||||
*/
|
||||
public static function memberCanReceiveNewMessage( \IPS\Member $member, \IPS\Member $sender )
|
||||
public static function memberCanReceiveNewMessage( \IPS\Member $member, \IPS\Member $sender, $type='new' )
|
||||
{
|
||||
/* Messenger is disabled */
|
||||
if ( $member->members_disable_pm )
|
||||
/* Messenger is hard disabled */
|
||||
if ( $member->members_disable_pm == 2 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
else if ( $member->members_disable_pm == 1 )
|
||||
{
|
||||
/* We will allow moderators */
|
||||
return $sender->modPermissions() !== FALSE;
|
||||
}
|
||||
|
||||
/* Group can not use messenger */
|
||||
if ( !$member->canAccessModule( \IPS\Application\Module::get( 'core', 'messaging' ) ) )
|
||||
@@ -357,6 +374,24 @@ class _Conversation extends \IPS\Content\Item
|
||||
|
||||
/* Send the notification for the first message */
|
||||
$comment->sendNotifications();
|
||||
|
||||
/* If this came from an alert dismiss the alert */
|
||||
if( \IPS\Request::i()->alert )
|
||||
{
|
||||
try
|
||||
{
|
||||
$alert = \IPS\core\Alerts\Alert::load( \IPS\Request::i()->alert );
|
||||
|
||||
if( $alert->forMember( \IPS\Member::loggedIn() ) )
|
||||
{
|
||||
$alert->dismiss();
|
||||
|
||||
$this->alert = $alert->id;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ){}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -853,4 +888,29 @@ class _Conversation extends \IPS\Content\Item
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build form to create
|
||||
*
|
||||
* @param \IPS\Node\Model|NULL $container Container (e.g. forum), if appropriate
|
||||
* @param \IPS\Content\Item|NULL $item Content item, e.g. if editing
|
||||
* @return \IPS\Helpers\Form
|
||||
*/
|
||||
protected static function buildCreateForm( \IPS\Node\Model $container=NULL, \IPS\Content\Item $item=NULL )
|
||||
{
|
||||
$form = parent::buildCreateForm( $container, $item );
|
||||
|
||||
try
|
||||
{
|
||||
$alert = \IPS\core\Alerts\Alert::load( \IPS\Request::i()->alert );
|
||||
|
||||
if( $alert->forMember( \IPS\Member::loggedIn() ) )
|
||||
{
|
||||
$form->hiddenValues['alert'] = $alert->id;
|
||||
$form->hiddenValues['messenger_title'] = $alert->title;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) {}
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
@@ -138,16 +138,17 @@ class _Message extends \IPS\Content\Comment
|
||||
* @param \IPS\DateTime|NULL $time The time
|
||||
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
|
||||
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
|
||||
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
|
||||
* @return static
|
||||
*/
|
||||
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
|
||||
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
|
||||
{
|
||||
if ( $member === NULL )
|
||||
{
|
||||
$member = \IPS\Member::loggedIn();
|
||||
}
|
||||
|
||||
$comment = parent::create( $item, $comment, $first, $guestName, $incrementPostCount, $member, $time, $ipAddress, $hiddenStatus );
|
||||
$comment = parent::create( $item, $comment, $first, $guestName, $incrementPostCount, $member, $time, $ipAddress, $hiddenStatus, $anonymous );
|
||||
|
||||
/* Mark unread for this person */
|
||||
\IPS\Db::i()->update( 'core_message_topic_user_map', array( 'map_has_unread' => FALSE, 'map_read_time' => time() ), array( 'map_user_id=? AND map_topic_id=?', $member->member_id, $item->id ) );
|
||||
@@ -177,7 +178,7 @@ class _Message extends \IPS\Content\Comment
|
||||
{
|
||||
$member = \IPS\Member::load( $map['map_user_id'] );
|
||||
/* skip this for members which can't or don't want to use the messenger or for deleted users */
|
||||
if ( $member->members_disable_pm or !$member->canAccessModule( $messengerModule ) or !$member->member_id )
|
||||
if ( $member->members_disable_pm == 2 or !$member->canAccessModule( $messengerModule ) or !$member->member_id )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
namespace IPS\core;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Text\DOMParser;
|
||||
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
@@ -55,7 +58,7 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
/**
|
||||
* @brief Action Type
|
||||
*/
|
||||
public static $actionTypes = array( 'swap', 'moderate' );
|
||||
public static $actionTypes = array( 'swap', 'moderate', 'block' );
|
||||
|
||||
/**
|
||||
* Table
|
||||
@@ -78,6 +81,7 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
/* Filters */
|
||||
$table->filters = array(
|
||||
'profanity_require_approval' => "action='moderate'",
|
||||
'profanity_block' => "action='block'",
|
||||
'profanity_replace_text' => "action='swap'",
|
||||
);
|
||||
|
||||
@@ -92,11 +96,15 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'profanity_replace_with_x', FALSE, array( 'sprintf' => array( $row['swop'] ) ) );
|
||||
}
|
||||
else if ( $val == 'block' )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'profanity_block' );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $row['min_posts'] )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'profanity_filter_action_moderate_min_posts', NULL, array( 'pluralize' => array( $row['min_posts'] ) ) );
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'profanity_filter_action_moderate_min_posts', FALSE, array( 'pluralize' => array( $row['min_posts'] ) ) );
|
||||
}
|
||||
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('profanity_filter_action_moderate');
|
||||
@@ -164,10 +172,29 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
$form->addTab('add');
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'profanity_type', ( $current ) ? $current->type : NULL, NULL, array() ) );
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'profanity_type', ( $current ) ? $current->type : NULL, NULL, array(), function( $val )
|
||||
{
|
||||
if ( $val )
|
||||
{
|
||||
try
|
||||
{
|
||||
$word = static::load( $val, 'type' );
|
||||
|
||||
if ( ! isset( \IPS\Request::i()->id ) or ( isset( \IPS\Request::i()->id ) and \IPS\Request::i()->id != $word->wid ) )
|
||||
{
|
||||
throw new \LogicException( 'profanity_already_exists' );
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* Nothing exists, so we are good */
|
||||
}
|
||||
}
|
||||
} ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'profanity_action', ( $current ) ? $current->action : 'swap', FALSE, array(
|
||||
'options' => array(
|
||||
'swap' => 'profanity_filter_action_swap',
|
||||
'block' => 'profanity_filter_action_block',
|
||||
'moderate' => 'profanity_filter_action_moderate'
|
||||
),
|
||||
'toggles' => array(
|
||||
@@ -296,6 +323,11 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
{
|
||||
$return = static::_checkUrlFilters( $content );
|
||||
}
|
||||
|
||||
if ( $return == FALSE )
|
||||
{
|
||||
$return = static::_checkEmailFilters( $content );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
@@ -487,12 +519,72 @@ class _Profanity extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
catch( \Exception $e ) {}
|
||||
catch( \ValueError $e ) {}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Email Filters
|
||||
*
|
||||
* @param string $content The content to check
|
||||
* @return bool
|
||||
*/
|
||||
protected static function _checkEmailFilters( $content )
|
||||
{
|
||||
/* If we are allowing ANY URL's and not doing anything, then do that */
|
||||
if ( \IPS\Settings::i()->email_filter_action == 'moderate')
|
||||
{
|
||||
/* Ensure that image and file names don't trip up the email filter */
|
||||
$source = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
|
||||
$source->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $content ) );
|
||||
|
||||
$contentImages = $source->getElementsByTagName( 'img' );
|
||||
|
||||
foreach( $contentImages as $element )
|
||||
{
|
||||
foreach( [ 'data-src', 'src', 'srcset', 'alt', 'title' ] as $attr )
|
||||
{
|
||||
if ( $element->hasAttribute( $attr ) )
|
||||
{
|
||||
/* Remove any filenames, alts or titles so they don't trigger the moderation filter */
|
||||
$element->setAttribute( $attr, '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get document URLs */
|
||||
$contentUrls = $source->getElementsByTagName( 'a' );
|
||||
|
||||
foreach( $contentUrls as $element )
|
||||
{
|
||||
foreach( [ 'href', 'title' ] as $attr )
|
||||
{
|
||||
if ( $element->hasAttribute( $attr ) )
|
||||
{
|
||||
if ( $attr === 'href' and ! mb_stristr( $element->getAttribute( $attr ), 'mailto:' ) )
|
||||
{
|
||||
/* Remove any hrefs or titles so they don't trigger the moderation filter */
|
||||
$element->setAttribute( $attr, '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Get DOMDocument output */
|
||||
$content = \IPS\Text\DOMParser::getDocumentBodyContents( $source );
|
||||
|
||||
if( preg_match( '/' . \IPS\Text\Parser::EMAIL_REGEX . '/u', $content, $matches ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Caches
|
||||
* @note Defined cache keys will be cleared automatically as needed
|
||||
|
||||
@@ -329,9 +329,10 @@ class _Field extends \IPS\CustomField
|
||||
*
|
||||
* @param array $values Current values
|
||||
* @param int $location \IPS\core\ProfileFields\Field::PROFILE for profile, \IPS\core\ProfileFields\Field::REG for registration screen or \IPS\core\ProfileFields\Field::STAFF for ModCP/ACP
|
||||
* @param bool $raw Returns the raw value if true or the display value if false. Useful for comparisons for field types like Yes/NO to see if a value is set.
|
||||
* @return array
|
||||
*/
|
||||
public static function values( $values, $location=0 )
|
||||
public static function values( $values, $location=0, $raw=FALSE )
|
||||
{
|
||||
$return = array();
|
||||
foreach ( static::fieldData() as $groupId => $fields )
|
||||
@@ -362,7 +363,7 @@ class _Field extends \IPS\CustomField
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[ $groupId ][ static::$langKey . '_' . $row['pf_id'] ] = static::constructFromData( $row )->displayValue( $values[ 'field_' . $row['pf_id'] ], FALSE, $location, \IPS\Member::load( $values['member_id'] ) );
|
||||
$return[ $groupId ][ static::$langKey . '_' . $row['pf_id'] ] = !( $raw ) ? static::constructFromData( $row )->displayValue( $values[ 'field_' . $row['pf_id'] ], FALSE, $location, \IPS\Member::load( $values['member_id'] ) ) : $values[ 'field_' . $row['pf_id'] ];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -395,14 +395,14 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
$count--;
|
||||
}
|
||||
|
||||
return array( 'count' => $count, 'words' => $language->addToStack( 'num_replies', NULL, array( 'pluralize' => array( $count ) ) ) );
|
||||
return array( 'count' => $count, 'words' => $language->addToStack( 'num_replies', FALSE, array( 'pluralize' => array( $count ) ) ) );
|
||||
}
|
||||
|
||||
if ( $object::supportsReviews( NULL, $container ) )
|
||||
{
|
||||
$count = $object->mapped('num_reviews');
|
||||
|
||||
return array( 'count' => $count, 'words' => $language->addToStack( 'num_reviews', NULL, array( 'pluralize' => array( $count ) ) ) );
|
||||
return array( 'count' => $count, 'words' => $language->addToStack( 'num_reviews', FALSE, array( 'pluralize' => array( $count ) ) ) );
|
||||
}
|
||||
|
||||
/* Valid object, but there isn't any date data available */
|
||||
@@ -456,14 +456,14 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$plaintextLanguage )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_container', NULL, array(
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_container', FALSE, array(
|
||||
'htmlsprintf' => array( $author->link(), $this->objectDatePosted->html( FALSE ) ),
|
||||
'sprintf' => array( $object->indefiniteArticle(), $container->url(), $container->_title ),
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_container_nolink', NULL, array(
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_container_nolink', FALSE, array(
|
||||
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $container->getTitleForLanguage( $plaintextLanguage ), $author->name, $this->objectDatePosted->relative( \IPS\DateTime::RELATIVE_FORMAT_NORMAL, $plaintextLanguage ) ),
|
||||
) );
|
||||
}
|
||||
@@ -472,14 +472,14 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$plaintextLanguage )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', NULL, array(
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
|
||||
'htmlsprintf' => array( $author->link(), $this->objectDatePosted->html( FALSE ) ),
|
||||
'sprintf' => array( $object->indefiniteArticle() )
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', NULL, array(
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
|
||||
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, $this->objectDatePosted->relative( \IPS\DateTime::RELATIVE_FORMAT_NORMAL, $plaintextLanguage ) )
|
||||
) );
|
||||
}
|
||||
@@ -489,14 +489,14 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$plaintextLanguage )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', NULL, array(
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
|
||||
'htmlsprintf' => array( $author->link(), $this->objectDatePosted->html( FALSE ) ),
|
||||
'sprintf' => array( $object->indefiniteArticle() )
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', NULL, array(
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
|
||||
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, $this->objectDatePosted->relative( \IPS\DateTime::RELATIVE_FORMAT_NORMAL, $plaintextLanguage ) )
|
||||
) );
|
||||
}
|
||||
@@ -505,14 +505,14 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$plaintextLanguage )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_node', NULL, array(
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_node', FALSE, array(
|
||||
'htmlsprintf' => array( $author->link() ),
|
||||
'sprintf' => array( $object->url(), $object->_title )
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_node_nolink', NULL, array(
|
||||
return $plaintextLanguage->addToStack( 'promote_metadescription_node_nolink', FALSE, array(
|
||||
'sprintf' => array( $object->getTitleForLanguage( $plaintextLanguage ), $author->name )
|
||||
) );
|
||||
}
|
||||
@@ -979,7 +979,7 @@ class _Promote extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
try
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_thing_in_thing', NULL, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $object::$title ), $object->item()->mapped('title') ) ) );
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_thing_in_thing', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $object::$title ), $object->item()->mapped('title') ) ) );
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
|
||||
@@ -276,7 +276,7 @@ class _Report extends \IPS\Content\Item implements \IPS\Content\ReadMarkers
|
||||
$container = NULL;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'tables', 'core', 'front' )->icon( $className, $container );
|
||||
return \IPS\Theme::i()->getTemplate( 'modcp', 'core', 'front' )->reportTableDescription( $className, $this, $container );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -317,7 +317,7 @@ class _Rules extends \IPS\Node\Model
|
||||
/* Loop over the filters */
|
||||
foreach( $this->_filters as $key => $filter )
|
||||
{
|
||||
if( isset( static::$extensions[ $key ] ) )
|
||||
if( isset( static::$extensions[ $key ] ) AND method_exists( static::$extensions[ $key ], 'matches' ) )
|
||||
{
|
||||
/* Ask the extension if this member matches the defined rule...if not, just return FALSE now */
|
||||
if( !static::$extensions[ $key ]->matches( $member, $filter, $this ) )
|
||||
@@ -342,7 +342,7 @@ class _Rules extends \IPS\Node\Model
|
||||
{
|
||||
return array(
|
||||
0 => 'ipsBadge ipsBadge_positive',
|
||||
1 => \IPS\Member::loggedIn()->language()->addToStack( 'automoderation_points_needed_badge', NULL, array( 'pluralize' => array( $this->points_needed ) ) )
|
||||
1 => \IPS\Member::loggedIn()->language()->addToStack( 'automoderation_points_needed_badge', FALSE, array( 'pluralize' => array( $this->points_needed ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ class _Import extends \IPS\Node\Model
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'rss_import_auto_follow', $this->auto_follow, FALSE, array(), NULL, NULL, NULL, 'rss_import_auto_follow' ) );
|
||||
$this->_extension->form( $form, $this );
|
||||
|
||||
\IPS\Member::loggedIn()->language()->words['rss_import_auto_follow'] = \IPS\Member::loggedIn()->language()->addToStack( 'rss_import_auto_follow_lang', NULL, array( 'sprintf' => array( $class::_definiteArticle() ) ) );
|
||||
\IPS\Member::loggedIn()->language()->words['rss_import_auto_follow'] = \IPS\Member::loggedIn()->language()->addToStack( 'rss_import_auto_follow_lang', FALSE, array( 'sprintf' => array( $class::_definiteArticle() ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -428,7 +428,7 @@ class _Import extends \IPS\Node\Model
|
||||
$timeNow = \IPS\DateTime::create();
|
||||
if( $article['date'] > $timeNow )
|
||||
{
|
||||
$artice['date'] = \IPS\DateTime::create();
|
||||
$article['date'] = \IPS\DateTime::create();
|
||||
}
|
||||
|
||||
$article['guid'] = $guid;
|
||||
@@ -576,6 +576,12 @@ class _Import extends \IPS\Node\Model
|
||||
'follow_visible' => 1,
|
||||
) );
|
||||
}
|
||||
|
||||
/* Re-index to pick up any changes to hidden, etc */
|
||||
if ( $item instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $item );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -449,7 +449,7 @@ class _Rss extends \IPS\Node\Model
|
||||
|
||||
if ( \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$url = $url->setQueryString( array( 'member_id' => \IPS\Member::loggedIn()->member_id, 'key' => md5( \IPS\Member::loggedIn()->member_id . \IPS\Member::loggedIn()->email . \IPS\Member::loggedIn()->member_pass_hash . \IPS\Member::loggedIn()->member_pass_salt ) ) );
|
||||
$url = $url->setQueryString( array( 'member_id' => \IPS\Member::loggedIn()->member_id, 'key' => \IPS\Member::loggedIn()->getUniqueMemberHash() ) );
|
||||
}
|
||||
|
||||
return $url;
|
||||
|
||||
@@ -104,7 +104,8 @@ class _Reply extends \IPS\Content\Comment implements \IPS\Content\Hideable
|
||||
*/
|
||||
public function url( $action='find' )
|
||||
{
|
||||
if ( $action === 'find' )
|
||||
/* Status Posts and comments don't support get PrefComment */
|
||||
if ( $action === 'find' or $action === 'getPrefComment' )
|
||||
{
|
||||
return $this->item()->url();
|
||||
}
|
||||
@@ -173,8 +174,8 @@ class _Reply extends \IPS\Content\Comment implements \IPS\Content\Hideable
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Profile owner should always be able to delete */
|
||||
if ( $member->member_id == $this->item()->member_id )
|
||||
/* Can we delete our own status updates? */
|
||||
if ( $this->item()->canDelete() )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ class _Status extends \IPS\Content\Item implements \IPS\Content\Lockable, \IPS\C
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief Include In Sitemap
|
||||
*/
|
||||
public static $includeInSitemap = FALSE;
|
||||
|
||||
/**
|
||||
* Columns needed to query for search result / stream view
|
||||
*
|
||||
@@ -468,7 +473,7 @@ class _Status extends \IPS\Content\Item implements \IPS\Content\Lockable, \IPS\C
|
||||
|
||||
/* Set the content and check to make sure it passes any profanity filters */
|
||||
$this->content = $this->id ? $values['status_content_' . $this->id ] : $values['status_content'];
|
||||
$sendFilterNotifications = $this->checkProfanityFilters( FALSE, $this->_new, NULL, FALSE );
|
||||
$sendFilterNotifications = $this->checkProfanityFilters( FALSE, !$this->_new, NULL, FALSE, 'core_Members', $this->_new ? ['status'] : NULL );
|
||||
|
||||
/* If this is an edit, and $sendFilterNotifications is FALSE, send after edit notifications */
|
||||
if ( !$this->_new AND $sendFilterNotifications === FALSE )
|
||||
@@ -490,6 +495,12 @@ class _Status extends \IPS\Content\Item implements \IPS\Content\Lockable, \IPS\C
|
||||
*/
|
||||
public function url( $action=NULL )
|
||||
{
|
||||
/* Status Posts and comments don't support get PrefComment */
|
||||
if ( $action === 'getPrefComment' )
|
||||
{
|
||||
$action = NULL;
|
||||
}
|
||||
|
||||
if( $this->_url === NULL )
|
||||
{
|
||||
$member = \IPS\Member::load( $this->member_id );
|
||||
@@ -621,25 +632,6 @@ class _Status extends \IPS\Content\Item implements \IPS\Content\Lockable, \IPS\C
|
||||
return ( $member->moderateNewContent( $considerPostBeforeRegistering ) or \IPS\Settings::i()->profile_comment_approval );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can delete?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canDelete( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Profile owner should always be able to delete */
|
||||
if ( $member->member_id == $this->member_id )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return parent::canDelete( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached comment count
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Stored Replies Node
|
||||
* @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 Core
|
||||
* @since 03 September 2021
|
||||
*/
|
||||
|
||||
namespace IPS\core;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored Replies Node
|
||||
*/
|
||||
class _StoredReplies extends \IPS\Node\Model implements \IPS\Node\Permissions
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_editor_stored_replies';
|
||||
|
||||
/**
|
||||
* @brief [Node] Node Title
|
||||
*/
|
||||
public static $nodeTitle = 'editor_stored_replies';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] ID Database Column
|
||||
*/
|
||||
public static $databasePrefix = 'reply_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Order Database Column
|
||||
*/
|
||||
public static $databaseColumnOrder = 'position';
|
||||
|
||||
/**
|
||||
* @brief [Node] ACP Restrictions
|
||||
* @code
|
||||
array(
|
||||
'app' => 'core', // The application key which holds the restrictrions
|
||||
'module' => 'foo', // The module key which holds the restrictions
|
||||
'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix"
|
||||
'add' => 'foo_add',
|
||||
'edit' => 'foo_edit',
|
||||
'permissions' => 'foo_perms',
|
||||
'delete' => 'foo_delete'
|
||||
),
|
||||
'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
|
||||
'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete"
|
||||
* @endcode
|
||||
*/
|
||||
protected static $restrictions = array(
|
||||
'app' => 'core',
|
||||
'module' => 'editor',
|
||||
'prefix' => 'storedReplies_'
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief [Node] App for permission index
|
||||
*/
|
||||
public static $permApp = 'core';
|
||||
|
||||
/**
|
||||
* @brief [Node] Type for permission index
|
||||
*/
|
||||
public static $permType = 'editorStoredReplies';
|
||||
|
||||
/**
|
||||
* @brief The map of permission columns
|
||||
*/
|
||||
public static $permissionMap = array(
|
||||
'view' => 'view'
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief [Node] Prefix string that is automatically prepended to permission matrix language strings
|
||||
*/
|
||||
public static $permissionLangPrefix = 'perm_editor_stored_reply_';
|
||||
|
||||
/**
|
||||
* @brief [Node] Enabled/Disabled Column
|
||||
*/
|
||||
public static $databaseColumnEnabledDisabled = 'enabled';
|
||||
|
||||
/**
|
||||
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
|
||||
*/
|
||||
public static $titleLangPrefix = 'editor_stored_replies_';
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Set whether or not this node is enabled
|
||||
*
|
||||
* @param bool|int $enabled Whether to set it enabled or disabled
|
||||
* @return void
|
||||
*/
|
||||
protected function set__enabled( $enabled )
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get Title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get__title()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Add/Edit Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form
|
||||
* @return void
|
||||
*/
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'editor_stored_replies_title', $this->id ? $this->title : NULL, TRUE ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'editor_stored_replies_enabled', $this->id ? $this->enabled : TRUE, FALSE, array( 'togglesOn' => array( 'editor_stored_replies_content' ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'editor_stored_replies_content', $this->id ? $this->text : FALSE, FALSE, array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => ( $this->id ? "core-editor-replies-{$this->id}" : "core-editor-replies" ), 'attachIds' => $this->id ? array( $this->id, NULL, 'editor_stored_replies' ) : NULL ), NULL, NULL, NULL, 'editor_stored_replies_content' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Format form values from add/edit form for save
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return array
|
||||
*/
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
foreach( [
|
||||
'editor_stored_replies_title' => 'title',
|
||||
'editor_stored_replies_enabled' => 'enabled',
|
||||
'editor_stored_replies_content' => 'text'
|
||||
] as $input => $name )
|
||||
{
|
||||
$values[ $name ] = $values[ $input ];
|
||||
unset( $values[ $input ] );
|
||||
}
|
||||
|
||||
/* Remove any lazy loading stuffs */
|
||||
$values['text'] = \IPS\Text\Parser::removeLazyLoad( $values['text'] );
|
||||
|
||||
$values['added_by'] = \IPS\Member::loggedIn()->loggedIn()->member_id;
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Perform actions after saving the form
|
||||
*
|
||||
* @param array $values Values from the form
|
||||
* @return void
|
||||
*/
|
||||
public function postSaveForm( $values )
|
||||
{
|
||||
/* Looks a bit weird, but as this is postSave, $this->id is filled and $this->_new is false, _permissions is always null if it's a new entry */
|
||||
\IPS\File::claimAttachments( ( $this->_permissions === null ) ? "core-editor-replies" : "core-editor-replies-{$this->id}", $this->id, NULL, 'editor_stored_replies' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
\IPS\File::unclaimAttachments( 'core_Admin', $this->id, NULL, 'editor_stored_replies' );
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Caches
|
||||
* @note Defined cache keys will be cleared automatically as needed
|
||||
*/
|
||||
protected $caches = array( 'editorStoredReplies' );
|
||||
|
||||
/**
|
||||
* Get data store
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getStore()
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->editorStoredReplies ) )
|
||||
{
|
||||
/* Don't get the reply text as this could make for a large store [but, dear future developer it might be fine to include it if you really have a need I didn't forsee]
|
||||
Oh and we grab permissions here so we don't need to do a full query each time the editor is loaded */
|
||||
\IPS\Data\Store::i()->editorStoredReplies = iterator_to_array(
|
||||
\IPS\Db::i()->select(
|
||||
'reply_id, reply_title, reply_added_by, reply_enabled, core_permission_index.perm_id, core_permission_index.perm_view',
|
||||
static::$databaseTable
|
||||
)->join(
|
||||
'core_permission_index',
|
||||
array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=" . static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnId, static::$permApp, static::$permType )
|
||||
)->setKeyField('reply_id')
|
||||
);
|
||||
}
|
||||
|
||||
return \IPS\Data\Store::i()->editorStoredReplies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permission index permissions
|
||||
*
|
||||
* @param array $insert Permission data to insert
|
||||
* @return void
|
||||
*/
|
||||
public function setPermissions( $insert )
|
||||
{
|
||||
parent::setPermissions( $insert );
|
||||
|
||||
/* Clear cache */
|
||||
unset( \IPS\Data\Store::i()->editorStoredReplies );
|
||||
}
|
||||
}
|
||||
@@ -476,6 +476,12 @@ class _Stream extends \IPS\Node\Model
|
||||
'unread' => 'stream_read_unread',
|
||||
) ) ) );
|
||||
|
||||
/* Solved */
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'stream_solved', $this->solved, TRUE, array( 'options' => array(
|
||||
'all' => 'stream_solved_all',
|
||||
'solved' => 'stream_solved_solved',
|
||||
'unsolved' => 'stream_solved_unsolved',
|
||||
) ) ) );
|
||||
|
||||
/* Follow */
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'stream_follow', $this->follow, TRUE, array(
|
||||
@@ -940,6 +946,15 @@ class _Stream extends \IPS\Node\Model
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $this->solved == 'unsolved' )
|
||||
{
|
||||
$terms[] = \IPS\Member::loggedIn()->language()->addToStack('stream_blurb_unsolved');
|
||||
}
|
||||
else if ( $this->solved =='solved')
|
||||
{
|
||||
$terms[] = \IPS\Member::loggedIn()->language()->addToStack('stream_blurb_solved');
|
||||
}
|
||||
|
||||
if ( \count( $terms ) )
|
||||
{
|
||||
@@ -971,6 +986,7 @@ class _Stream extends \IPS\Node\Model
|
||||
'stream_ownership' => $this->ownership,
|
||||
'stream_date_type' => $this->date_type,
|
||||
'stream_classes' => NULL,
|
||||
'stream_solved' => $this->solved,
|
||||
'containers' => static::containersToUrl( $this->containers ),
|
||||
);
|
||||
|
||||
@@ -1105,6 +1121,9 @@ class _Stream extends \IPS\Node\Model
|
||||
{
|
||||
/* Init */
|
||||
$query = \IPS\Content\Search\Query::init( $member );
|
||||
|
||||
/* Get Member */
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Content Filters */
|
||||
$filters = array();
|
||||
@@ -1130,15 +1149,16 @@ class _Stream extends \IPS\Node\Model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Build the filters */
|
||||
foreach ( $classes as $class => $options )
|
||||
{
|
||||
/* Init */
|
||||
if ( ! $this->include_comments or ( isset( $options['items'] ) and !isset( $options['comments'] ) and \in_array( 'IPS\Content\Item', class_parents( $class ) ) and $class::$firstCommentRequired ) )
|
||||
{
|
||||
/* items with no comments are flagged as being the last comment */
|
||||
$filter = \IPS\Content\Search\ContentFilter::init( $class, TRUE, TRUE, TRUE )->onlyLastComment();
|
||||
/* As we do not want comments to appear separately, we want to only show the content item if no comments
|
||||
or the last comment of the content item */
|
||||
$filter= \IPS\Content\Search\ContentFilter::init( $class, TRUE, TRUE, TRUE )->onlyLastComment();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1166,6 +1186,8 @@ class _Stream extends \IPS\Node\Model
|
||||
{
|
||||
if( !$this->include_comments )
|
||||
{
|
||||
/* As we do not want comments to appear separately, we want to only show the content item if no comments
|
||||
or the last comment of the content item */
|
||||
$filters[] = \IPS\Content\Search\ContentFilter::init( $class, TRUE, TRUE, TRUE )->onlyLastComment();
|
||||
}
|
||||
|
||||
@@ -1200,7 +1222,7 @@ class _Stream extends \IPS\Node\Model
|
||||
switch ( $this->ownership )
|
||||
{
|
||||
case 'started':
|
||||
$query->filterByItemAuthor( \IPS\Member::loggedIn() );
|
||||
$query->filterByItemAuthor( $member );
|
||||
break;
|
||||
|
||||
case 'postedin':
|
||||
@@ -1232,7 +1254,7 @@ class _Stream extends \IPS\Node\Model
|
||||
switch ( $this->date_type )
|
||||
{
|
||||
case 'last_visit':
|
||||
$query->$filterDateMethod( \IPS\DateTime::ts( \IPS\Member::loggedIn()->last_visit ) );
|
||||
$query->$filterDateMethod( \IPS\DateTime::ts( $member->last_visit ) );
|
||||
break;
|
||||
case 'relative':
|
||||
if ( \is_array( $this->date_relative_days ) )
|
||||
@@ -1247,6 +1269,16 @@ class _Stream extends \IPS\Node\Model
|
||||
$query->$filterDateMethod( $this->date_start ? \IPS\DateTime::ts( $this->date_start ) : NULL, $this->date_end ? \IPS\DateTime::ts( $this->date_end ) : NULL );
|
||||
break;
|
||||
}
|
||||
|
||||
/* Solved */
|
||||
if ( $this->solved == 'unsolved' )
|
||||
{
|
||||
$query->filterByUnsolved();
|
||||
}
|
||||
else if( $this->solved == 'solved' )
|
||||
{
|
||||
$query->filterBySolved();
|
||||
}
|
||||
|
||||
/* Sort */
|
||||
if ( $this->include_comments )
|
||||
@@ -1315,6 +1347,9 @@ class _Stream extends \IPS\Node\Model
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
/* Delete Subscriptions */
|
||||
\IPS\Db::i()->delete( 'core_stream_subscriptions', ['stream_id=?', $this->id ] );
|
||||
|
||||
if ( $this->member )
|
||||
{
|
||||
parent::delete();
|
||||
@@ -1345,4 +1380,80 @@ class _Stream extends \IPS\Node\Model
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the member subscribe to this stream?
|
||||
*
|
||||
* @param \IPS\Member|null $member
|
||||
* @return bool
|
||||
*/
|
||||
public function canSubscribe( \IPS\Member $member = NULL ) : bool
|
||||
{
|
||||
if( !\IPS\Settings::i()->activity_stream_subscriptions )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if( !$this->id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( \IPS\Db::i()->select( 'COUNT(*)', 'core_stream_subscriptions', array( 'member_id=?', $member->member_id ) )->first() >= \IPS\Settings::i()->activity_stream_subscriptions_max )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return !$this->isSubscribed($member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the member unsubscribe from this stream?
|
||||
*
|
||||
* @param \IPS\Member|null $member
|
||||
* @return bool
|
||||
*/
|
||||
public function canUnsubscribe( \IPS\Member $member = NULL ) : bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return $this->isSubscribed($member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we already subscribed to this stream?
|
||||
*
|
||||
* @param \IPS\Member|null $member
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubscribed( \IPS\Member $member = NULL ) : bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return (bool) \IPS\Db::i()->select( 'COUNT(*)', 'core_stream_subscriptions', array( 'stream_id=? AND member_id=?', $this->id, $member->member_id ) )->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Return the custom badge for each row
|
||||
*
|
||||
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
|
||||
*/
|
||||
protected function get__badge()
|
||||
{
|
||||
$badge = NULL;
|
||||
if ( $this->default )
|
||||
{
|
||||
$badge = array(
|
||||
0 => 'positive ipsPos_right',
|
||||
1 => 'default_no_parenthesis'
|
||||
);
|
||||
}
|
||||
|
||||
return $badge;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Discovery Stream Subscription
|
||||
* @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 30 Jul 2021
|
||||
*/
|
||||
|
||||
namespace IPS\core\Stream;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Discovery Stream Subscription
|
||||
*/
|
||||
class _Subscription extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_stream_subscriptions';
|
||||
|
||||
|
||||
/*
|
||||
* Get the stream object
|
||||
*
|
||||
* return \IPS\core\Stream
|
||||
*/
|
||||
public function get_stream() : \IPS\core\Stream
|
||||
{
|
||||
return \IPS\core\Stream::load( $this->stream_id) ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Digest
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function send( array $data, $stream, \IPS\Member $recipient, $subscriptionRow, $showMoreLink = FALSE )
|
||||
{
|
||||
$email = \IPS\Email::buildFromTemplate( 'core', 'activity_stream_subscription', array( $stream, $data, $recipient, $subscriptionRow, $showMoreLink ), \IPS\Email::TYPE_LIST );
|
||||
$email->setUnsubscribe( 'core', 'unsubscribeStream', array( $subscriptionRow['id'], $stream->_title, md5( $recipient->email . ';' . $recipient->ip_address . ';' . $recipient->joined->getTimestamp() ) ) );
|
||||
|
||||
$email->send($recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of digests
|
||||
*
|
||||
* @param string $frequency One of either "daily" or "weekly" to denote the kind of digest to send
|
||||
* @param int $numberToSend The number of digests to send for this batch
|
||||
* @return bool
|
||||
*/
|
||||
public static function sendBatch( $frequency = 'daily', $numberToSend = 25 )
|
||||
{
|
||||
$subscriptions = iterator_to_array(
|
||||
\IPS\Db::i()->select( 'core_stream_subscriptions.*, core_members.last_visit', 'core_stream_subscriptions', array('frequency = ? AND sent < ? and last_visit > ?', $frequency, ( $frequency == 'daily' ) ? time() - 86400 : time() - 604800, time() - \IPS\Settings::i()->activity_stream_subscriptions_inactive_limit * 86400), 'sent ASC', array(0, $numberToSend), NULL, NULL, \IPS\Db::SELECT_DISTINCT | \IPS\Db::SELECT_FROM_WRITE_SERVER )
|
||||
->join( 'core_members', 'core_stream_subscriptions.member_id=core_members.member_id') );
|
||||
|
||||
if ( !\count( $subscriptions ) )
|
||||
{
|
||||
/* Nothing to send */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ( $subscriptions as $row )
|
||||
{
|
||||
$member = \IPS\Member::load( $row['member_id'] );
|
||||
if ( !$member->email )
|
||||
{
|
||||
/* Update sent time, so the batch doesn't get stuck in a loop */
|
||||
\IPS\Db::i()->update( 'core_stream_subscriptions', array( 'sent' => time() ), array( 'id=?', $row['id'] ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Build it */
|
||||
$mail = new static;
|
||||
$data = $mail->getContentForStream( $row );
|
||||
$ids[] = $row['id'];
|
||||
if( $items = \count( $data ) )
|
||||
{
|
||||
$showMore = FALSE;
|
||||
if( $items > 10 )
|
||||
{
|
||||
$data = \array_slice($data, 0, 10);
|
||||
$showMore = TRUE;
|
||||
}
|
||||
|
||||
$mail->send( $data, \IPS\core\Stream::load( $row['stream_id'] ), \IPS\Member::load( $row['member_id'] ), $row, $showMore );
|
||||
}
|
||||
}
|
||||
|
||||
if( \count( $ids ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_stream_subscriptions', array( 'sent' => time() ), \IPS\Db::i()->in( 'id', $ids ) );
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \IPS\core\Stream $stream
|
||||
* @param \IPS\Member|null $member
|
||||
* @return Subscription|null
|
||||
*/
|
||||
public static function loadByStreamAndMember( \IPS\core\Stream $stream, \IPS\Member $member = NULL ) : ?\IPS\core\Stream\Subscription
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
try
|
||||
{
|
||||
return static::constructFromData( \IPS\Db::i()->select('*', static::$databaseTable, ['stream_id=? AND member_id=?', $stream->id, $member->member_id ] )->first() );
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the content for the stream
|
||||
*
|
||||
* @param array $subscriptionRow
|
||||
* @return array
|
||||
*/
|
||||
public function getContentForStream( array $subscriptionRow ) : array
|
||||
{
|
||||
$items = [];
|
||||
$stream = \IPS\core\Stream::load( $subscriptionRow['stream_id'] );
|
||||
|
||||
$query = $stream->query( \IPS\Member::load( $subscriptionRow['member_id']) );
|
||||
|
||||
|
||||
/* Override the timeframe and set it to the last sent time */
|
||||
$query->filterByCreateDate( \IPS\DateTime::ts( $subscriptionRow['sent'] ) );
|
||||
/* We want only 10 items for the email, so we'll grab 11 to see if we need to show the "more link" */
|
||||
$query->setLimit(11);
|
||||
|
||||
/* Get the results */
|
||||
$results = $query->search( NULL, $stream->tags ? explode( ',', $stream->tags ) : NULL, ( $stream->include_comments ? \IPS\Content\Search\Query::TAGS_MATCH_ITEMS_ONLY + \IPS\Content\Search\Query::TERM_OR_TAGS : \IPS\Content\Search\Query::TERM_OR_TAGS ) );
|
||||
|
||||
/* Load data we need like the authors, etc */
|
||||
$results->init();
|
||||
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
$data = $result->asArray();
|
||||
$itemClass = $data['indexData']['index_class'];
|
||||
$object = $itemClass::load( $data['indexData']['index_object_id']);
|
||||
|
||||
if( \in_array( 'IPS\Content\Comment', class_parents( $itemClass ) ) )
|
||||
{
|
||||
$itemClass = $itemClass::$itemClass;
|
||||
}
|
||||
|
||||
$containerUrl = NULL;
|
||||
$containerTitle = NULL;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$containerTitle = $containerClass::titleFromIndexData( $data['indexData'], $data['itemData'], $data['containerData'] );
|
||||
$containerUrl = $containerClass::urlFromIndexData( $data['indexData'], $data['itemData'], $data['containerData'] );
|
||||
}
|
||||
|
||||
$items[] = array_merge($data, [
|
||||
'title' => $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(),
|
||||
'url' => $object->url(),
|
||||
'content' => $object->content(),
|
||||
'object' => $object,
|
||||
'date' => \IPS\DateTime::ts( $object->mapped('date') ),
|
||||
'itemClass' => $itemClass,
|
||||
'containerUrl' => $containerUrl,
|
||||
'containerTitle' => $containerTitle
|
||||
]);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
/**
|
||||
* Has the member any subscribed streams
|
||||
*
|
||||
* @param \IPS\Member|null $member
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasSubscribedStreams( \IPS\Member $member = NULL ) : bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return (bool) \IPS\Db::i()->select( 'COUNT(*)', 'core_stream_subscriptions', array( 'member_id=?', $member->member_id ) )->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the member any subscribed streams
|
||||
*
|
||||
* @param \IPS\Member|null $member
|
||||
* @return \Iterator
|
||||
*/
|
||||
public static function getSubscribedStreams( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_stream_subscriptions', array( 'member_id=?', $member->member_id ) ), \get_called_class() );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -345,7 +345,7 @@ class _Warning extends \IPS\Content\Item
|
||||
$elements[] = \IPS\Member::loggedIn()->language()->addToStack('member_existing_penalties', TRUE, array( 'sprintf' => $member->name ) );
|
||||
}
|
||||
|
||||
\IPS\Member::loggedIn()->language()->words['warn_cheeve_point_reduction_desc'] = \IPS\Member::loggedIn()->language()->addToStack('warn_cheeve_point_reduction__desc', NULL, [ 'sprintf' => [ $member->name, $member->achievements_points, $member->name ] ] );
|
||||
\IPS\Member::loggedIn()->language()->words['warn_cheeve_point_reduction_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'warn_cheeve_point_reduction__desc', FALSE, [ 'sprintf' => [ $member->name, $member->achievements_points, $member->name ] ] );
|
||||
|
||||
/* Return */
|
||||
return $elements;
|
||||
@@ -427,7 +427,10 @@ class _Warning extends \IPS\Content\Item
|
||||
$member->members_bitoptions['unacknowledged_warnings'] = (bool) \IPS\Settings::i()->warnings_acknowledge;
|
||||
$member->save();
|
||||
$member->logHistory( 'core', 'warning', array( 'wid' => $this->id, 'points' => $this->points, 'reason' => $this->reason, 'consequences' => $consequences ) );
|
||||
|
||||
|
||||
/* Moderator log */
|
||||
\IPS\Session::i()->modLog( 'modlog__action_warn_user', array( $member->name => FALSE ) );
|
||||
|
||||
parent::processAfterCreate( $comment, $values );
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user