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

+99 -45
View File
@@ -11,6 +11,10 @@
namespace IPS\forums;
use IPS\Content\Comment;
use IPS\Db;
use IPS\Settings;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -27,6 +31,7 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
use \IPS\Node\Colorize;
use \IPS\Node\Statistics;
use \IPS\Content\ViewUpdates;
use \IPS\Node\DelayedCount;
/**
* @brief [ActiveRecord] Multiton Store
@@ -329,55 +334,40 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
* Last Poster ID Column
*/
protected static $lastPosterIdColumn = 'last_poster_id';
/**
* Set last comment
*
* @param \IPS\Content\Comment $comment The latest comment or NULL to work it out
* @return void
* @param Comment|null $comment The latest comment or NULL to work it out
* @param Item|null $updatedItem We sometimes run setLastComment() when an item has been edited, if so, that item will be here
* @return void
*/
public function setLastComment( \IPS\Content\Comment $comment=NULL )
public function _setLastComment( \IPS\Content\Comment $comment=NULL, \IPS\Content\Item $updatedItem=NULL )
{
if ( $comment === NULL )
/* If we were given an item, but it's older than the current last post, clear it */
if( $updatedItem !== NULL AND $updatedItem->mapped('last_comment') < $this->last_post )
{
$updatedItem = null;
}
/* If we still have no item, load the latest topic */
if( $updatedItem === null )
{
try
{
/*
/*
* We prefer fetching post, joining topic, etc. but that is not efficient and causes a temp table and filesort against posts table so we'll lean on the cached last_post value for the topic
* We also need to fetch from the write server in the event that something has just been deleted and the comment hasn't been passed.
* We also cannot look for future entries since this code is used in older upgrades, but we can look for the date instead.
*/
$select = \IPS\Db::i()->select( '*', 'forums_topics', array( "forums_topics.forum_id=? AND forums_topics.approved=1 AND forums_topics.state != ? AND forums_topics.last_post<=? AND forums_topics.publish_date <=?", $this->id, 'link', time(), time() ), 'forums_topics.last_post DESC', 1, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$select = \IPS\Db::i()->select( '*', 'forums_topics', array( "forums_topics.forum_id=? AND forums_topics.approved=1 AND forums_topics.state != ? AND forums_topics.last_post<=? AND forums_topics.publish_date <=?", $this->id, 'link', time(), time() ), 'forums_topics.last_post DESC', 1 )->first();
$topic = \IPS\forums\Topic::constructFromData( $select );
if ( $topic->last_poster_id and ! $topic->last_poster_name )
{
$member = \IPS\Member::load( $topic->last_poster_id );
if ( $member->member_id )
{
$topic->last_poster_name = $member->name;
}
else
{
$topic->last_poster_name = '';
$topic->last_poster_id = 0;
}
}
$this->last_post = $topic->last_post;
$this->last_poster_id = (int) $topic->last_poster_id;
$this->last_poster_name = $topic->last_poster_name;
$this->seo_last_name = \IPS\Http\Url\Friendly::seoTitle( $this->last_poster_name );
$this->last_title = $topic->title;
$this->seo_last_title = \IPS\Http\Url\Friendly::seoTitle( $this->last_title );
$this->last_id = $topic->tid;
$this->last_poster_anon = $topic->last_poster_anon;
return;
$updatedItem = \IPS\forums\Topic::constructFromData( $select );
}
catch ( \UnderflowException $e )
catch( \UnderflowException $e )
{
/* At this point we have nothing, so clear the last post data */
$this->last_post = NULL;
$this->last_poster_id = 0;
$this->last_poster_name = '';
@@ -387,15 +377,29 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
return;
}
}
$this->last_post = $comment->mapped('date');
$this->last_poster_id = (int) $comment->author()->member_id;
$this->last_poster_name = $comment->author()->member_id ? $comment->author()->name : $comment->mapped('author_name');
if ( $updatedItem->last_poster_id and ! $updatedItem->last_poster_name )
{
$member = \IPS\Member::load( $updatedItem->last_poster_id );
if ( $member->member_id )
{
$updatedItem->last_poster_name = $member->name;
}
else
{
$updatedItem->last_poster_name = '';
$updatedItem->last_poster_id = 0;
}
}
$this->last_post = $updatedItem->last_post;
$this->last_poster_id = (int) $updatedItem->last_poster_id;
$this->last_poster_name = $updatedItem->last_poster_name;
$this->seo_last_name = \IPS\Http\Url\Friendly::seoTitle( $this->last_poster_name );
$this->last_title = $comment->item()->title;
$this->last_title = $updatedItem->title;
$this->seo_last_title = \IPS\Http\Url\Friendly::seoTitle( $this->last_title );
$this->last_id = $comment->item()->tid;
$this->last_poster_anon = $comment->isAnonymous();
$this->last_id = $updatedItem->tid;
$this->last_poster_anon = $updatedItem->last_poster_anon;
}
/**
@@ -1064,8 +1068,9 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'forum_qa_rate_answers', $this->id ? ( ( $this->qa_rate_answers == '*' or $this->qa_rate_answers === NULL ) ? '*' : explode( ',', $this->qa_rate_answers ) ) : '*', FALSE, array( 'options' => $groupsNoGuests, 'unlimited' => '*', 'multiple' => TRUE, 'impliedUnlimited' => TRUE ), NULL, NULL, NULL, 'forum_qa_rate_answers' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'forum_forum_allow_rating', $this->id ? $this->forum_allow_rating : FALSE, FALSE, array(), NULL, NULL, NULL, 'forum_allow_rating' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'forum_disable_sharelinks', $this->id ? !$this->disable_sharelinks : TRUE, FALSE, array(), NULL, NULL, NULL, 'forum_disable_sharelinks' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'bw_enable_answers_moderator', $this->id ? $this->forums_bitoptions['bw_enable_answers_moderator'] : FALSE, FALSE, array( 'togglesOn' => array( 'bw_enable_answers__member' ) ), NULL, NULL, NULL, 'forum_best_answer' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'bw_enable_answers_moderator', $this->id ? $this->forums_bitoptions['bw_enable_answers_moderator'] : FALSE, FALSE, array( 'togglesOn' => array( 'bw_enable_answers__member', 'solved_stats_from_cutoff' ) ), NULL, NULL, NULL, 'forum_best_answer' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'bw_enable_answers__member', $this->id ? $this->forums_bitoptions['bw_enable_answers_member'] : FALSE, FALSE, array(), NULL, NULL, NULL, 'bw_enable_answers__member' ) );
$form->add( new \IPS\Helpers\Form\Interval( 'solved_stats_from_cutoff', $this->id ? $this->solved_stats_from_cutoff : 0, FALSE, array( 'valueAs' => \IPS\Helpers\Form\Interval::DAYS, 'unlimited' => 0, 'unlimitedLang' => 'alltime' ), NULL, NULL, NULL, 'solved_stats_from_cutoff' ) );
$form->addTab( 'posting_settings' );
$form->addHeader('posts');
@@ -1220,6 +1225,20 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
}
}
if ( isset( $values['solved_stats_from_cutoff'] ) )
{
/* If answers is disabled, set this to no. */
if ( !$values['forums_bitoptions']['bw_enable_answers_moderator'] )
{
$values['solved_stats_from_cutoff'] = 0;
}
else
{
/* Otherwise we need to reset the current time so it rebuilds */
$values['solved_stats_from'] = 0;
}
}
/* Remove forum_ prefix */
$_values = $values;
$values = array();
@@ -1633,7 +1652,7 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
/* If this is not a true mass move of contents of one container to another, then let parent handle it normally */
if( isset( $data['additional'] ) AND
( isset( $data['additional']['author'] ) OR ( isset( $data['additional']['no_comments'] ) AND $data['additional']['no_comments'] > 0 ) OR
( isset( $data['additional']['num_comments'] ) AND $data['additional']['num_comments'] > 0 ) OR isset( $data['additional']['state'] ) OR
( isset( $data['additional']['num_comments'] ) AND $data['additional']['num_comments'] >= 0 ) OR isset( $data['additional']['state'] ) OR
( isset( $data['additional']['pinned'] ) AND $data['additional']['pinned'] === TRUE ) OR ( isset( $data['additional']['featured'] ) AND $data['additional']['featured'] === TRUE ) OR ( isset( $data['additional']['last_post'] ) AND $data['additional']['last_post'] > 0 ) OR ( isset( $data['additional']['date'] ) AND $data['additional']['date'] > 0 ) )
)
{
@@ -1913,8 +1932,15 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
{
try
{
$where = [['forum_id=?', $this->_id]];
if ( $this->solved_stats_from_cutoff )
{
$cutoff = \IPS\DateTime::create()->sub( new \DateInterval( 'P' . $this->solved_stats_from_cutoff . 'D' ) )->getTimestamp();
$where[] = ['start_date>?', $cutoff];
}
$post = \IPS\Db::i()->select( '*', 'forums_posts', [
'pid IN (?)', \IPS\Db::i()->select( 'topic_answered_pid', 'forums_topics', ['forum_id=?', $this->_id] )
'pid IN (?)', \IPS\Db::i()->select( 'topic_answered_pid', 'forums_topics', $where )
], 'post_date ASC', 1 )->first();
try
@@ -2266,8 +2292,36 @@ class _Forum extends \IPS\Node\Model implements \IPS\Node\Permissions
break;
case 'comment':
return (bool) ( \In_array( $this->preview_posts, array( 1, 3 ) ) );
return (bool) ( \in_array( $this->preview_posts, array( 1, 3 ) ) );
break;
}
}
}
/**
* Count all comments, items, etc
*
* @return void
*/
protected function recount()
{
$this->_items = (int) Db::i()->select( 'count(*)', 'forums_topics', [ 'forum_id=? and approved=? and state != ?', $this->_id, 1, 'link' ] )->first();
$this->_unapprovedItems = (int) Db::i()->select( 'count(*)', 'forums_topics', [ 'forum_id=? and approved = ? and state !=?', $this->_id, 0, 'link' ] )->first();
$this->_comments = (int) Db::i()->select( 'count(*)', 'forums_posts', [ 'queued=? and forum_id=? and approved=? and state != ?', 0, $this->_id, 1, 'link' ] )
->join( 'forums_topics', 'forums_posts.topic_id=forums_topics.tid' )
->first();
$this->_unapprovedComments = (int) Db::i()->select( 'count(*)', 'forums_posts', [ 'queued =? and forum_id=? and approved=? and state != ?', 1, $this->_id, 1, 'link' ] )
->join( 'forums_topics', 'forums_posts.topic_id=forums_topics.tid' )
->first();
/* Count archived posts */
if( Settings::i()->archive_on )
{
/* Here we use the sum from the topics table because once the topic is archived, these won't change */
$archiveStats = Db::i()->select( 'IFNULL( sum(posts), 0 ) as approved, IFNULL( sum(topic_queuedposts), 0 ) as unapproved', 'forums_topics', [ 'forum_id=? and approved=? and state != ? and topic_archive_status=?', $this->_id, 1, 'link', Topic::ARCHIVE_DONE ] )->first();
$this->_comments += $archiveStats['approved'];
$this->_unapprovedComments += $archiveStats['unapproved'];
}
$this->save();
}
}
@@ -129,13 +129,13 @@ class _SavedAction extends \IPS\Node\Model
{
$form->addHeader( 'settings' );
$form->add( new \IPS\Helpers\Form\Translatable( 'mm_title', NULL, TRUE, array( 'app' => 'forums', 'key' => ( $this->mm_id ? "forums_mmod_{$this->mm_id}" : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'mm_forums', ( $this->mm_id and $this->mm_forums != '*' ) ? $this->mm_forums : 0, FALSE, array( 'class' => 'IPS\forums\Forum', 'multiple' => TRUE, 'zeroVal' => 'all' ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'mm_forums', ( $this->mm_id and $this->mm_forums != '*' ) ? $this->mm_forums : 0, FALSE, array( 'class' => 'IPS\forums\Forum', 'clubs' => true, 'multiple' => TRUE, 'zeroVal' => 'all' ) ) );
$form->addHeader( 'topic_properties' );
$form->add( new \IPS\Helpers\Form\Radio( 'topic_state', $this->mm_id ? $this->topic_state : 'leave', FALSE, array( 'options' => array( 'leave' => 'mm_leave', 'open' => 'unlock', 'close' => 'lock' ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'topic_pin', $this->mm_id ? $this->topic_pin : 'leave', FALSE, array( 'options' => array( 'leave' => 'mm_leave', 'pin' => 'pin', 'unpin' => 'unpin' ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'topic_approve', $this->mm_id ? $this->topic_approve : 0, FALSE, array( 'options' => array( 0 => 'mm_leave', 1 => 'unhide', 2 => 'hide' ) ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'topic_move', ( $this->mm_id and $this->topic_move != -1 ) ? $this->topic_move : 0, FALSE, array( 'class' => 'IPS\forums\Forum', 'zeroVal' => 'topic_move_none', 'zeroValTogglesOff' => array( 'topic_move_link' ), 'permissionCheck' => function ( $forum )
$form->add( new \IPS\Helpers\Form\Node( 'topic_move', ( $this->mm_id and $this->topic_move != -1 ) ? $this->topic_move : 0, FALSE, array( 'class' => 'IPS\forums\Forum', 'clubs' => true, 'zeroVal' => 'topic_move_none', 'zeroValTogglesOff' => array( 'topic_move_link' ), 'permissionCheck' => function ( $forum )
{
return $forum->sub_can_post and !$forum->redirect_url;
} ) ) );
@@ -120,58 +120,6 @@ class _Post extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \I
*/
public static $tabParameter = NULL; /* We do not want ?tab=comments appended to links in from findComment links */
/**
* Create comment
*
* @param \IPS\Content\Item $item The content item just created
* @param string $comment The comment
* @param bool $first Is the first comment?
* @param string $guestName If author is a guest, the name to use
* @param bool|NULL $incrementPostCount Increment post count? If NULL, will use static::incrementPostCount()
* @param \IPS\Member|NULL $member The author of this comment. If NULL, uses currently logged in member.
* @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, $anonymous=NULL )
{
$comment = parent::create( $item, $comment, $first, $guestName, $incrementPostCount, $member, $time, $ipAddress, $hiddenStatus, $anonymous );
if ( !$comment->hidden() )
{
$item->rebuildPopularTime();
}
return $comment;
}
/**
* Syncing to run when hiding
*
* @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @return void
*/
public function onHide( $member )
{
parent::onHide( $member );
$this->item()->rebuildPopularTime();
}
/**
* Syncing to run when unhiding
*
* @param bool $approving If true, is being approved for the first time
* @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @return void
*/
public function onUnhide( $approving, $member )
{
parent::onUnhide( $approving, $member );
$this->item()->rebuildPopularTime();
}
/**
* Should posting this increment the poster's post count?
*
@@ -353,32 +301,6 @@ class _Post extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \I
parent::save();
$this->item()->clearVotes();
}
/**
* Delete Post
*
* @return void
*/
public function delete()
{
/* It is possible to delete a post that is orphaned, so let's try to protect against that */
try
{
$item = $this->item();
}
catch( \OutOfRangeException $e )
{
$item = NULL;
}
parent::delete();
/* Deleting a post may make the item no longer popular */
if( $item !== NULL )
{
$item->rebuildPopularTime();
}
}
/**
* Get the snippet (stripped of HTML)
+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