Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+317 -107
View File
@@ -2,10 +2,10 @@
/**
* @brief Topic Model
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @subpackage Board
* @package IPS Community Suite
* @subpackage Forums
* @since 8 Jan 2014
* @version SVN_VERSION_NUMBER
*/
@@ -144,6 +144,20 @@ class _Topic extends \IPS\Content\Item implements
* @brief Hover preview
*/
public $tableHoverUrl = TRUE;
/**
* Callback from \IPS\Http\Url\Inernal::correctUrlFromVerifyClass()
*
* This is called when verifying the *the URL currently being viewed* is correct, before calling self::loadFromUrl()
* Can be used if there is a more effecient way to load and cache the objects that will be used later on that page
*
* @param \IPS\Http\Url $url The URL of the page being viewed, which belongs to this class
* @return void
*/
public static function preCorrectUrlFromVerifyClass( \IPS\Http\Url $url )
{
\IPS\forums\Forum::loadIntoMemory();
}
/**
* Check the request for legacy parameters we may need to redirect to
@@ -168,6 +182,7 @@ class _Topic extends \IPS\Content\Item implements
/* p=123 needs to go to comment=123 */
if( isset( \IPS\Request::i()->p ) )
{
$paramsToSet['do'] = 'findComment';
$paramsToSet['comment'] = \IPS\Request::i()->p;
$paramsToUnset[] = 'p';
}
@@ -233,13 +248,13 @@ class _Topic extends \IPS\Content\Item implements
}
/**
* Get items with permisison check
* Get items with permission check
*
* @param array $where Where clause
* @param string $order MySQL ORDER BY clause (NULL to order by date)
* @param int|array $limit Limit clause
* @param string|NULL $permissionKey A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index or NULL to ignore permissions
* @param bool|NULL $includeHiddenItems Include hidden files? Boolean or NULL to detect if currently logged member has permission
* @param mixed $includeHiddenItems Include hidden items? NULL to detect if currently logged in member has permission, -1 to return public content only, TRUE to return unapproved content and FALSE to only return unapproved content the viewing member submitted
* @param int $queryFlags Select bitwise flags
* @param \IPS\Member $member The member (NULL to use currently logged in member)
* @param bool $joinContainer If true, will join container data (set to TRUE if your $where clause depends on this data)
@@ -251,12 +266,13 @@ class _Topic extends \IPS\Content\Item implements
* @param bool $joinTags If true, will join the tags table
* @param bool $joinAuthor If true, will join the members table for the author
* @param bool $joinLastCommenter If true, will join the members table for the last commenter
* @param bool $showMovedLinks If true, moved item links are included in the results
* @return \IPS\Patterns\ActiveRecordIterator|int
*/
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=NULL, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE )
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE )
{
$where = static::getItemsWithPermissionWhere( $where, $permissionKey, $member, $joinContainer, $skipPermission );
return parent::getItemsWithPermission( $where, $order, $limit, $permissionKey, $includeHiddenItems, $queryFlags, $member, $joinContainer, $joinComments, $joinReviews, $countOnly, $joins, $skipPermission, $joinTags, $joinAuthor, $joinLastCommenter );
return parent::getItemsWithPermission( $where, $order, $limit, $permissionKey, $includeHiddenItems, $queryFlags, $member, $joinContainer, $joinComments, $joinReviews, $countOnly, $joins, $skipPermission, $joinTags, $joinAuthor, $joinLastCommenter, $showMovedLinks );
}
/**
@@ -279,7 +295,7 @@ class _Topic extends \IPS\Content\Item implements
* @param string $permissionKey A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index
* @param \IPS\Member $member The member (NULL to use currently logged in member)
* @param bool $joinContainer If true, will join container data (set to TRUE if your $where clause depends on this data)
* @param mixed $skipPermission If you are getting records from a specific container, pass the container to reduce the number of permission checks necessary or pass TRUE to skip conatiner-based permission. You must still specify this in the $where clause
* @param mixed $skipPermission If you are getting records from a specific container, pass the container to reduce the number of permission checks necessary or pass TRUE to skip container-based permission. You must still specify this in the $where clause
* @return array
*/
public static function getItemsWithPermissionWhere( $where, $permissionKey, $member, &$joinContainer, $skipPermission=FALSE )
@@ -304,18 +320,16 @@ class _Topic extends \IPS\Content\Item implements
}
}
}
/* Don't show links to moved topics */
$where['item'][] = array( "( NULLIF(forums_topics.moved_to, '') IS NULL )" );
$where['container'][] = array_merge( array( '( ' . $whereString . ' )' ), $whereParams );
}
/* Don't show topics from forums in which topics only show to the poster */
if ( $skipPermission !== TRUE and in_array( $permissionKey, array( 'view', 'read' ) ) )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( !$member->modPermission( 'can_read_all_topics' ) or $member->modPermission( 'forums' ) !== -1 )
if ( !$member->modPermission( 'can_read_all_topics' ) or ( $member->modPermission( 'forums' ) !== TRUE and $member->modPermission( 'forums' ) !== -1 ) )
{
if ( $skipPermission instanceof \IPS\forums\Forum )
{
@@ -343,7 +357,7 @@ class _Topic extends \IPS\Content\Item implements
}
}
}
/* Don't show topics in forums we can't view because our post count is too low */
if ( !$skipPermission and in_array( $permissionKey, array( 'view', 'read' ) ) )
{
@@ -496,17 +510,11 @@ class _Topic extends \IPS\Content\Item implements
/* Moderator actions */
if ( isset( $values['topic_create_state'] ) )
{
if ( static::modPermission( 'feature', NULL, $this->container() ) )
if ( static::modPermission( 'lock', NULL, $this->container() ) )
{
$this->state = ( in_array( 'lock', $values['topic_create_state'] ) ) ? 'closed' : 'open';
}
if ( static::modPermission( 'hide', NULL, $this->container() ) )
{
$this->approved = ( in_array( 'hide', $values['topic_create_state'] ) ) ? -1 : 1;
}
if ( static::modPermission( 'pin', NULL, $this->container() ) )
{
$this->pinned = ( in_array( 'pin', $values['topic_create_state'] ) ) ? 1 : 0;
@@ -521,17 +529,11 @@ class _Topic extends \IPS\Content\Item implements
if ( static::modPermission( 'lock', NULL, $this->container() ) )
{
/* Set open/close time */
if ( isset( $values['topic_open_time'] ) and $values['topic_open_time'] )
$this->topic_open_time = !empty( $values['topic_open_time'] ) ? $values['topic_open_time']->getTimestamp() : 0;
$this->topic_close_time = !empty( $values['topic_close_time'] ) ? $values['topic_close_time']->getTimestamp() : 0;
if( isset( $values['topic_create_state'] ) and !in_array( 'lock', $values['topic_create_state'] ) )
{
$this->topic_open_time = $values['topic_open_time']->getTimestamp();
}
if ( isset( $values['topic_close_time'] ) and $values['topic_close_time'] )
{
$this->topic_close_time = $values['topic_close_time']->getTimestamp();
}
else if( isset( $values['topic_create_state'] ) and !in_array( 'lock', $values['topic_create_state'] ) )
{
$this->topic_close_time = 0;
$this->state = 'open';
}
@@ -541,8 +543,20 @@ class _Topic extends \IPS\Content\Item implements
$this->state = 'closed';
}
}
}
/**
* Process created object AFTER the object has been created
*
* @param \IPS\Content\Comment|NULL $comment The first comment
* @param array $values Values from form
* @return void
*/
protected function processAfterCreate( $comment, $values )
{
$this->processAfterCreateOrEdit( $values );
parent::processAfterCreate( $comment, $values );
}
/**
@@ -553,11 +567,13 @@ class _Topic extends \IPS\Content\Item implements
*/
public function processAfterEdit( $values )
{
$this->processAfterCreateOrEdit( $values );
/* Initial Comment */
parent::processAfterEdit( $values );
/* Topic changed? */
if ( ! $this->hidden() and ( $this->tid === $this->container()->last_id ) and ( $this->title_seo !== $this->container()->seo_last_title ) )
if ( ! $this->hidden() and ( $this->tid === $this->container()->last_id ) )
{
$this->container()->seo_last_title = $this->title_seo;
$this->container()->last_title = $this->title;
@@ -574,6 +590,32 @@ class _Topic extends \IPS\Content\Item implements
}
}
}
/**
* Process after the object has been edited or created on the front-end
*
* @param array $values Values from form
* @return void
*/
protected function processAfterCreateOrEdit( $values )
{
/* Moderator actions */
if ( isset( $values['topic_create_state'] ) )
{
if ( static::modPermission( 'hide', NULL, $this->container() ) )
{
if( in_array( 'hide', $values['topic_create_state'] ) )
{
$this->hide( NULL );
}
/* Only unhide if the topic is not pending approval */
elseif( $this->hidden() !== 1 )
{
$this->unhide( NULL );
}
}
}
}
/**
* @brief Cached URLs
@@ -633,6 +675,35 @@ class _Topic extends \IPS\Content\Item implements
return $stats;
}
/**
* Set the new popular time if needed
*
*/
public function rebuildPopularTime( )
{
$popularNowSettings = json_decode( \IPS\Settings::i()->forums_popular_now, TRUE );
if ( $popularNowSettings['posts'] and $popularNowSettings['minutes'] )
{
$popularNowInterval = new \DateInterval( 'PT' . $popularNowSettings['minutes'] . 'M' );
$comments = $this->comments( NULL, NULL, 'date', 'desc', NULL, FALSE, \IPS\DateTime::create()->sub( $popularNowInterval ) );
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();
}
}
}
/**
* Set name
@@ -643,7 +714,7 @@ class _Topic extends \IPS\Content\Item implements
public function set_title( $title )
{
$this->_data['title'] = $title;
$this->_data['title_seo'] = \IPS\Http\Url::seoTitle( $title );
$this->_data['title_seo'] = \IPS\Http\Url\Friendly::seoTitle( $title );
}
/**
@@ -655,11 +726,11 @@ class _Topic extends \IPS\Content\Item implements
{
if( !$this->_data['title_seo'] )
{
$this->title_seo = \IPS\Http\Url::seoTitle( $this->title );
$this->title_seo = \IPS\Http\Url\Friendly::seoTitle( $this->title );
$this->save();
}
return $this->_data['title_seo'] ?: \IPS\Http\Url::seoTitle( $this->title );
return $this->_data['title_seo'] ?: \IPS\Http\Url\Friendly::seoTitle( $this->title );
}
/**
@@ -722,9 +793,9 @@ class _Topic extends \IPS\Content\Item implements
*/
public function searchIndexPermissions()
{
$return = parent::searchIndexPermissions();
$return = $this->container()->searchIndexPermissions();
if ( !$this->container()->can_view_others )
if ( $this->starter_id and !$this->container()->can_view_others )
{
/* If the search index permissions are empty, just return now because no one can see content in this forum */
if( !$return )
@@ -732,40 +803,16 @@ class _Topic extends \IPS\Content\Item implements
return $return;
}
$normal = $return;
$return = array();
$members = array();
foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $moderator )
{
if ( $moderator['perms'] === '*' or in_array( 'can_read_all_topics', explode( ',', $moderator['perms'] ) ) )
{
if( $moderator['type'] === 'g' )
{
$return[] = $moderator['id'];
}
else
{
$members[] = "m{$moderator['id']}";
}
}
}
$return = ( $normal == '*' ) ? array_unique( $return ) : array_intersect( explode( ',', $normal ), array_unique( $return ) );
$return = $this->container()->permissionsThatCanAccessAllTopics();
if ( $this->starter_id )
{
$members[] = "m{$this->starter_id}";
}
if( count( $members ) )
{
$return = array_merge( $return, $members );
$return[] = "m{$this->starter_id}";
}
$return = implode( ',', $return );
}
return $return;
}
@@ -855,11 +902,11 @@ class _Topic extends \IPS\Content\Item implements
if ( !empty( $openIds ) )
{
\IPS\Db::i()->update( 'forums_topics', array( 'state' => 'open', 'topic_close_time' => 0 ), \IPS\Db::i()->in( 'tid', $openIds ) );
\IPS\Db::i()->update( 'forums_topics', array( 'state' => 'open', 'topic_open_time' => 0 ), \IPS\Db::i()->in( 'tid', $openIds ) );
}
if ( !empty( $closeIds ) )
{
\IPS\Db::i()->update( 'forums_topics', array( 'state' => 'closed', 'topic_open_time' => 0 ), \IPS\Db::i()->in( 'tid', $closeIds ) );
\IPS\Db::i()->update( 'forums_topics', array( 'state' => 'closed', 'topic_close_time' => 0 ), \IPS\Db::i()->in( 'tid', $closeIds ) );
}
}
@@ -922,12 +969,123 @@ class _Topic extends \IPS\Content\Item implements
public function delete()
{
parent::delete();
\IPS\forums\Topic\ArchivedPost::db()->delete( 'forums_archive_posts', array( 'archive_topic_id=?', $this->tid ) );
try
{
\IPS\forums\Topic\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 ){}
\IPS\Db::i()->delete( 'forums_question_ratings', array( 'topic=?', $this->tid ) );
\IPS\Db::i()->delete( 'forums_answer_ratings', array( 'topic=?', $this->tid ) );
/* Delete any moved topic links that point to this topic */
\IPS\Db::i()->delete( 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) );
}
/**
* Merge other items in (they will be deleted, this will be kept)
*
* @param array $items Items to merge in
* @param bool $keepLinks Retain redirect links for the items that were merge in
* @return void
*/
public function mergeIn( array $items, $keepLinks=FALSE )
{
/* If this is a QA forum we need to make sure we only have one best answer (at most) post-merge */
if( $this->isQuestion() )
{
/* Does this topic already have a best answer? */
if( $this->topic_answered_pid )
{
/* Then we need to make sure none of the items also has a best answer */
foreach( $items as $item )
{
/* Reset best answer for this topic */
if( $item->topic_answered_pid )
{
try
{
$post = \IPS\forums\Topic\Post::load( $item->topic_answered_pid );
$post->post_bwoptions['best_answer'] = FALSE;
$post->save();
}
catch( \OutOfRangeException $e ){}
}
}
}
/* The topic doesn't have a best answer, but we still need to make sure we only have one best answer total post-merge */
else
{
$bestAnswerSeen = FALSE;
foreach( $items as $item )
{
if( $item->topic_answered_pid )
{
/* Have we seen a best answer yet? If not, then we're ok. */
if( $bestAnswerSeen === FALSE )
{
/* This topic had no best answer flag set, so set it now */
$this->topic_answered_pid = $item->topic_answered_pid;
$this->save();
$bestAnswerSeen = TRUE;
continue;
}
/* If we have though, reset any others */
try
{
$post = \IPS\forums\Topic\Post::load( $item->topic_answered_pid );
$post->post_bwoptions['best_answer'] = FALSE;
$post->save();
}
catch( \OutOfRangeException $e ){}
}
}
}
}
return parent::mergeIn( $items, $keepLinks );
}
/**
* Hide
*
* @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @param string $reason Reason
* @return void
*/
public function hide( $member, $reason = NULL )
{
/* Hide any moved topic links that point to this topic */
foreach( \IPS\Db::i()->select( '*', 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) ) as $link )
{
\IPS\forums\Topic::constructFromData( $link )->hide( $member, $reason );
}
return parent::hide( $member, $reason );
}
/**
* Unhide
*
* @param \IPS\Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @return void
*/
public function unhide( $member )
{
/* Unhide any moved topic links that point to this topic */
foreach( \IPS\Db::i()->select( '*', 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) ) as $link )
{
\IPS\forums\Topic::constructFromData( $link )->unhide( $member );
}
return parent::unhide( $member );
}
/* !Saved Actions */
/**
@@ -1052,14 +1210,14 @@ class _Topic extends \IPS\Content\Item implements
{
return TRUE;
}
/* Or if we're a moderator */
if
(
$member->modPermission( 'can_set_best_answer' )
and
(
$member->modPermission( \IPS\forums\Forum::$modPerm ) === -1
( $member->modPermission( \IPS\forums\Forum::$modPerm ) === TRUE or $member->modPermission( \IPS\forums\Forum::$modPerm ) === -1 )
or
(
is_array( $member->modPermission( \IPS\forums\Forum::$modPerm ) )
@@ -1217,6 +1375,27 @@ class _Topic extends \IPS\Content\Item implements
}
}
/**
* Definite Article
*
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
* @return string
*/
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL )
{
$bitOptions = ( $containerData['forums_bitoptions'] instanceof \IPS\Patterns\Bitwise ) ? $containerData['forums_bitoptions'] : new \IPS\Patterns\Bitwise( array( 'forums_bitoptions' => $containerData['forums_bitoptions'] ), \IPS\forums\Forum::$bitOptions['forums_bitoptions'] );
if ( $bitOptions['bw_enable_answers'] )
{
$lang = $lang ?: \IPS\Member::loggedIn()->language();
return $lang->addToStack( '__defart_question', FALSE );
}
else
{
return parent::_definiteArticle( $containerData, $lang );
}
}
/* !Sitemap */
/**
@@ -1327,38 +1506,6 @@ class _Topic extends \IPS\Content\Item implements
return parent::canEdit( $member );
}
/**
* Can pin?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canPin( $member=NULL )
{
if ( $this->isArchived() )
{
return FALSE;
}
return parent::canPin( $member );
}
/**
* Can unpin?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canUnpin( $member=NULL )
{
if ( $this->isArchived() )
{
return FALSE;
}
return parent::canUnpin( $member );
}
/**
* Can feature?
*
@@ -1663,4 +1810,67 @@ class _Topic extends \IPS\Content\Item implements
return $metaDescription;
}
/**
* Get output for API
*
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
* @apiresponse \IPS\forums\Forum forum Forum
* @apiresponse int posts Number of posts
* @apiresponse int views Number of posts
* @apiresponse string prefix The prefix tag, if there is one
* @apiresponse [string] tags The tags
* @apiresponse \IPS\forums\Topic\Post firstPost The first post in the topic
* @apiresponse \IPS\forums\Topic\Post lastPost The last post in the topic
* @apiresponse \IPS\forums\Topic\Post bestAnswer The best answer, if this is a question and there is one
* @apiresponse bool locked Topic is locked
* @apiresponse bool hidden Topic is hidden
* @apiresponse bool pinned Topic is pinned
* @apiresponse bool featured Topic is featured
* @apiresponse bool archived Topic is archived
* @apiresponse \IPS\Poll poll Poll data, if there is one
* @apiresponse string url URL
* @apiresponse float rating Average Rating
*/
public function apiOutput()
{
$firstPost = $this->comments( 1, 0, 'date', 'asc' );
$lastPost = $this->comments( 1, 0, 'date', 'desc' );
$bestAnswer = $this->bestAnswer();
return array(
'id' => $this->tid,
'title' => $this->title,
'forum' => $this->container()->apiOutput(),
'posts' => $this->posts,
'views' => $this->views,
'prefix' => $this->prefix(),
'tags' => $this->tags(),
'firstPost' => $firstPost ? $firstPost->apiOutput() : null,
'lastPost' => $lastPost ? $lastPost->apiOutput() : null,
'bestAnswer' => $bestAnswer ? $bestAnswer->apiOutput() : null,
'locked' => (bool) $this->locked(),
'hidden' => (bool) $this->hidden(),
'pinned' => (bool) $this->mapped('pinned'),
'featured' => (bool) $this->mapped('featured'),
'archived' => (bool) $this->isArchived(),
'poll' => $this->poll_state ? \IPS\Poll::load( $this->poll_state )->apiOutput() : null,
'url' => (string) $this->url(),
'rating' => $this->averageRating(),
);
}
/**
* Returns the content
*
* @return string
* @throws \BadMethodCallException
* @note This is overridden for performance reasons - selecting a post by a PID is more efficient than select * from posts order by date desc limit 1
*/
public function content()
{
$firstComment = $this->firstComment();
return $firstComment ? $firstComment->content() : '';
}
}