Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -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
|
||||
|
||||
@@ -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