Version 4.7.23
This commit is contained in:
1 parent
7124a02564
commit
25ddeb65d6
1791 files changed
+76990
-44452
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user