Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
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 [];
}
@@ -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? */
+489
View File
@@ -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.
+2 -2
View File
@@ -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'] ];
}
}
+11 -11
View File
@@ -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 )
{
+1 -1
View File
@@ -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 );
}
/**
+2 -2
View File
@@ -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 ) ) )
);
}
+8 -2
View File
@@ -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
{
+1 -1
View File
@@ -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;
+4 -3
View File
@@ -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;
}
+12 -20
View File
@@ -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 );
}
}
+116 -5
View File
@@ -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 );
}