Version 5.0.13

This commit is contained in:
Neo committed 2025-12-19 19:04:57 -08:00
1 parent b89858b2ff
commit 5efe744c5f
1407 files changed
+69690 -30177

No files matched your search

+119 -119
View File
@@ -24,6 +24,7 @@ use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Search\Index;
use IPS\Content\Search\SearchContent;
use IPS\Content\Taggable;
use IPS\Content\ViewUpdates;
use IPS\Data\Store;
use IPS\DateTime;
@@ -57,6 +58,7 @@ use IPS\Member\Club;
use IPS\Member\Group;
use IPS\Node\Colorize;
use IPS\Node\DelayedCount;
use IPS\Node\Grouping;
use IPS\Node\Icon;
use IPS\Node\Model;
use IPS\Node\Permissions;
@@ -107,6 +109,7 @@ class Forum extends Model implements Permissions
use ClubContainer;
use Colorize;
use DelayedCount;
use Grouping;
use Statistics;
use ViewUpdates;
use Icon;
@@ -284,7 +287,7 @@ class Forum extends Model implements Permissions
*/
public function __set( mixed $key, mixed $value ) : void
{
if( mb_strpos( $key, "forum_" ) === 0 AND $key !== 'forum_allow_rating' )
if( mb_strpos( $key, "forum_" ) === 0 )
{
$key = preg_replace( "/^forum_(.+?)$/", "$1", $key );
$this->$key = $value;
@@ -705,6 +708,8 @@ class Forum extends Model implements Permissions
*/
public function lastPost( int $count=1, bool $isChild=false ): ?array
{
$results = [];
if ( $count > 5 )
{
throw new OverflowException( 'Too many posts to return, maximum is 5' );
@@ -718,13 +723,15 @@ class Forum extends Model implements Permissions
{
try
{
$results = [];
foreach(
new ActiveRecordIterator(
Db::i()->select( '*', 'forums_posts', array( 'topic_id=? AND queued=0', Db::i()->select( 'tid', 'forums_topics', array( 'forum_id=? AND approved=1 AND starter_id=? AND is_future_entry=0', $this->_id, Member::loggedIn()->member_id ), 'last_post DESC', 1 )->first() ), 'post_date DESC', $count ),
Post::class ) as $post )
{
/* @var Post $post */
$results[ $post->post_date ] = [
'_author' => $post->author()->member_id,
'author' => $post->author(),
'topic_url' => $post->item()->url(),
'topic_title' => $post->item()->title,
@@ -743,33 +750,24 @@ class Forum extends Model implements Permissions
foreach( $this->children() as $child )
{
/* We need to merge all children and pick the top 5 */
if ( $childLastPost = $child->lastPost( $count ) )
if ( $childLastPost = $child->lastPost( $count, true ) )
{
$results = $results + $childLastPost;
if ( is_array( $childLastPost ) )
{
if ( isset( $childLastPost['date'] ) )
{
$results[$childLastPost['date']] = $childLastPost;
}
else
{
$results = $results + $childLastPost;
}
}
}
}
if ( count( $results ) )
{
krsort( $results );
if ( $count === 1 )
{
/* return the first from the array */
return array_shift( $results );
}
/* array_slice re-indexes, and we'd lose the krsort timestamp key */
return array_intersect_key( $results, array_flip( array_slice( array_keys( $results ), 0, $count ) ) );
}
else
{
return null;
}
}
elseif ( !$this->permission_showtopic and !$this->can('view') )
{
$results = [];
if( !$this->sub_can_post )
{
foreach( $this->children() as $child )
@@ -777,47 +775,38 @@ class Forum extends Model implements Permissions
/* We need to merge all children and pick the top 5 */
if ( $childLastPost = $child->lastPost( $count, true ) )
{
$results = $results + $childLastPost;
if ( is_array( $childLastPost ) )
{
if ( isset( $childLastPost['date'] ) )
{
$results[$childLastPost['date']] = $childLastPost;
}
else
{
$results = $results + $childLastPost;
}
}
}
}
}
if ( count( $results ) )
{
krsort( $results );
if ( $count === 1 )
{
/* return the first from the array */
return array_shift( $results );
}
/* array_slice re-indexes, and we'd lose the krsort timestamp key */
return array_intersect_key( $results, array_flip( array_slice( array_keys( $results ), 0, $count ) ) );
}
else
{
return null;
}
}
else
{
$results = [];
/* Do we have any data, or do we need to update? The default or cleared state is NULL, forums without topics should have [] */
if ( $this->sub_can_post and $this->posts and ! $this->_data['last_post_data'] )
if ( $this->sub_can_post and $this->posts and !$this->_data['last_post_data'] )
{
$this->setLastComment();
/* We can't use save as dynamic variables have now been assigned which confuses save() */
Db::i()->update( 'forums_forums', [ 'last_post_data' => $this->_data['last_post_data'] ], [ 'id=?', $this->_id ] );
Db::i()->update( 'forums_forums', ['last_post_data' => $this->_data['last_post_data']], ['id=?', $this->_id] );
}
$lastPostData = $this->_last_post_data;
if ( count( $lastPostData ) )
{
foreach( $lastPostData as $lastPostTime => $lastPost )
foreach ( $lastPostData as $lastPostTime => $lastPost )
{
if ( $this->sub_can_post and !$this->permission_showtopic and ! $this->can('read') )
if ( $this->sub_can_post and !$this->permission_showtopic and !$this->can( 'read' ) )
{
$lastPost['last_title'] = null;
}
@@ -829,31 +818,31 @@ class Forum extends Model implements Permissions
}
else
{
$author = ! empty( $lastPost['last_poster_id'] ) ? $lastPost['last_poster_id'] : ( $lastPost['last_poster_name'] ?? Member::loggedIn()->language()->addToStack( "guest" ) );
$author = !empty( $lastPost['last_poster_id'] ) ? $lastPost['last_poster_id'] : ( $lastPost['last_poster_name'] ?? Member::loggedIn()->language()->addToStack( "guest" ) );
}
$results[ $lastPostTime ] = array(
'author' => null,
'_author' => $author,
'topic_url' => Url::internal( "app=forums&module=forums&controller=topic&id={$lastPost['last_id']}", 'front', 'forums_topic', array( $lastPost['seo_last_title'] ) ),
'topic_title' => $lastPost['last_title'],
'date' => ( $lastPost['last_post'] ?? (int) ( substr( $lastPostTime, 0, strpos( $lastPostTime, '.' ) ) ) ),
'last_poster_anon' => $lastPost['last_poster_anon'],
$results[$lastPostTime] = [
'author' => null,
'_author' => $author,
'topic_url' => Url::internal( "app=forums&module=forums&controller=topic&id={$lastPost['last_id']}", 'front', 'forums_topic', [$lastPost['seo_last_title']] ),
'topic_title' => $lastPost['last_title'],
'date' => ( $lastPost['last_post'] ?? (int)( substr( $lastPostTime, 0, strpos( $lastPostTime, '.' ) ) ) ),
'last_poster_anon' => $lastPost['last_poster_anon'],
'last_post_snippet' => $lastPost['last_post_snippet'] ?? null,
'posts' => $lastPost['posts'] ?? 0
);
'posts' => $lastPost['posts'] ?? 0
];
}
}
foreach( $this->children() as $child )
foreach ( $this->children() as $child )
{
/* @var Forum $child */
/* We need to merge all children and pick the top 5 */
if ( $childLastPost = $child->lastPost( $count, true ) )
{
if( is_array( $childLastPost ) )
if ( is_array( $childLastPost ) )
{
if( isset( $childLastPost['date'] ) )
if ( isset( $childLastPost['date'] ) )
{
$results[$childLastPost['date']] = $childLastPost;
}
@@ -864,75 +853,76 @@ class Forum extends Model implements Permissions
}
}
}
}
if ( count( $results ) )
if ( count( $results ) )
{
/* Don't process the author details if we're not going to use them */
if ( ! $isChild )
{
/* Don't process the author details if we're not going to use them */
if ( ! $isChild )
{
$membersToLoad = [];
$membersToLoad = [];
/* Let's get the author details */
foreach ( $results as $timestamp => $data )
/* Let's get the author details */
foreach ( $results as $timestamp => $data )
{
if ( $data['_author'] and is_int( $data['_author'] ) )
{
if ( $data['_author'] and is_int( $data['_author'] ) )
$membersToLoad[ $timestamp ] = $data['_author'];
}
else
{
/* Default to a guest account so we can be sure the author is never null */
$results[ $timestamp ]['author'] = new Member;
if ( $data['_author'] and is_string( $data['_author'] ) )
{
$membersToLoad[ $timestamp ] = $data['_author'];
$results[ $timestamp ]['author']->name = $data['_author'];
}
}
}
$idsToLoad = array_unique( array_values( $membersToLoad ) );
/* Load the members not in static::$lastPostLoadedMembers */
$idsToLoad = array_diff( $idsToLoad, array_keys( static::$lastPostLoadedMembers ) );
if ( count( $idsToLoad ) )
{
foreach( Db::i()->select( '*', 'core_members', [ Db::i()->in( 'member_id', $idsToLoad ) ] ) as $lastPostMember )
{
static::$lastPostLoadedMembers[ $lastPostMember['member_id'] ] = Member::constructFromData( $lastPostMember );
}
}
if ( count( $membersToLoad ) )
{
foreach ( $membersToLoad as $timestamp => $memberId )
{
if ( isset( static::$lastPostLoadedMembers[ $memberId ] ) )
{
$results[$timestamp]['author'] = static::$lastPostLoadedMembers[ $memberId ];
}
else
{
/* Default to a guest account so we can be sure the author is never null */
$results[ $timestamp ]['author'] = new Member;
if ( $data['_author'] and is_string( $data['_author'] ) )
{
$results[ $timestamp ]['author']->name = $data['_author'];
}
}
}
$idsToLoad = array_unique( array_values( $membersToLoad ) );
/* Load the members not in static::$lastPostLoadedMembers */
$idsToLoad = array_diff( $idsToLoad, array_keys( static::$lastPostLoadedMembers ) );
if ( count( $idsToLoad ) )
{
foreach( Db::i()->select( '*', 'core_members', [ Db::i()->in( 'member_id', $idsToLoad ) ] ) as $lastPostMember )
{
static::$lastPostLoadedMembers[ $lastPostMember['member_id'] ] = Member::constructFromData( $lastPostMember );
}
}
if ( count( $membersToLoad ) )
{
foreach ( $membersToLoad as $timestamp => $memberId )
{
if ( isset( static::$lastPostLoadedMembers[ $memberId ] ) )
{
$results[$timestamp]['author'] = static::$lastPostLoadedMembers[ $memberId ];
}
else
{
/* Technically we shouldn't ever get here, but let's not throw an exception */
$results[$timestamp]['author'] = Member::load( $memberId );
}
/* Technically we shouldn't ever get here, but let's not throw an exception */
$results[$timestamp]['author'] = Member::load( $memberId );
}
}
}
krsort( $results );
if ( $count === 1 )
{
/* return the first from the array */
return array_shift( $results );
}
/* array_slice re-indexes, and we'd lose the krsort timestamp key */
return array_intersect_key( $results, array_flip( array_slice( array_keys( $results ), 0, $count ) ) );
}
else
krsort( $results );
if ( $count === 1 )
{
return null;
/* return the first from the array */
return array_shift( $results );
}
/* array_slice re-indexes, and we'd lose the krsort timestamp key */
return array_intersect_key( $results, array_flip( array_slice( array_keys( $results ), 0, $count ) ) );
}
else
{
return null;
}
}
@@ -2091,10 +2081,8 @@ class Forum extends Model implements Permissions
/* Do the move */
Db::i()->update( 'forums_topics', array( 'forum_id' => $node->_id ), array( 'forum_id=?', $this->_id ) );
Db::i()->update( 'forums_question_ratings', array( 'forum' => $node->_id ), array( 'forum=?', $this->_id ) );
/* Rebuild tags */
if ( in_array( 'IPS\Content\Taggable', class_implements( $contentItemClass ) ) )
if( IPS::classUsesTrait( $contentItemClass, Taggable::class ) )
{
Db::i()->update( 'core_tags', array(
'tag_aap_lookup' => md5( static::$permApp . ';' . static::$permType . ';' . $node->_id ),
@@ -2214,11 +2202,14 @@ class Forum extends Model implements Permissions
* @return array
* @apiresponse int id ID number
* @apiresponse string name Forum name
* @apiresponse string description Forum description
* @apiresponse string cardImage Forum card image
* @apiresponse string path Forum name including parents (e.g. "Example Category > Example Forum")
* @apiresponse string type The type of forum: "discussions", "questions", "category", or "redirect"
* @apiresponse int topics Number of topics in forum
* @apiresponse string url URL
* @apiresponse int|null parentId Parent Node ID
* @apiresponse int followerCount Total members following this forum
* @clientapiresponse object|null permissions Node permissions
*/
public function apiOutput( Member $authorizedMember = NULL ): array
@@ -2246,11 +2237,14 @@ class Forum extends Model implements Permissions
$return = array(
'id' => $this->id,
'name' => $this->_title,
'description' => $this->description,
'cardImage' => null,
'path' => implode( ' > ', $path ),
'type' => $type,
'topics' => $this->topics,
'url' => (string) $this->url(),
'parentId' => static::$databaseColumnParent ? $this->{static::$databaseColumnParent} : NULL
'parentId' => static::$databaseColumnParent ? $this->{static::$databaseColumnParent} : NULL,
'followerCount' => Topic::containerFollowerCount( $this )
);
if( $authorizedMember === NULL )
@@ -2271,6 +2265,12 @@ class Forum extends Model implements Permissions
}
}
if ( $this->card_image )
{
$file = File::get( 'forums_Cards', $this->card_image );
$return['card_image'] = $file->fullyQualifiedUrl( $this->card_image );
}
return $return;
}
@@ -115,7 +115,7 @@ class SavedAction extends Model
{
foreach ( static::getStore() as $action )
{
if ( $action['mm_enabled'] and ( $action['mm_forums'] == '*' or in_array( $forum->id, array_filter( explode( ',', $action['mm_forums'] ) ) ) ) )
if ( $action['mm_enabled'] and ( $action['mm_forums'] == '*' or in_array( $forum->id, $forum->normalizeIds( $action['mm_forums'] ) ) ) )
{
$return[ $action['mm_id'] ] = static::constructFromData( $action );
}
@@ -158,16 +158,27 @@ class SavedAction extends Model
{
$form->addHeader( 'settings' );
$form->add( new Translatable( 'mm_title', NULL, TRUE, array( 'app' => 'forums', 'key' => ( $this->mm_id ? "forums_mmod_{$this->mm_id}" : NULL ) ) ) );
$form->add( new 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 Node( 'mm_forums', ( $this->mm_id and $this->mm_forums != '*' ) ? $this->mm_forums : 0, FALSE, array(
'class' => 'IPS\forums\Forum',
'multiple' => true,
'nodeGroups' => true,
'clubs' => true,
'zeroVal' => 'all' )
) );
$form->addHeader( 'topic_properties' );
$form->add( new Radio( 'topic_state', $this->mm_id ? $this->topic_state : 'leave', FALSE, array( 'options' => array( 'leave' => 'mm_leave', 'open' => 'unlock', 'close' => 'lock' ) ) ) );
$form->add( new Radio( 'topic_pin', $this->mm_id ? $this->topic_pin : 'leave', FALSE, array( 'options' => array( 'leave' => 'mm_leave', 'pin' => 'pin', 'unpin' => 'unpin' ) ) ) );
$form->add( new Radio( 'topic_approve', $this->mm_id ? $this->topic_approve : 0, FALSE, array( 'options' => array( 0 => 'mm_leave', 1 => 'unhide', 2 => 'hide' ) ) ) );
$form->add( new 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 )
{
return $forum->sub_can_post and !$forum->redirect_url;
} ) ) );
$form->add( new 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 )
{
return $forum->sub_can_post and !$forum->redirect_url;
} )
) );
$form->add( new YesNo( 'topic_move_link', $this->mm_id ? $this->topic_move_link : TRUE, FALSE, array(), NULL, NULL, NULL, 'topic_move_link' ) );
$form->addHeader( 'topic_title' );
@@ -198,13 +209,7 @@ class SavedAction extends Model
}
else
{
$forums = array();
foreach ( $values['mm_forums'] as $forum )
{
$forums[] = $forum->_id;
}
$values['mm_forums'] = ( implode( ',', $forums ) );
$values['mm_forums'] = array_keys( $values['mm_forums'] );
}
}
@@ -88,9 +88,9 @@ class ArchivedPost extends Post
'date' => 'content_date',
'ip_address' => 'ip_address',
'edit_time' => 'edit_time',
'edit_show' => 'append_edit',
'edit_show' => 'show_edited_by',
'edit_member_name' => 'edit_name',
'edit_reason' => 'post_edit_reason',
'edit_reason' => 'edit_reason',
'hidden' => 'queued',
'first' => 'is_first'
);
@@ -326,50 +326,6 @@ class Post extends Comment implements Embeddable,
/* !Questions & Answers */
/**
* 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;
}
/* Downvoting disabled? */
if ( $rating === -1 and !Settings::i()->forums_answers_downvote )
{
return FALSE;
}
/* 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;
}
/* Check the forum settings */
if ( $this->item()->container()->qa_rate_answers !== NULL and $this->item()->container()->qa_rate_answers != '*' and !$member->inGroup( explode( ',', $this->item()->container()->qa_rate_answers ) ) )
{
return FALSE;
}
return TRUE;
}
/**
* Custom where conditions used specifically for finding comments
*
@@ -385,17 +341,6 @@ class Post extends Comment implements Embeddable,
return parent::findCommentWhere();
}
/**
* [ActiveRecord] Save
*
* @return void
*/
public function save(): void
{
parent::save();
$this->item()->clearVotes();
}
/**
* Delete Post
+242 -122
View File
@@ -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>
}