Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

+284 -56
View File
@@ -12,6 +12,17 @@
namespace IPS\forums;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Db;
use IPS\forums\Topic\ArchivedPost;
use IPS\Request;
use function array_merge;
use function array_reverse;
use function explode;
use function is_null;
use function is_string;
use function preg_replace;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -445,23 +456,6 @@ class _Topic extends \IPS\Content\Item implements
/* Return */
return $where;
}
/**
* Total item \count(including children)
*
* @param \IPS\Node\Model $container The container
* @param bool $includeItems If TRUE, items will be included (this should usually be true)
* @param bool $includeComments If TRUE, comments will be included
* @param bool $includeReviews If TRUE, reviews will be included
* @param int $depth Used to keep track of current depth to avoid going too deep
* @return int|NULL|string When depth exceeds 10, will return "NULL" and initial call will return something like "100+"
* @note This method may return something like "100+" if it has lots of children to avoid exahusting memory. It is intended only for display use
* @note This method includes counts of hidden and unapproved content items as well
*/
public static function contentCount( \IPS\Node\Model $container, $includeItems=TRUE, $includeComments=FALSE, $includeReviews=FALSE, $depth=0 )
{
return parent::contentCount( $container, FALSE, TRUE, $includeReviews, $depth );
}
/**
* Total item, items only \count(including children)
@@ -527,7 +521,7 @@ class _Topic extends \IPS\Content\Item implements
$current[] = 'pin';
}
}
$canHide = ( $item ) ? $item->canHide() : ( \IPS\Member::loggedIn()->group['g_hide_own_posts'] == '1' or \in_array( 'IPS\forums\Topics', explode( ',', \IPS\Member::loggedIn()->group['g_hide_own_posts'] ) ) );
$canHide = ( $item ) ? $item->canHide() : ( \IPS\Member::loggedIn()->group['g_hide_own_posts'] == '1' or \in_array( 'IPS\forums\Topic', explode( ',', \IPS\Member::loggedIn()->group['g_hide_own_posts'] ) ) );
if ( static::modPermission( 'hide', NULL, $container ) or $canHide )
{
$options['hide'] = 'create_topic_hidden';
@@ -729,6 +723,11 @@ class _Topic extends \IPS\Content\Item implements
*/
public static $seoTitleColumn = 'title_seo';
/**
* @brief Are hot topics enabled?
*/
protected static $_hotTopicsEnabled = null;
/**
* Stats for table view
*
@@ -737,7 +736,19 @@ class _Topic extends \IPS\Content\Item implements
*/
public function stats( $includeFirstCommentInCommentCount=TRUE )
{
if ( $this->popular_time !== NULL and $this->popular_time > time() )
/* Cache whether hot topics are enabled */
if( static::$_hotTopicsEnabled === null )
{
static::$_hotTopicsEnabled = false;
$popularNowSettings = json_decode( \IPS\Settings::i()->forums_popular_now, TRUE );
if( !empty( $popularNowSettings['minutes'] ) )
{
static::$_hotTopicsEnabled = true;
}
}
if ( static::$_hotTopicsEnabled and $this->popular_time !== NULL and $this->popular_time > time() )
{
$this->hotStats[] = 'forums_comments';
$this->hotStats[] = 'answers_no_number';
@@ -768,34 +779,94 @@ class _Topic extends \IPS\Content\Item implements
return $stats;
}
/**
* @var array|null
*/
protected static ?array $_hotTopics = null;
/**
* Set the new popular time if needed
*
* @return void
*/
public function rebuildPopularTime()
{
$popularNowSettings = json_decode( \IPS\Settings::i()->forums_popular_now, TRUE );
if ( $popularNowSettings['posts'] and $popularNowSettings['minutes'] )
if( !$popularNowSettings['minutes'] )
{
$popularNowInterval = new \DateInterval( 'PT' . $popularNowSettings['minutes'] . 'M' );
return ( $this->popular_time !== NULL and $this->popular_time > time() );
}
/* Don't use trending data for new topics */
elseif( \IPS\CIC AND
(
( $this->mapped('date') > \IPS\DateTime::create()->sub( new \DateInterval( 'PT1H' ) )->getTimestamp() OR $this->mapped('num_comments' ) <= 2 )
OR !\IPS\cloud\Application::trendingIsEnabled()
) )
{
return FALSE;
}
$comments = iterator_to_array( new \IPS\Patterns\ActiveRecordIterator(
\IPS\Db::i()->select( '*', 'forums_posts', array( 'queued IN(0,2) AND post_date >? AND topic_id=?', \IPS\DateTime::create()->sub( $popularNowInterval )->getTimestamp(), $this->tid ), 'post_date DESC', NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER ),
'IPS\\forums\\Topic\\Post'
) );
if ( \count( $comments ) >= $popularNowSettings['posts'] )
$popularNowInterval = new \DateInterval( 'PT' . $popularNowSettings['minutes'] . 'M' );
if( \IPS\CIC )
{
/* Don't use trending data for new topics or those with not many replies */
if ( ( $this->mapped( 'date' ) > \IPS\DateTime::create()->sub( new \DateInterval( 'PT1H' ) )->getTimestamp() or $this->mapped( 'num_comments' ) <= 2 ) )
{
$commentToBasePopularNowTimeOff = \array_slice( $comments, ( $popularNowSettings['posts'] - 1 ), 1 );
$commentToBasePopularNowTimeOff = array_pop( $commentToBasePopularNowTimeOff );
$this->popular_time = \IPS\DateTime::ts( $commentToBasePopularNowTimeOff->post_date )->add( $popularNowInterval )->getTimestamp();
$this->save();
$this->popular_time = null;
}
elseif ( $this->popular_time !== NULL )
else
{
$this->popular_time = NULL;
$this->save();
if ( static::$_hotTopics === null )
{
try
{
static::$_hotTopics = \IPS\Redis::i()->zRevRange( 'trending', 0, 15 );
}
catch ( \RedisException $e )
{
$this->popular_time = null;
}
}
if ( static::$_hotTopics !== null )
{
foreach ( static::$_hotTopics as $value )
{
$parts = explode( "__", $value );
if ( $parts[0] == 'IPS\forums\Topic' and $parts[1] == $this->tid )
{
$this->popular_time = \IPS\DateTime::ts( $this->mapped( 'last_comment' ) )->add( $popularNowInterval )->getTimestamp();
break;
}
}
}
}
$this->save();
}
else
{
if ( $popularNowSettings['posts'] )
{
$comments = iterator_to_array( new \IPS\Patterns\ActiveRecordIterator(
\IPS\Db::i()->select( '*', 'forums_posts', array( 'queued IN(0,2) AND post_date >? AND topic_id=?', \IPS\DateTime::create()->sub( $popularNowInterval )->getTimestamp(), $this->tid ), 'post_date DESC' ),
'IPS\\forums\\Topic\\Post'
) );
if ( \count( $comments ) >= $popularNowSettings['posts'] )
{
$commentToBasePopularNowTimeOff = \array_slice( $comments, ( $popularNowSettings['posts'] - 1 ), 1 );
$commentToBasePopularNowTimeOff = array_pop( $commentToBasePopularNowTimeOff );
$this->popular_time = \IPS\DateTime::ts( $commentToBasePopularNowTimeOff->post_date )->add( $popularNowInterval )->getTimestamp();
$this->save();
}
elseif ( $this->popular_time !== NULL )
{
$this->popular_time = NULL;
$this->save();
}
}
}
}
@@ -1044,7 +1115,7 @@ class _Topic extends \IPS\Content\Item implements
{
try
{
\IPS\forums\Topic\ArchivedPost::db()->update( 'forums_archive_posts', array( 'archive_forum_id' => $container->_id ), array( 'archive_topic_id=?', $this->tid ) );
ArchivedPost::db()->update( 'forums_archive_posts', array( 'archive_forum_id' => $container->_id ), array( 'archive_topic_id=?', $this->tid ) );
}
/* catch db exceptions if e.g. if the connection credentials didn't work or if the database doesn't exist anymore */
catch ( \IPS\Db\Exception $e ){}
@@ -1060,11 +1131,11 @@ class _Topic extends \IPS\Content\Item implements
{
parent::delete();
if( \in_array( $this->archive_status, array( static::ARCHIVE_DONE, static::ARCHIVE_WORKING, static::ARCHIVE_RESTORE ) ) )
if( \in_array( $this->topic_archive_status, array( static::ARCHIVE_DONE, static::ARCHIVE_WORKING, static::ARCHIVE_RESTORE ) ) )
{
try
{
\IPS\forums\Topic\ArchivedPost::db()->delete( 'forums_archive_posts', array( 'archive_topic_id=?', $this->tid ) );
ArchivedPost::db()->delete( 'forums_archive_posts', array( 'archive_topic_id=?', $this->tid ) );
}
/* catch db exceptions if e.g. if the connection credentials didn't work or if the database doesn't exist anymore */
catch ( \IPS\Db\Exception $e ){}
@@ -1162,9 +1233,6 @@ class _Topic extends \IPS\Content\Item implements
}
}
/* Update popular time */
$this->rebuildPopularTime();
return parent::mergeIn( $items, $keepLinks );
}
@@ -1966,6 +2034,7 @@ class _Topic extends \IPS\Content\Item implements
static $comments = array();
$idField = static::$databaseColumnId;
$_hash = md5( $this->$idField . json_encode( \func_get_args() ) );
$expectingSingleComment = $limit === 1;
if( !$bypassCache and isset( $comments[ $_hash ] ) )
{
@@ -2007,7 +2076,167 @@ class _Topic extends \IPS\Content\Item implements
try
{
$comments[ $_hash ] = $this->_comments( $class, $limit ?: static::getCommentsPerPage(), $offset, ( isset( $class::$databaseColumnMap[ $order ] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[ $order ] ) : $order ) . ' ' . $orderDirection, $member, $includeHiddenComments, $cutoff, $includeWarnings, $extraWhereClause, $includeDeleted );
$alreadyDone = false;
$originalLimit = $limit;
$originalOffset = $offset;
if ( $this->isArchived() and $limit === 1 and $order === 'date' )
{
$minMax = ( $orderDirection === 'asc' ) ? 'MIN' : 'MAX';
/* Do some mojo to get the lowest comment and highest comment for archived topics */
$row = ArchivedPost::db()->select( "{$minMax}( CONCAT( archive_content_date, '.', archive_id) ) as pid", 'forums_archive_posts', array( [ 'archive_topic_id=?', $this->tid, ArchivedPost::db()->in( 'archive_queued', [0,2] ) ] ) )->first();
$pid = (int) explode( '.', $row )[1];
if( \is_array( $extraWhereClause ) or is_null( $extraWhereClause ) )
{
$extraWhereClause[] = [ 'archive_id=?', $pid ];
}
else if ( is_string( $extraWhereClause ) )
{
$extraWhereClause = [
[ $extraWhereClause ],
[ 'archive_id=?', $pid ]
];
}
$data = $this->_comments( $class, 1, 0, ( isset( $class::$databaseColumnMap[$order] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[$order] ) : $order ) . ' ' . $orderDirection, $member, $includeHiddenComments, $cutoff, $canViewWarn, $extraWhereClause, $includeDeleted );
$comments[ $_hash ] = \is_array( $data ) ? array_reverse( $data, true ) : $data;
$alreadyDone = true;
}
else if ( $this->mapped('num_comments') > \IPS\LARGE_TOPIC_REPLIES and $order === 'date' and $orderDirection === 'asc' and ( $originalLimit === null or $originalLimit === static::getCommentsPerPage() ) )
{
/* This is a large topic, if we're requesting the last 50% of pages, we will reverse sort to reduce the offset */
$page = ( Request::i()->page ? \intval( Request::i()->page ) : 1 );
$pagePercent = ceil( ( $page / $this->commentPageCount() ) * 100 );
$commentCount = $this->commentCount();
if ( $pagePercent > 50 )
{
$postsOnLastPage = $commentCount % static::getCommentsPerPage();
$pageFromEnd = $this->commentPageCount() - $page;
if( $pageFromEnd > 1 )
{
$offset = ( $pageFromEnd - 1 ) * static::getCommentsPerPage();
/* We need to adjust the offset to be from the end of the topic keeping in mind the last page may have fewer posts than a full 25 */
$offset += $postsOnLastPage;
}
elseif( $pageFromEnd == 1 )
{
/* $postsOnLastPage might be 0 if we have exactly the amount of comments on the last page.
So if we are one page from the end AND $postsOnLastPage is 0, then just use the number of comments per page. */
$offset = $postsOnLastPage ?: static::getCommentsPerPage();
}
else
{
$offset = 0;
}
/* Are we on the last page, if so the limit should be on the number of posts on the last page */
if ( $page == $this->commentPageCount() )
{
$limit = $postsOnLastPage;
}
$data = $this->_comments( $class, $limit ?: $this->getCommentsPerPage(), $offset, ( isset( $class::$databaseColumnMap[$order] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[$order] ) : $order ) . ' desc', $member, $includeHiddenComments, $cutoff, $canViewWarn, $extraWhereClause, $includeDeleted );
$comments[ $_hash ] = \is_array( $data ) ? array_reverse( $data, true ) : $data;
/* When we set an implicit limit of 1, we just want a Post object but if the sql returns just 1 item but we could take more, then we want an array
yeah this is bad code but it's been here for years so... */
if ( ! \is_array( $data ) and ! $expectingSingleComment )
{
$comments[ $_hash ] = [ $data->pid => $data ];
}
$alreadyDone = true;
}
else if ( $this->isArchived() )
{
/* When viewing the first handful of pages in archived topics with hundreds of thousands of replies, it's more efficient to get the ID first */
$archiveIds = [];
if ( $offset === NULL )
{
$_pageValue = ( \IPS\Request::i()->page ? \intval( \IPS\Request::i()->page ) : 1 );
if( $_pageValue < 1 )
{
$_pageValue = 1;
}
$offset = ( $_pageValue - 1 ) * static::getCommentsPerPage();
}
foreach( ArchivedPost::db()->select( "archive_id", 'forums_archive_posts', array( [ 'archive_topic_id=?', $this->tid, ArchivedPost::db()->in( 'archive_queued', [0,2] ) ] ), 'archive_id ' . $orderDirection, [ $offset, static::getCommentsPerPage() ] ) as $row )
{
$archiveIds[] = $row;
}
if( \is_array( $extraWhereClause ) or is_null( $extraWhereClause ) )
{
$extraWhereClause[] = [ ArchivedPost::db()->in( 'archive_id', $archiveIds ) ];
}
else if ( is_string( $extraWhereClause ) )
{
$extraWhereClause = [
[ $extraWhereClause ],
[ ArchivedPost::db()->in( 'archive_id', $archiveIds ) ]
];
}
$comments[ $_hash ] = $this->_comments( $class, static::getCommentsPerPage(), 0, ( isset( $class::$databaseColumnMap[$order] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[$order] ) : $order ) . ' ' . $orderDirection, $member, $includeHiddenComments, $cutoff, $includeWarnings, $extraWhereClause, $includeDeleted );
$alreadyDone = true;
}
}
if ( ! $alreadyDone )
{
/* Optimise getting just the first or last post */
if ( $this->mapped('num_comments') > \IPS\LARGE_TOPIC_REPLIES and ! $cutoff and ! $this->isArchived() and $limit === 1 and $order === 'date' )
{
$minMax = ( $orderDirection === 'asc' ) ? 'MIN' : 'MAX';
$pidWhere = [
[ 'topic_id=?', $this->tid ],
];
if ( \in_array( 'IPS\Content\Hideable', class_implements( $class ) ) )
{
if ( $includeHiddenComments === NULL )
{
$includeHiddenComments = $this->canViewHiddenComments();
}
if ( ! $includeHiddenComments )
{
/* @var \IPS\Content\Comment $class */
$pidWhere[] = [ $class::db()->in( 'queued', [0,2] ) ];
}
}
try
{
$row = $class::db()->select( "{$minMax}( CONCAT( post_date, '.', pid) ) as pid", 'forums_posts', $pidWhere, flags: ( $bypassCache ) ? \IPS\Db::SELECT_FROM_WRITE_SERVER : 0 )->first();
$pid = (int) explode( '.', $row )[1];
if( \is_array( $extraWhereClause ) or is_null( $extraWhereClause ) )
{
$extraWhereClause[] = [ 'pid=?', $pid ];
}
else if ( is_string( $extraWhereClause ) )
{
$extraWhereClause = [
[ $extraWhereClause ],
[ 'pid=?', $pid ]
];
}
}
catch( \UnderflowException ) { }
}
$comments[ $_hash ] = $this->_comments( $class, $originalLimit ?: static::getCommentsPerPage(), $originalOffset, ( isset( $class::$databaseColumnMap[$order] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[$order] ) : $order ) . ' ' . $orderDirection, $member, $includeHiddenComments, $cutoff, $includeWarnings, $extraWhereClause, $includeDeleted );
}
}
catch( \IPS\Db\Exception $e )
{
@@ -2022,7 +2251,7 @@ class _Topic extends \IPS\Content\Item implements
$post->post .= "<p>" . \IPS\Member::loggedIn()->language()->addToStack('archived_topic_missing_posts_admin') . "</p><p><strong>{$e->getMessage()}<br><textarea>" . var_export( $e, TRUE ) . '</textarea></p>';
}
$comments[ $_hash ] = array( $post );
$comments[ $_hash ] = $expectingSingleComment ? $post : array( $post );
}
/* Restore comment class now */
@@ -2346,20 +2575,19 @@ class _Topic extends \IPS\Content\Item implements
*
* @return string|null
*/
/**
* We need to force an index so that super long topics load when fetching IDs before running the main query
*
* @return string
*/
protected function forceIndexForPaginatedIds()
{
if ( ! $this->isArchived() )
{
return 'first_post';
}
return NULL;
}
protected function forceIndexForPaginatedIds()
{
if ( ! $this->isArchived() )
{
return 'first_post';
}
else
{
return 'archive_topic_id';
}
return null;
}
/**
* WHERE clause for getting items for ACP overview statistics