Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -74,9 +74,8 @@ use OutOfRangeException;
|
||||
use RuntimeException;
|
||||
use SplObserver;
|
||||
use SplSubject;
|
||||
use UnderflowException;
|
||||
use function array_reverse;
|
||||
use function array_slice;
|
||||
use function class_implements;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function explode;
|
||||
@@ -88,7 +87,9 @@ use function is_array;
|
||||
use function is_null;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use const IPS\LARGE_TOPIC_LOCK;
|
||||
use const IPS\LARGE_TOPIC_REPLIES;
|
||||
use const IPS\LARGE_TOPIC_WARNING;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -129,6 +130,7 @@ class Topic extends Item implements Embeddable,
|
||||
ReadMarkers::markRead as public _markRead;
|
||||
Reactable::reactionClass as public _reactionClass;
|
||||
Solvable::toggleSolveComment as protected _toggleSolveComment;
|
||||
Lockable::canUnlock as protected _canUnlock;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +221,8 @@ class Topic extends Item implements Embeddable,
|
||||
'is_future_entry' => 'is_future_entry',
|
||||
'future_date' => 'publish_date',
|
||||
'last_vote' => 'last_vote',
|
||||
'assigned' => 'assignment_id'
|
||||
'assigned' => 'assignment_id',
|
||||
'num_helpful' => 'helpful_count'
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -919,6 +922,67 @@ class Topic extends Item implements Embeddable,
|
||||
{
|
||||
return static::_canCreatePoll( $member, $container ) and ( $container === NULL or $container->allow_poll );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can unlock?
|
||||
*
|
||||
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canUnlock( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Intentionally doing an override instead of adding to actionEnabled()
|
||||
because we want to bypass any Permissions extension here. Never, ever, ever let
|
||||
large topics be unlocked! */
|
||||
if( $this->isLargeTopic() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->_canUnlock( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can Merge?
|
||||
*
|
||||
* @param Member|null $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canMerge( Member|null $member=NULL ): bool
|
||||
{
|
||||
/* Same idea here for merging. We cannot merge into a large topic,
|
||||
not even by extension override */
|
||||
if( $this->isLargeTopic() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* And just to be safe, don't allow it if we are even approaching the threshold */
|
||||
if( $this->postsToClose() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canMerge( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can comment?
|
||||
*
|
||||
* @param Member|NULL $member The member (NULL for currently logged in member)
|
||||
* @param bool $considerPostBeforeRegistering If TRUE, and $member is a guest, will return TRUE if "Post Before Registering" feature is enabled
|
||||
* @return bool
|
||||
*/
|
||||
public function canComment( ?Member $member=NULL, bool $considerPostBeforeRegistering = TRUE ): bool
|
||||
{
|
||||
/* Don't care who you are, NO COMMENTS FOR YOU */
|
||||
if( $this->isLargeTopic() and $this->locked() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canComment( $member, $considerPostBeforeRegistering );
|
||||
}
|
||||
|
||||
/**
|
||||
* SplObserver notification that poll has been voted on
|
||||
@@ -971,7 +1035,8 @@ class Topic extends Item implements Embeddable,
|
||||
}
|
||||
}
|
||||
|
||||
if ( Bridge::i()->featureIsEnabled( 'realtime' ) )
|
||||
/* Do we need a hash for who's viewing and/or who's typing? */
|
||||
if ( Bridge::i()->featureIsEnabled( 'live_full' ) )
|
||||
{
|
||||
$hash = [
|
||||
'app' => 'forums',
|
||||
@@ -1009,7 +1074,6 @@ class Topic extends Item implements Embeddable,
|
||||
}
|
||||
|
||||
parent::move( $container, $keepLink );
|
||||
Db::i()->update( 'forums_question_ratings', array( 'forum' => $container->_id ), array( 'topic=?', $this->tid ) );
|
||||
|
||||
/* While you can't normally move archived topics, when you mass manage content from the AdminCP by using the menu next to the forum,
|
||||
this still allows topics to be moved. If we don't update the archive forum database the forum counts will be off */
|
||||
@@ -1043,11 +1107,11 @@ class Topic extends Item implements Embeddable,
|
||||
catch ( Exception $e ){}
|
||||
}
|
||||
|
||||
Db::i()->delete( 'forums_question_ratings', array( 'topic=?', $this->tid ) );
|
||||
Db::i()->delete( 'forums_answer_ratings', array( 'topic=?', $this->tid ) );
|
||||
|
||||
/* Delete any moved topic links that point to this topic - moved_on>? is for query optimisation purposes */
|
||||
Db::i()->delete( 'forums_topics', array( "moved_on>? AND moved_to LIKE CONCAT( ?, '%' ) AND state=?", 0, $this->tid . '&', 'link' ) );
|
||||
|
||||
/* Delete any parent/child relationsihps */
|
||||
Db::i()->delete( 'forums_topics_children', [ 'topic_id=? or parent_topic=?', $this->tid, $this->tid ] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1299,6 +1363,12 @@ class Topic extends Item implements Embeddable,
|
||||
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Guests can never do this */
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If we asked this question, we can set the best answer */
|
||||
if ( $member === $this->author() and $this->container()->forums_bitoptions['bw_solved_set_by_member'] )
|
||||
{
|
||||
@@ -1327,31 +1397,6 @@ class Topic extends Item implements Embeddable,
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Answer Votes
|
||||
*/
|
||||
protected array $answerVotes = array();
|
||||
|
||||
/**
|
||||
* Answer Votes
|
||||
*
|
||||
* @param Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function answerVotes( Member $member ) : array
|
||||
{
|
||||
if ( !isset( $this->answerVotes[ $member->member_id ] ) )
|
||||
{
|
||||
$this->answerVotes[ $member->member_id ] = iterator_to_array(
|
||||
Db::i()->select( 'post,rating', 'forums_answer_ratings', array( 'topic=? AND `member`=?', $this->tid, $member->member_id ) )
|
||||
->setKeyField( 'post' )
|
||||
->setValueField( 'rating' )
|
||||
);
|
||||
}
|
||||
|
||||
return $this->answerVotes[ $member->member_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Best Answer
|
||||
*
|
||||
@@ -1370,74 +1415,6 @@ class Topic extends Item implements Embeddable,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the user rate answers?
|
||||
*
|
||||
* @param int $rating 1 for positive, -1 for negative, 0 for either
|
||||
* @param Member|NULL $member The member (NULL for currently logged in member)
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function canVote( int $rating=0, ?Member $member=NULL ) : bool
|
||||
{
|
||||
/* Is $rating valid */
|
||||
if ( !in_array( $rating, array( -1, 0, 1 ) ) )
|
||||
{
|
||||
throw new InvalidArgumentException;
|
||||
}
|
||||
|
||||
/* Guests can't vote */
|
||||
$member = $member ?: Member::loggedIn();
|
||||
if ( !$member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Can't vote your own answers */
|
||||
if ( $member === $this->author() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Votes
|
||||
*/
|
||||
protected ?array $votes = NULL;
|
||||
|
||||
/**
|
||||
* Votes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function votes() : array
|
||||
{
|
||||
if ( $this->votes === NULL )
|
||||
{
|
||||
$this->votes = iterator_to_array(
|
||||
Db::i()->select( '`member`,rating', 'forums_question_ratings', array( 'topic=?', $this->tid ) )
|
||||
->setKeyField( 'member' )
|
||||
->setValueField( 'rating' )
|
||||
);
|
||||
}
|
||||
|
||||
return $this->votes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Votes Cache
|
||||
*
|
||||
* @return void
|
||||
* @note This is necessary so that when voting on a question or answer, the cached votes ($votes and $answerVotes) are reloaded properly.
|
||||
*/
|
||||
public function clearVotes() : void
|
||||
{
|
||||
$this->votes = NULL;
|
||||
$this->answerVotes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Container has assignable enabled
|
||||
*
|
||||
@@ -1447,17 +1424,6 @@ class Topic extends Item implements Embeddable,
|
||||
{
|
||||
return (bool) $this->container()->forums_bitoptions['bw_enable_assignments'];
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Save
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
parent::save();
|
||||
$this->clearVotes();
|
||||
}
|
||||
|
||||
/* !Sitemap */
|
||||
|
||||
@@ -1698,22 +1664,33 @@ class Topic extends Item implements Embeddable,
|
||||
$pagePercent = ceil( ( $page / $this->commentPageCount() ) * 100 );
|
||||
$commentCount = $this->commentCount();
|
||||
|
||||
if ( $pagePercent > 50 )
|
||||
if ( $pagePercent >= 50 )
|
||||
{
|
||||
$postsOnLastPage = $commentCount % static::getCommentsPerPage();
|
||||
$pageFromEnd = $this->commentPageCount() - $page;
|
||||
$offset = $pageFromEnd > 1 ? ( $pageFromEnd - 1 ) * static::getCommentsPerPage() : 0;
|
||||
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 ?: static::getCommentsPerPage();
|
||||
}
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* 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;
|
||||
}
|
||||
|
||||
$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;
|
||||
@@ -1793,7 +1770,7 @@ class Topic extends Item implements Embeddable,
|
||||
|
||||
try
|
||||
{
|
||||
$row = $class::db()->select( "{$minMax}( CONCAT( post_date, '.', pid) ) as pid", 'forums_posts', $pidWhere )->first();
|
||||
$row = $class::db()->select( "{$minMax}( CONCAT( post_date, '.', pid) ) as pid", 'forums_posts', $pidWhere, flags: ( $bypassCache ) ? Db::SELECT_FROM_WRITE_SERVER : 0 )->first();
|
||||
$pid = (int) explode( '.', $row )[1];
|
||||
|
||||
if( \is_array( $extraWhereClause ) or is_null( $extraWhereClause ) )
|
||||
@@ -1808,7 +1785,7 @@ class Topic extends Item implements Embeddable,
|
||||
];
|
||||
}
|
||||
}
|
||||
catch( \UnderflowException ) { }
|
||||
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 );
|
||||
@@ -1955,6 +1932,8 @@ class Topic extends Item implements Embeddable,
|
||||
* @apiresponse bool featured Topic is featured
|
||||
* @apiresponse bool archived Topic is archived
|
||||
* @apiresponse \IPS\Poll poll Poll data, if there is one
|
||||
* @apiresponse \IPS\core\Assignments\Assignment assignment Assignment data
|
||||
* @apiresponse int parentTopic If this was split from a larger topic, returns the ID of the parent
|
||||
* @apiresponse string url URL
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
@@ -1981,7 +1960,9 @@ class Topic extends Item implements Embeddable,
|
||||
'poll' => $this->poll_state ? Poll::load( $this->poll_state )->apiOutput( $authorizedMember ) : null,
|
||||
'url' => (string) $this->url(),
|
||||
'is_future_entry' => $this->is_future_entry,
|
||||
'publish_date' => $this->publish_date ? DateTime::ts( $this->publish_date )->rfc3339() : NULL
|
||||
'publish_date' => $this->publish_date ? DateTime::ts( $this->publish_date )->rfc3339() : NULL,
|
||||
'assignment' => $this->assignment ? $this->assignment->apiOutput( $authorizedMember, false ) : NULL,
|
||||
'parentTopic' => $this->parent()?->tid ?? null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2282,4 +2263,143 @@ class Topic extends Item implements Embeddable,
|
||||
{
|
||||
return (bool) Settings::i()->forum_post_use_minimal_editor;
|
||||
}
|
||||
|
||||
// <editor-fold desc="large topics">
|
||||
|
||||
/**
|
||||
* Is this considered a "large" topic?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLargeTopic() : bool
|
||||
{
|
||||
/* Intentionally not using commentCount because it runs a query */
|
||||
return $this->mapped( 'num_comments' ) >= LARGE_TOPIC_LOCK;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many more posts can we make before this will close?
|
||||
* Return null if there are too many for us to care.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function postsToClose() : ?int
|
||||
{
|
||||
$postsAllowed = LARGE_TOPIC_LOCK - $this->mapped( 'num_comments' );
|
||||
return ( $postsAllowed < LARGE_TOPIC_WARNING ) ? $postsAllowed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static array $parentChildTopics = [];
|
||||
|
||||
/**
|
||||
* Returns the parent topic, if one exists
|
||||
*
|
||||
* @return Topic|null
|
||||
*/
|
||||
public function parent() : ?Topic
|
||||
{
|
||||
if( $data = $this->_topicRelationships() )
|
||||
{
|
||||
if( $data['parent_topic'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
return Topic::load( $data['parent_topic'] );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What number is this in the sequence?
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function sequence() : int
|
||||
{
|
||||
if( $data = $this->_topicRelationships() )
|
||||
{
|
||||
return $data['sequence'];
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all child topics, in sequence
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function children() : ?array
|
||||
{
|
||||
if( $data = $this->_topicRelationships() )
|
||||
{
|
||||
if( isset( $data['children'] ) )
|
||||
{
|
||||
$childTopics = iterator_to_array(
|
||||
new ActiveRecordIterator(
|
||||
Db::i()->select( '*', 'forums_topics', Db::i()->in( 'tid', $data['children'] ) )->setKeyField( 'tid' ),
|
||||
Topic::class
|
||||
)
|
||||
);
|
||||
|
||||
$return = [];
|
||||
|
||||
/* Make sure we return them in sequence */
|
||||
foreach( $data['children'] as $childId )
|
||||
{
|
||||
$return[] = $childTopics[ $childId ];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Local cache for parent/child topics
|
||||
* @return array|null
|
||||
*/
|
||||
protected function _topicRelationships() : ?array
|
||||
{
|
||||
if( !array_key_exists( $this->tid, static::$parentChildTopics ) )
|
||||
{
|
||||
/* Default to null so that we don't load this again */
|
||||
static::$parentChildTopics[ $this->tid ] = null;
|
||||
|
||||
/* Load parent and child data in one shot */
|
||||
foreach( Db::i()->select( '*', 'forums_topics_children', [ 'topic_id=? or parent_topic=?', $this->tid, $this->tid ], 'sequence' ) as $row )
|
||||
{
|
||||
static::$parentChildTopics[ $row['topic_id'] ] = $row;
|
||||
if( $row['topic_id'] != $row['parent_topic'] )
|
||||
{
|
||||
if( !isset( static::$parentChildTopics[ $row['parent_topic'] ]['children'] ) )
|
||||
{
|
||||
static::$parentChildTopics[ $row['parent_topic'] ]['children'] = [];
|
||||
}
|
||||
static::$parentChildTopics[ $row['parent_topic'] ]['children'][] = $row['topic_id'];
|
||||
}
|
||||
}
|
||||
|
||||
/* If this is a child topic, load up the siblings, we may not have picked it up in the last query */
|
||||
if( static::$parentChildTopics[ $this->tid ] !== null and $parentId = static::$parentChildTopics[ $this->tid ]['parent_topic'] )
|
||||
{
|
||||
static::$parentChildTopics[ $parentId ]['children'] = iterator_to_array(
|
||||
Db::i()->select( 'topic_id', 'forums_topics_children', [ 'parent_topic=?', $parentId ] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return static::$parentChildTopics[ $this->tid ];
|
||||
}
|
||||
|
||||
// </editor-fold>
|
||||
}
|
||||
Reference in new issue
Block a user