Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+253 -36
View File
@@ -66,23 +66,52 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Post count for member
*
* @param \IPS\Member $member The memner
* @param \IPS\Member $member The member
* @param bool $includeNonPostCountIncreasing If FALSE, will skip any posts which would not cause the user's post count to increase
* @param bool $includeHiddenAndPendingApproval If FALSE, will skip any hidden posts, or posts pending approval
* @return int
*/
public static function memberPostCount( \IPS\Member $member )
public static function memberPostCount( \IPS\Member $member, bool $includeNonPostCountIncreasing = FALSE, bool $includeHiddenAndPendingApproval = TRUE )
{
return ( isset( static::$databaseColumnMap['author'] ) and static::incrementPostCount() ) ? \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ) )->first() : 0;
if ( !isset( static::$databaseColumnMap['author'] ) )
{
return 0;
}
if ( !$includeNonPostCountIncreasing and !static::incrementPostCount() )
{
return 0;
}
$where = [];
$where[] = [ static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ];
if ( !$includeHiddenAndPendingApproval )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$where[] = [ static::$databasePrefix . static::$databaseColumnMap['hidden'] . '=0' ];
}
if ( isset( static::$databaseColumnMap['approved'] ) )
{
$where[] = [ static::$databasePrefix . static::$databaseColumnMap['approved'] . '=1' ];
}
}
return \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, $where )->first();
}
/**
* Post count for member
*
* @param \IPS\Member $member The memner
* @deprecated Use options provided to memberPostCount()
*
* @param \IPS\Member $member The member
* @return int
*/
public static function rawMemberPostCount( \IPS\Member $member )
{
return static::memberPostCount( $member );
return static::memberPostCount( $member, TRUE );
}
/**
@@ -294,10 +323,16 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public function author()
{
if ( $this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') )
if( $this->isAnonymous() )
{
$guest = new \IPS\Member;
$guest->name = \IPS\Member::loggedIn()->language()->addToStack( "post_anonymously_placename" );
return $guest;
}
elseif ( $this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') )
{
return \IPS\Member::load( $this->mapped('author') );
}
}
else
{
$guest = new \IPS\Member;
@@ -344,6 +379,16 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) );
}
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Recognizable' ) )
{
\IPS\Db::i()->delete( 'core_member_recognize', array( 'r_content_class=? AND r_content_id=?', \get_class( $this ), $this->$idColumn ) );
}
if( $this instanceof \IPS\Content\Anonymous )
{
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_object_class=? AND anonymous_object_id=?', \get_class( $this ), $this->$idColumn ) );
}
/* Remove any entries in the promotions table */
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', \get_class( $this ), $this->$idColumn ) );
@@ -355,6 +400,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
}
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
parent::delete();
@@ -428,6 +475,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$title = $title ?: $this->mapped('title');
}
}
/* If this content is "post before register", skip this check (we'll do it after registration is complete) */
if ( $this->hidden() === -3 or ( $item::$firstCommentRequired and $first and $item->hidden() === -3 ) )
{
return FALSE;
}
/* And the author */
$member = $this->author();
@@ -444,21 +497,21 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/* If this is a content item that doesn't require a first comment, or a comment that isn't the first comment, this just pass the content as it stands. */
if ( !$this->hidden() AND ( ( $this instanceof \IPS\Content\Item ) OR ( $this instanceof \IPS\Content\Comment AND ( !$item::$firstCommentRequired OR ( $item::$firstCommentRequired AND !$first ) ) ) ) AND !$member->group['g_bypass_badwords'] )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$hiddenByFilter )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
}
/* If the comment is hidden (probably requires moderator approval) and this is the first post in the item that requires a comment (i.e. a post in a topic), then we need to set the item as requiring approval instead. */
elseif ( $item instanceof \IPS\Content\Hideable AND !$member->group['g_bypass_badwords'] AND ( $item::$firstCommentRequired and $first ) )
{
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$itemHiddenByFilter )
{
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
if ( isset( $item::$databaseColumnMap['approved'] ) and $itemHiddenByFilter )
@@ -682,6 +735,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return NULL;
}
/**
* @brief Flag to skip rebuilding container data (because it will be rebuilt in one batch later)
*/
public $skipContainerRebuild = FALSE;
/**
* Do Moderator Action
@@ -721,7 +779,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/* Log */
\IPS\Session::i()->modLog( 'modlog__action_' . $action, array( static::$title => TRUE, $this->url()->__toString() => FALSE, $this->mapped('title') ?: ( method_exists( $this, 'item' ) ? $this->item()->mapped('title') : NULL ) => FALSE ), ( $this instanceof \IPS\Content\Item ) ? $this : $this->item() );
$idColumn = static::$databaseColumnId;
/* These ones just need a property setting */
if ( \in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'lock', 'unlock' ) ) )
{
@@ -736,6 +796,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
case 'unfeature':
$val = FALSE;
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
case 'feature':
$column = static::$databaseColumnMap['featured'];
break;
@@ -757,6 +818,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$this->$column = $val;
$this->save();
if ( $action == 'feature' )
{
\IPS\Db::i()->insert( 'core_content_featured', [
'feature_content_id' => $this->$idColumn,
'feature_content_class' => \get_class( $this ),
'feature_content_author' => $this->author()->member_id,
'feature_date' => time()
] );
/* Points */
$this->author()->achievementAction( 'core', 'ContentPromotion', [
'content' => $this,
'promotype' => 'feature'
] );
}
return;
}
@@ -874,13 +951,27 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
try
{
if ( $this->container() )
if ( $this->container() AND !$this->skipContainerRebuild )
{
$this->container()->resetCommentCounts();
$this->container()->setLastComment();
$this->container()->setLastReview();
$this->container()->save();
}
/* Update mappings */
if ( $this->container() and \IPS\IPS::classUsesTrait( $this->container(), 'IPS\Node\Statistics' ) )
{
if ( $this instanceof \IPS\Content\Comment )
{
$this->container()->rebuildPostedIn( array( $this->mapped('item') ) );
}
else
{
$idColumn = static::$databaseColumnId;
$this->container()->rebuildPostedIn( array($this->$idColumn) );
}
}
}
catch( \BadMethodCallException $e ) {}
catch( \OutOfRangeException $e ) {}
@@ -959,7 +1050,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Recount the container counters */
if( $this->container() )
if( $this->container() AND !$this->skipContainerRebuild )
{
$this->container()->resetCommentCounts();
$this->container()->setLastComment();
@@ -1145,12 +1236,26 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$this->sendApprovedNotification();
}
/* Award points */
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) ? static::$itemClass : \get_called_class();
if ( $this instanceof \IPS\Content\Item )
{
$this->author()->achievementAction( 'core', 'NewContentItem', $this );
}
elseif ( $this instanceof \IPS\Content\Review )
{
$this->author()->achievementAction( 'core', 'Review', $this );
}
elseif ( $this instanceof \IPS\Content\Comment and ( !$itemClass::$firstCommentRequired or $this->isFirst() ) )
{
$this->author()->achievementAction( 'core', 'Comment', $this );
}
/* Send webhook if necessary */
if ( $approving )
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_create', $this, $this->webhookFilters() );
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) ? static::$itemClass : \get_called_class();
if ( $this instanceof \IPS\Content\Item and $itemClass::$firstCommentRequired === TRUE and $firstComment = $this->firstComment() )
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_class( $firstComment ), 3 ) ) . '_create', $firstComment, $firstComment->webhookFilters() );
@@ -1270,7 +1375,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Can the member report content? */
if ( $member->group['g_can_report'] != '1' AND !\in_array( \get_class( $this ), explode( ',', $member->group['g_can_report'] ) ) )
$classToCheck = ( $this instanceof \IPS\Content\Comment or $this instanceof \IPS\Content\Review ) ? \get_class( $this->item() ) : \get_class( $this );
if ( $member->group['g_can_report'] != '1' AND !\in_array( $classToCheck, explode( ',', $member->group['g_can_report'] ) ) )
{
return 'no_module_permission';
}
@@ -1479,7 +1586,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return;
}
foreach ( array( 'author', 'author_name', 'edit_member_name' ) as $k )
foreach ( array( 'author', 'author_name', 'edit_member_name', 'is_anon' ) as $k )
{
if ( isset( static::$databaseColumnMap[ $k ] ) )
{
@@ -1489,15 +1596,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
case 'author':
$this->$col = $newAuthor->member_id ? $newAuthor->member_id : 0;
break;
case 'author_name':
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not */
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
break;
case 'is_anon':
/* We are specifying an author so turn off is_anon column */
if ( $newAuthor->member_id )
{
$this->$col = 0;
}
break;
}
}
}
$this->save();
if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' and $log )
@@ -1523,7 +1638,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param array $reactions Reaction Data
* @return string
*/
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() )
public static function searchResult( array $indexData, array $authorData, array $itemData, ?array $containerData, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() )
{
/* Item details */
$itemClass = $indexData['index_class'];
@@ -1623,7 +1738,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param string $view 'expanded' or 'condensed'
* @return callable
*/
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $view )
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, ?array $containerData, array $reputationData, $reviewRating, $view )
{
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
}
@@ -1641,7 +1756,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData )
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group );
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
if( \in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
{
@@ -1771,7 +1886,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
if ( $days and isset( static::$databaseColumnMap['ip_address'] ) and isset( static::$databaseColumnMap['date'] ) )
{
$time = time() - ( 86400 * $days );
\IPS\Db::i()->update( static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['ip_address'] => '' ), array( static::$databasePrefix . static::$databaseColumnMap['date'] . ' <= ' . $time ) );
\IPS\Db::i()->update( static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['ip_address'] => '' ), array( static::$databasePrefix . static::$databaseColumnMap['ip_address'] . "!='' AND " . static::$databasePrefix . static::$databaseColumnMap['date'] . ' <= ' . $time ) );
}
}
@@ -1819,11 +1934,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Get a count of the database table
*
* @return int
* @param bool $approximate Accept an approximate result if the table is large (approximate results are faster on large tables)
* @return int
*/
public static function databaseTableCount()
public static function databaseTableCount( bool $approximate=FALSE ): int
{
$key = 'tbl_cnt_' . static::$databaseTable;
$key = 'tbl_cnt_' . ( $approximate ? 'approx_' : 'accurate_' ) . static::$databaseTable;
$fetchAgain = FALSE;
if ( ! isset( \IPS\Data\Store::i()->$key ) )
@@ -1840,12 +1956,25 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$fetchAgain = TRUE;
}
}
if ( $fetchAgain )
{
\IPS\Data\Store::i()->$key = array( 'time' => time(), 'count' => \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable )->first() );
$count = 0;
/* Accept approximate result? */
if ( $approximate )
{
$approxRows = \IPS\Db::i()->query( "SHOW TABLE STATUS LIKE '" . \IPS\Db::i()->prefix . static::$databaseTable . "';" )->fetch_assoc();
$count = (int) $approxRows['Rows'];
}
/* If the table is a reasonable size, we'll get the real value instead */
if ( $count < 1000000 )
{
$count = \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable )->first();
}
\IPS\Data\Store::i()->$key = array( 'time' => time(), 'count' => $count );
}
$data = \IPS\Data\Store::i()->$key;
return $data['count'];
}
@@ -1957,6 +2086,21 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
public function sendApprovedNotification()
{
$this->sendNotifications();
$this->sendAuthorApprovalNotification();
}
/**
* Send Author Approval Notification
*
* @return void
*/
public function sendAuthorApprovalNotification()
{
/* Tell the author their content has been approved */
$member = \IPS\Member::load( $this->mapped('author') );
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'approved_content', $this, array( $this ), array(), FALSE );
$notification->recipients->attach( $member );
$notification->send();
}
/**
@@ -2044,7 +2188,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
public function sendAfterEditNotifications( $oldContent )
{
$existingData = static::_getQuoteAndMentionIdsFromContent( $oldContent );
$this->sendQuoteAndMentionNotifications( array_unique( array_merge( $existingData['quotes'], $existingData['mentions'] ) ) );
$this->sendQuoteAndMentionNotifications( array_unique( array_merge( $existingData['quotes'], $existingData['mentions'], $existingData['embeds'] ) ) );
}
/**
@@ -2316,7 +2460,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param string $title Title
* @return callable
*/
public function approvalQueueHtml( $ref=NULL, $container, $title )
public function approvalQueueHtml( $ref, $container, $title )
{
return \IPS\Theme::i()->getTemplate( 'modcp', 'core', 'front' )->approvalQueueItem( $this, $ref, $container, $title );
}
@@ -2349,13 +2493,14 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Definite Article
*
* @param \IPS\Lang|NULL $lang The language to use, or NULL for the language of the currently logged in member
* @param \IPS\Lang|NULL $lang The language to use, or NULL for the language of the currently logged in member
* @param integer|boolean $count Number of items. If not FALSE, pluralized version of phrase will be used
* @return string
*/
public function definiteArticle( \IPS\Lang $lang = NULL )
public function definiteArticle( \IPS\Lang $lang = NULL, $count = FALSE )
{
$container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper();
return static::_definiteArticle( $container ? $container->_data : array(), $lang );
return static::_definiteArticle( $container ? $container->_data : array(), $lang, array(), $count );
}
/**
@@ -2364,11 +2509,24 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param array $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
* @param \IPS\Lang|NULL $lang The language to use, or NULL for the language of the currently logged in member
* @param array $options Options to pass to \IPS\Lang::addToStack
* @param integer|boolean $count Number of items. If not false, pluralized version of phrase will be used.
* @return string
*/
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL, $options = array() )
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL, $options = array(), $count = FALSE )
{
$lang = $lang ?: \IPS\Member::loggedIn()->language();
if( ( \is_int( $count ) || $count === TRUE ) && $lang->checkKeyExists('__defart_' . static::$title . '_plural') )
{
/* If $count is TRUE, use the pluralized form but don't pluralize here - useful if we're passing to JS for example */
if( \is_int( $count ) )
{
$options['pluralize'] = array( $count );
}
return $lang->addToStack( '__defart_' . static::$title . '_plural', FALSE, $options );
}
return $lang->addToStack( '__defart_' . static::$title, FALSE, $options );
}
@@ -2386,7 +2544,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
try
{
return (string) \IPS\File::get( 'core_Icons', $shareLogos[0] )->url->setScheme( ( \IPS\Request::i()->isSecure() ) ? 'https' : 'http' );
return (string) \IPS\File::get( 'core_Icons', array_shift($shareLogos) )->url->setScheme( ( \IPS\Request::i()->isSecure() ) ? 'https' : 'http' );
}
catch( \Exception $e )
{
@@ -2553,4 +2711,63 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$filters['author'] = $this->author()->member_id;
return $filters;
}
/**
* Set anonymous state
*
* @param bool $state The state, TRUE for anonymous FALSE for not
* @param \IPS\Member|NULL $member The member posting anonymously or NULL for logged in member
* @return void
*/
public function setAnonymous( $state = TRUE, $member = NULL )
{
if( !$this instanceof \IPS\Content\Anonymous or !$this->container()->canPostAnonymously() )
{
throw new \BadMethodCallException;
}
$member = $member ?: \IPS\Member::loggedIn();
$class = \get_class( $this );
$idColumn = static::$databaseColumnId;
$anonColumn = static::$databaseColumnMap['is_anon'];
if( $state )
{
/* Insert the anonymous map */
$save = array(
'anonymous_member_id' => $member->member_id,
'anonymous_object_class' => $class,
'anonymous_object_id' => $this->$idColumn
);
\IPS\Db::i()->replace( 'core_anonymous_posts', $save );
$member = new \IPS\Member;
$this->$anonColumn = 1;
}
else
{
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_member_id=? and anonymous_object_class=? and anonymous_object_id=?', $member->member_id, $class, $this->$idColumn ) );
$this->$anonColumn = 0;
}
$this->save();
$this->changeAuthor( $member, FALSE );
if ( \in_array( 'IPS\Content\Comment', class_parents( $this ) ) )
{
$this->item()->rebuildFirstAndLastCommentData();
}
$this->expireWidgetCaches();
}
/**
* Is this an anonymous entry?
*
* @return bool
*/
public function isAnonymous()
{
return $this->mapped('is_anon');
}
}