Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

+199 -153
View File
@@ -11,6 +11,9 @@
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Http\Url;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -482,7 +485,7 @@ abstract class _Item extends \IPS\Content
'follow_area' => $followArea,
'follow_rel_id' => $obj->$idColumn,
'follow_member_id' => \IPS\Member::loggedIn()->member_id,
'follow_is_anon' => 0,
'follow_is_anon' => isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : 0,
'follow_added' => time() + 1, // Make sure streams show follows after content is created
'follow_notify_do' => 1,
'follow_notify_meta' => '',
@@ -629,8 +632,12 @@ abstract class _Item extends \IPS\Content
if( isset( static::$databaseColumnMap['is_future_entry'] ) )
{
$isFutureEntry = ( $time->getTimestamp() > time() );
$column = static::$databaseColumnMap['is_future_entry'];
$this->$column = $time->getTimestamp() > time();
if( $this->$column != $isFutureEntry )
{
$this->$column = $isFutureEntry;
}
}
}
}
@@ -769,7 +776,7 @@ abstract class _Item extends \IPS\Content
$return['guest_name'] = new \IPS\Helpers\Form\Text( 'guest_name', NULL, FALSE, array( 'minLength' => \IPS\Settings::i()->min_user_name_length, 'maxLength' => \IPS\Settings::i()->max_user_name_length, 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('comment_guest_name'), 'htmlAutocomplete' => "username" ), NULL, NULL, NULL, 'guest_name' );
}
}
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' and \IPS\Settings::i()->guest_captcha )
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' )
{
$return['captcha'] = new \IPS\Helpers\Form\Captcha;
}
@@ -1343,8 +1350,8 @@ abstract class _Item extends \IPS\Content
if ( $this instanceof \IPS\Content\FuturePublishing AND isset( $values[ static::$formLangPrefix . 'date' ] ) )
{
$column = static::$databaseColumnMap['is_future_entry'];
if ( $container AND $this->$column )
if ( $container AND isset( $this->changed[ $column ] ) AND !$this->changed[ $column ] ) // If the changed value is now false, it has just been published
{
if ( ( ! ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) AND $values[ static::$formLangPrefix . 'date' ] == 0 ) OR ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() <= time() ) )
{
@@ -1352,7 +1359,7 @@ abstract class _Item extends \IPS\Content
$this->publish();
}
}
else if ( $container and ! $this->$column )
else if ( $container AND isset( $this->changed[ $column ] ) AND $this->changed[ $column ] === TRUE ) // If the changed value is true, it has just been unpublished
{
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
{
@@ -1472,12 +1479,7 @@ abstract class _Item extends \IPS\Content
*/
protected function processAfterTagUpdate()
{
/* If we edited tags for a topic, we have to manually update search index because it wants
the first comment and not the content item itself. */
if ( $this instanceof \IPS\Content\Item and $this instanceof \IPS\Content\Searchable and static::$firstCommentRequired and $this->firstComment() )
{
\IPS\Content\Search\Index::i()->index( $this->firstComment() );
}
}
/**
@@ -1638,7 +1640,26 @@ abstract class _Item extends \IPS\Content
}
throw new \BadMethodCallException;
}
/**
* Get a shareable URL
*
* @param int|null $commentId
* @return Url
*/
public function shareableUrl( int $commentId = NULL ): Url
{
$idColumn = static::$databaseColumnId;
$url = $this->url();
if ( $commentId )
{
$url = $url->setFragment( 'findComment-' . $commentId );
}
return $url;
}
/**
* Get URL from index data
*
@@ -1825,7 +1846,7 @@ abstract class _Item extends \IPS\Content
{
try
{
if ( $loadedExtensions[ $map['location_key'] ]->attachmentPermissionCheck( \IPS\Member::loggedIn(), $map['id1'], $map['id2'], $map['id3'], $row, TRUE ) )
if ( method_exists( $loadedExtensions[ $map['location_key'] ], 'attachmentPermissionCheck') and $loadedExtensions[ $map['location_key'] ]->attachmentPermissionCheck( \IPS\Member::loggedIn(), $map['id1'], $map['id2'], $map['id3'], $row, TRUE ) )
{
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
}
@@ -1979,11 +2000,11 @@ abstract class _Item extends \IPS\Content
if ( isset( static::$commentClass ) and $oldContainer->_unapprovedComments !== NULL and isset( static::$databaseColumnMap['unapproved_comments'] ) )
{
$oldContainer->_unapprovedComments = \intval( $oldContainer->_unapprovedComments - $this->mapped('unapproved_comments') );
$oldContainer->_unapprovedComments = ( $oldContainer->_unapprovedComments > 0 ) ? \intval( $oldContainer->_unapprovedComments - $this->mapped('unapproved_comments') ) : 0;
}
if ( isset( static::$reviewClass ) and $oldContainer->_unapprovedReviews !== NULL and isset( static::$databaseColumnMap['unapproved_reviews'] ) )
{
$oldContainer->_unapprovedReviews = \intval( $oldContainer->_unapprovedReviews - $this->mapped('unapproved_reviews') );
$oldContainer->_unapprovedReviews = ( $oldContainer->_unapprovedReviews > 0 ) ? \intval( $oldContainer->_unapprovedReviews - $this->mapped('unapproved_reviews') ) : 0;
}
/* Make a link */
@@ -2109,7 +2130,7 @@ abstract class _Item extends \IPS\Content
}
if ( isset( static::$commentClass ) and $container->_unapprovedComments !== NULL and isset( static::$databaseColumnMap['unapproved_comments'] ) )
{
$container->_unapprovedComments = ( $container->_unapprovedComments + $this->mapped('unapproved_comments') );
$container->_unapprovedComments = ( $container->_unapprovedComments >= 0 ) ? ( $container->_unapprovedItems + $this->mapped('unapproved_comments') ) : 0;
}
if ( isset( static::$reviewClass ) and $this->container()->_unapprovedReviews !== NULL and isset( static::$databaseColumnMap['unapproved_reviews'] ) )
{
@@ -2132,7 +2153,7 @@ abstract class _Item extends \IPS\Content
{
if ( $this instanceof \IPS\Content\Item AND ( isset( static::$commentClass ) OR isset( static::$reviewClass ) ) )
{
\IPS\Content\Search\Index::i()->index( ( static::$firstCommentRequired ) ? $this->firstComment() : $this );
\IPS\Content\Search\Index::i()->index( ( static::$firstCommentRequired and $this->firstComment() ) ? $this->firstComment() : $this );
\IPS\Content\Search\Index::i()->indexSingleItem( $this );
}
else
@@ -2159,9 +2180,6 @@ abstract class _Item extends \IPS\Content
{
$link->markRead();
}
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
}
/**
@@ -2732,7 +2750,7 @@ abstract class _Item extends \IPS\Content
{
if ( $this->container()->_comments !== NULL )
{
$this->container()->setLastComment();
$this->container()->setLastComment( null, $this );
$this->container()->save();
}
@@ -2848,7 +2866,7 @@ abstract class _Item extends \IPS\Content
}
if ( $this->container()->_unapprovedComments !== NULL )
{
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments - $this->mapped('unapproved_comments') );
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments > 0 ) ? ( $this->container()->_unapprovedComments - $this->mapped('unapproved_comments') ) : 0;
}
$this->container()->save();
} catch ( \BadMethodCallException $e ) {}
@@ -3038,8 +3056,6 @@ abstract class _Item extends \IPS\Content
$setComment = FALSE;
if ( isset( static::$commentClass ) and static::$firstCommentRequired )
{
$commentClass = static::$commentClass;
if ( isset( static::$databaseColumnMap['first_comment_id'] ) AND $comment = $this->firstComment() )
{
$comment->changeAuthor( $newAuthor, $log );
@@ -3051,7 +3067,7 @@ abstract class _Item extends \IPS\Content
/* Update container, but don't bother if we just updated the comment because it will have triggered the container to update */
if ( !$setComment AND $container = $this->containerWrapper() )
{
$container->setLastComment();
$container->setLastComment( updatedItem: $this );
$container->setLastReview();
$container->save();
}
@@ -3159,11 +3175,16 @@ abstract class _Item extends \IPS\Content
{
$containerWhere = array_merge( $containerWhere, $value );
unset( $where[ $key ] );
/* $containerWhere is used for exclusion purposes now,
so we leave this as part of the where condition as well. */
$where = array_merge( $where, $value );
}
}
}
/* Exclude hidden items */
$includeAdditionalApprovalClauses = true;
if( $includeHiddenItems === \IPS\Content\Hideable::FILTER_AUTOMATIC )
{
$containersTheUserCanViewHiddenItemsIn = static::canViewHiddenItemsContainers( $member );
@@ -3199,6 +3220,8 @@ abstract class _Item extends \IPS\Content
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'];
$where[] = array( "{$col}=1" );
}
$includeAdditionalApprovalClauses = false;
}
elseif ( \in_array( 'IPS\Content\Hideable', class_implements( \get_called_class() ) ) and $includeHiddenItems !== \IPS\Content\Hideable::FILTER_SHOW_HIDDEN )
{
@@ -3226,6 +3249,8 @@ abstract class _Item extends \IPS\Content
{
$where[] = array( "{$col}=1" );
}
$includeAdditionalApprovalClauses = false;
}
elseif ( isset( static::$databaseColumnMap['hidden'] ) )
{
@@ -3247,6 +3272,8 @@ abstract class _Item extends \IPS\Content
{
$where[] = array( "{$col}=0" );
}
$includeAdditionalApprovalClauses = false;
}
}
else
@@ -3274,17 +3301,21 @@ abstract class _Item extends \IPS\Content
}
}
/* No matter if we can or cannot view hidden items, we do not want these to show: -2 is queued for deletion and -3 is posted before register */
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'];
$where[] = array( "{$col}!=-2 AND {$col} !=-3" );
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
$where[] = array( "{$col}!=-2 AND {$col}!=-3" );
}
/* This only makes sense if we have not already filtered by a single value */
if( $includeAdditionalApprovalClauses )
{
/* No matter if we can or cannot view hidden items, we do not want these to show: -2 is queued for deletion and -3 is posted before register */
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'];
$where[] = array( "{$col}!=-2 AND {$col} !=-3" );
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
$where[] = array( "{$col}!=-2 AND {$col}!=-3" );
}
}
/* Future items? */
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) )
@@ -3321,18 +3352,28 @@ abstract class _Item extends \IPS\Content
$categories = array();
$lookupKey = md5( $containerClass::$permApp . $containerClass::$permType . $permissionKey . json_encode( $member->groups ) );
/* EME: Switch to use exclusion instead of inclusion */
if( !isset( static::$permissionSelect[ $lookupKey ] ) )
{
static::$permissionSelect[ $lookupKey ] = array();
$permQuery = \IPS\Db::i()->select( 'perm_type_id', 'core_permission_index', array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND (" . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' )" ) );
$permQueryJoinContainer = (bool) ( \count( $containerWhere ) );
$clubWhere = "";
/* If we cannot access clubs, skip them */
if ( \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) AND !$member->canAccessModule( \IPS\Application\Module::get( 'core', 'clubs', 'front' ) ) )
{
$containerWhere[] = array( $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::clubIdColumn() . ' IS NULL' );
$permQueryWhere = array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND (" . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::clubIdColumn() . " IS NOT NULL OR !(" . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' ))" );
$permQueryJoinContainer = true;
}
else
{
$permQueryWhere = array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND !(" . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' )" );
}
$permQuery = \IPS\Db::i()->select( 'perm_type_id', 'core_permission_index', $permQueryWhere );
if ( \count( $containerWhere ) )
if( $permQueryJoinContainer )
{
$permQuery->join( $containerClass::$databaseTable, array_merge( $containerWhere, array( 'core_permission_index.perm_type_id=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), 'STRAIGHT_JOIN' );
}
@@ -3347,11 +3388,7 @@ abstract class _Item extends \IPS\Content
if( \count( $categories ) )
{
$where[] = array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['container'] . ' IN(' . implode( ',', $categories ) . ')' );
}
else
{
$where[] = array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['container'] . '=0' );
$where[] = array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['container'] . ' NOT IN (' . implode( ',', $categories ) . ')' );
}
}
@@ -3363,8 +3400,10 @@ abstract class _Item extends \IPS\Content
$select = \IPS\Db::i()->select( 'COUNT(*) as cnt', static::$databaseTable, $where, NULL, NULL, $groupBy, NULL, $queryFlags );
if ( $joinContainer AND isset( static::$containerNodeClass ) )
{
/* EME: Removed the $containerWhere from the join because it is now in the where clause.
We are now using $containerWhere to exclude forums that shouldn't be visible. */
$containerClass = static::$containerNodeClass;
$select->join( $containerClass::$databaseTable, array_merge( $containerWhere, array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ) );
$select->join( $containerClass::$databaseTable, array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) );
}
if ( $joinComments )
{
@@ -4290,48 +4329,7 @@ abstract class _Item extends \IPS\Content
/* If we have a cutoff with no offset explicitly defined, we should not automatically generate one for pagination since our results will be limited */
$offset = ( $cutoff and $offset === NULL ) ? 0 : ( $offset !== NULL ? $offset : ( ( $_pageValue - 1 ) * $limit ) );
$ids = array();
/* If we are ordering by (date) DESC/ASC and only fetching one result, we are trying to find the first/last comment. This can be sped up greatly by selecting MIN(id) or MAX(id) instead of running the normal query, which often results in a filesort */
if( $limit == 1 AND !$offset AND mb_strtolower( $order ) == mb_strtolower( $class::$databasePrefix . $class::$databaseColumnMap['date'] . ' desc' ) )
{
try
{
$where = array( array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=(?)', $class::db()->select( 'MAX(' . $class::$databasePrefix . $class::$databaseColumnId . ')', $class::$databaseTable, $where ) ) );
$offset = 0;
$order = NULL;
}
catch( \UnderflowException $e ){}
}
elseif( $limit == 1 AND !$offset AND mb_strtolower( $order ) == mb_strtolower( $class::$databasePrefix . $class::$databaseColumnMap['date'] . ' asc' ) )
{
try
{
$where = array( array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=(?)', $class::db()->select( 'MIN(' . $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . ')', $class::$databaseTable, $where ) ) );
$offset = 0;
$order = NULL;
}
catch( \UnderflowException $e ){}
}
/* Large topics, private messages, etc. benefit greatly from splitting the query into two queries */
elseif ( $this->mapped('num_comments') >= 500 )
{
/* Its more efficient to just get the primary ids first without joins and then we can fetch all the data on the second query once the offset gets larger */
if( method_exists( $this, 'forceIndexForPaginatedIds' ) and $this->forceIndexForPaginatedIds() and $offset < 20000 )
{
/* Only use the index if we're not at a massive offset */
$ids = iterator_to_array( $class::db()->select( $class::$databasePrefix . $class::$databaseColumnId, $class::$databaseTable, $where, $order, array($offset, $limit) )->forceIndex( $this->forceIndexForPaginatedIds() )->setKeyField( $class::$databasePrefix . $class::$databaseColumnId ) );
}
else
{
$ids = iterator_to_array( $class::db()->select( $class::$databasePrefix . $class::$databaseColumnId, $class::$databaseTable, $where, $order, array($offset, $limit) )->setKeyField( $class::$databasePrefix . $class::$databaseColumnId ) );
}
$where = array( array( $class::db()->in( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId, array_keys( $ids ) ) ) );
$offset = 0;
$order = NULL;
}
/* Construct the query */
$results = array();
$bits = \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS;
@@ -4369,21 +4367,7 @@ abstract class _Item extends \IPS\Content
$results[ $result->$commentIdColumn ] = $result;
}
}
/* If we used two queries, ensure $result is sorted by the order of $ids */
if ( \count( $ids ) )
{
$newResults = array();
foreach( $ids as $k => $v )
{
$newResults[ $k ] = $results[ $k ];
}
$results = $newResults;
unset($newResults);
}
/* Get the reputation stuff now so we don 't have to do lots of queries later */
if ( \IPS\Settings::i()->reputation_enabled AND \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) AND \count( $results ) AND \IPS\Dispatcher::hasInstance() )
{
@@ -4983,7 +4967,7 @@ abstract class _Item extends \IPS\Content
$app = static::$application ?? null;
$idColumn = static::$databaseColumnId;
$dataLayer = array(
'author_id' => \IPS\core\DataLayer::i()->getSsoId( $this->author()->member_id ),
'author_id' => $this->author()->member_id,
'author_name' => $this->author()->real_name ?: null,
'content_age' => $this->mapped( 'date' ) ? \intval( floor( ( time() - $this->mapped( 'date' ) ) / 86400 ) ) : null,
'content_container_id' => null,
@@ -5100,7 +5084,7 @@ abstract class _Item extends \IPS\Content
$return['guest_name'] = new \IPS\Helpers\Form\Text( 'guest_name', NULL, FALSE, array( 'minLength' => \IPS\Settings::i()->min_user_name_length, 'maxLength' => \IPS\Settings::i()->max_user_name_length, 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('comment_guest_name') ) );
}
}
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' and \IPS\Settings::i()->guest_captcha )
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' )
{
$return['captcha'] = new \IPS\Helpers\Form\Captcha;
}
@@ -5168,7 +5152,7 @@ abstract class _Item extends \IPS\Content
'follow_area' => $followArea,
'follow_rel_id' => $this->$idColumn,
'follow_member_id' => \IPS\Member::loggedIn()->member_id,
'follow_is_anon' => 0,
'follow_is_anon' => isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : 0,
'follow_added' => time() + 1, // Make sure streams show follows after content is created
'follow_notify_do' => 1,
'follow_notify_meta' => '',
@@ -5192,13 +5176,15 @@ abstract class _Item extends \IPS\Content
{
if ( static::$firstCommentRequired and !$comment->isFirst() )
{
$commentClass = static::$commentClass;
\IPS\Content\Search\Index::i()->index( $this->firstComment() );
}
else
{
\IPS\Content\Search\Index::i()->index( $this );
/* The first comment might have been pruned, so check the time frame and reindex if necessary */
if( \IPS\Settings::i()->search_method == 'mysql' and \IPS\Settings::i()->search_index_timeframe )
{
$cutoff = \IPS\DateTime::create()->sub( new \DateInterval( 'P' . \IPS\Settings::i()->search_index_timeframe . 'D' ) )->getTimestamp();
if( $this->mapped( 'date' ) <= $cutoff )
{
\IPS\Content\Search\Index::i()->index( $this->firstComment() );
}
}
}
}
@@ -5241,7 +5227,7 @@ abstract class _Item extends \IPS\Content
$form->add( new \IPS\Helpers\Form\Text( 'guest_name', NULL, FALSE, array( 'minLength' => \IPS\Settings::i()->min_user_name_length, 'maxLength' => \IPS\Settings::i()->max_user_name_length, 'placeholder' => \IPS\Member::loggedIn()->language()->addToStack('comment_guest_name') ) ) );
}
}
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' and \IPS\Settings::i()->guest_captcha )
if ( \IPS\Settings::i()->bot_antispam_type !== 'none' )
{
$form->add( new \IPS\Helpers\Form\Captcha );
}
@@ -5488,7 +5474,7 @@ abstract class _Item extends \IPS\Content
* Can Comment/Review
*
* @param string $type Type
* @param \IPS\Member\NULL $member The member (NULL for currently logged in member)
* @param \IPS\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
*/
@@ -5924,9 +5910,11 @@ abstract class _Item extends \IPS\Content
/**
* Resync last comment
*
* @param Comment|null $comment The comment
*
* @return void
*/
public function resyncLastComment()
public function resyncLastComment( \IPS\Content\Comment $comment = NULL )
{
if( !isset( static::$commentClass ) )
{
@@ -5942,6 +5930,12 @@ abstract class _Item extends \IPS\Content
$resync = TRUE;
}
}
/* If this comment isn't newer than the cached value, we don't need to do anything */
if( $resync AND $comment !== NULL AND isset( static::$databaseColumnMap['last_comment'] ) AND $comment->mapped('date') < $this->mapped('last_comment' ) )
{
return;
}
if ( $resync )
{
@@ -5955,7 +5949,6 @@ abstract class _Item extends \IPS\Content
{
throw new \UnderflowException;
}
if ( isset( static::$databaseColumnMap['last_comment'] ) )
{
$lastCommentField = static::$databaseColumnMap['last_comment'];
@@ -6044,7 +6037,7 @@ abstract class _Item extends \IPS\Content
*
* @return void
*/
public function resyncLastReview()
public function resyncLastReview( \IPS\Content\Comment $comment = NULL )
{
if( !isset( static::$reviewClass ) )
{
@@ -6060,6 +6053,12 @@ abstract class _Item extends \IPS\Content
$resync = TRUE;
}
}
/* If this comment isn't newer than the cached value, we don't need to do anything */
if( $resync AND $comment !== NULL AND isset( static::$databaseColumnMap['last_review'] ) AND $comment->mapped('date') < static::$databaseColumnMap['last_review'] )
{
return;
}
if ( $resync )
{
@@ -6152,19 +6151,12 @@ abstract class _Item extends \IPS\Content
* @param bool $includeItems If TRUE, items will be included (this should usually be true)
* @param bool $includeComments If TRUE, comments will be included
* @param bool $includeReviews If TRUE, reviews will be included
* @param int $depth Used to keep track of current depth to avoid going too deep
* @return int|NULL|string When depth exceeds 10, will return "NULL" and initial call will return something like "100+"
* @note This method may return something like "100+" if it has lots of children to avoid exahusting memory. It is intended only for display use
* @param int $depth Used to keep track of current depth
* @return int
* @note This method includes counts of hidden and unapproved content items as well
*/
public static function contentCount( \IPS\Node\Model $container, $includeItems=TRUE, $includeComments=FALSE, $includeReviews=FALSE, $depth=0 )
{
/* Are we in too deep? */
if ( $depth > 3 )
{
return '+';
}
/* Generate a key */
$_key = md5( \get_class( $container ) . $container->_id );
@@ -6256,19 +6248,10 @@ abstract class _Item extends \IPS\Content
}
/* Add Children */
$childDepth = $depth++;
$childDepth = ++$depth;
foreach ( $container->children() as $child )
{
$toAdd = static::contentCount( $child, $includeItems, $includeComments, $includeReviews, $childDepth );
if ( \is_string( $toAdd ) )
{
return $count . '+';
}
else
{
$count += $toAdd;
}
$count += static::contentCount( $child, $includeItems, $includeComments, $includeReviews, $childDepth );
}
return $count;
}
@@ -6413,15 +6396,32 @@ abstract class _Item extends \IPS\Content
$anonymous ? (bool) \IPS\Member::loggedIn()->modPermissions() : FALSE;
$lastDate = NULL;
$lowestCommentDate = 0;
$highestCommentDate = $this->isLastPage() ? time() : 0; // If we're on the last page, we want any events from the earliest comment to now, not just the last post
/* Get the lowest and highest possible timestamps from $comments */
foreach( $comments as $comment )
{
$commentDate = $comment->mapped( 'date' );
if( $lowestCommentDate === 0 || $commentDate < $lowestCommentDate )
{
$lowestCommentDate = $commentDate;
}
if( $commentDate > $highestCommentDate )
{
$highestCommentDate = $commentDate;
}
}
/* Get moderation items */
$idColumn = static::$databaseColumnId;
$moderationItems = array();
if ( $this->showCommentMeta('moderation') )
{
foreach ( \IPS\Db::i()->select( '*', 'core_moderator_logs', array('class=? AND item_id=?', \get_class( $this ), $this->$idColumn), 'ctime ASC' ) as $row )
foreach ( \IPS\Db::i()->select( '*', 'core_moderator_logs', array('class=? AND ( ctime BETWEEN ? AND ? ) AND item_id=?', \get_class( $this ), $lowestCommentDate - 1, $highestCommentDate + 1, $this->$idColumn), 'ctime ASC' ) as $row )
{
if ( \in_array( $row['lang_key'], array('modlog__action_unfeature', 'modlog__action_feature', 'modlog__action_unlock', 'modlog__action_lock', 'modlog__action_unpin', 'modlog__action_pin', 'modlog__item_edit', 'modlog__comment_edit_title') ) )
{
@@ -6447,7 +6447,7 @@ abstract class _Item extends \IPS\Content
{
$modActionDate = $data['ctime'];
if ( ( $modActionDate > $commentDate and $modActionDate < $nextCommentDate ) or ( $modActionDate > $commentDate and !$next ) )
if ( ( $modActionDate >= $commentDate and $modActionDate <= $nextCommentDate ) or ( $modActionDate >= $commentDate and !$next ) )
{
$commentDate = $modActionDate;
$blurb = NULL;
@@ -6456,7 +6456,15 @@ abstract class _Item extends \IPS\Content
/* Edits always get their own meta action bubble, so handle those individually */
if ( \in_array( $data['lang_key'], array( 'modlog__item_edit', 'modlog__comment_edit_title' ) ) )
{
$note = array_keys( json_decode( $data['note'], TRUE ) );
$modNotes = json_decode( $data['note'], TRUE );
if( $modNotes )
{
$note = array_keys( $modNotes );
}
else
{
$note = array();
}
if( ( \count( $note ) == 4 && $data['lang_key'] == 'modlog__item_edit' ) || ( \count( $note ) == 3 && $data['lang_key'] == 'modlog__comment_edit_title' ) )
{
@@ -7338,6 +7346,11 @@ abstract class _Item extends \IPS\Content
{
$container = $this->container();
if( $container->_unapprovedItems !== NULL and $approving )
{
$container->_unapprovedItems = ( $container->_unapprovedItems > 0 ) ? ( $container->_unapprovedItems - 1 ) : 0;
}
if ( ! $this->isFutureDate() )
{
$container->resetCommentCounts();
@@ -7859,7 +7872,7 @@ abstract class _Item extends \IPS\Content
}
$unreadCount = \IPS\Db::i()->select(
'COUNT(*) as count',
'COUNT(' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnId . ') as count',
static::$databaseTable,
$whereClause
)->first();
@@ -8171,7 +8184,7 @@ abstract class _Item extends \IPS\Content
{
if ( \IPS\Settings::i()->tags_alphabetical )
{
natcasesort( $this->tags['tags'] );
sort( $this->tags['tags'], SORT_NATURAL | SORT_FLAG_CASE );
}
return $this->tags['tags'];
@@ -8212,10 +8225,11 @@ abstract class _Item extends \IPS\Content
{
$set = array( $set );
}
$insert = [];
foreach ( $set as $key => $tag )
{
\IPS\Db::i()->insert( 'core_tags', array(
$insert[] = array(
'tag_aai_lookup' => $aaiLookup,
'tag_aap_lookup' => $aapLookup,
'tag_meta_app' => static::$application,
@@ -8226,7 +8240,7 @@ abstract class _Item extends \IPS\Content
'tag_added' => time(),
'tag_prefix' => $key === 'prefix',
'tag_text' => $tag
), TRUE );
);
if ( $key === 'prefix' )
{
@@ -8237,6 +8251,11 @@ abstract class _Item extends \IPS\Content
$this->tags['tags'][] = $tag;
}
}
if( \count( $insert ) )
{
\IPS\Db::i()->insert( 'core_tags', $insert, TRUE );
}
\IPS\Db::i()->insert( 'core_tags_cache', array(
'tag_cache_key' => $aaiLookup,
@@ -8260,12 +8279,6 @@ abstract class _Item extends \IPS\Content
}
}
/* Add to search index */
if ( $this instanceof \IPS\Content\Searchable )
{
\IPS\Content\Search\Index::i()->index( $this );
}
/* Callback once tags are updated */
$this->processAfterTagUpdate();
}
@@ -8670,6 +8683,13 @@ abstract class _Item extends \IPS\Content
$this->$future = 0;
}
$dateColumn = static::$databaseColumnMap['date'];
if ( \is_array( $dateColumn ) )
{
$dateColumn = array_pop( $dateColumn );
}
$this->$dateColumn = time();
if ( isset( static::$databaseColumnMap['last_comment'] ) or isset( static::$databaseColumnMap['last_review'] ) )
{
$lastCommentField = static::$databaseColumnMap['last_comment'];
@@ -9684,4 +9704,30 @@ abstract class _Item extends \IPS\Content
return $filters;
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string|null title Title ( if available )
* @apiresponse \IPS\Node\Model container Container
* @apiresponse \IPS\Member author The member that created the item
* @apiresponse datetime date Date
* @apiresponse string content Content
* @apiresponse string url URL
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->id,
'title' => $this->title ? $this->title : NULL,
'container' => $this->containerWrapper()?->apiOutput( $authorizedMember ),
'author' => $this->author()->apiOutput( $authorizedMember ),
'date' => \IPS\DateTime::ts( $this->date )->rfc3339(),
'content' => \IPS\Text\Parser::removeLazyLoad( $this->content() ),
'url' => (string) $this->url()
);
}
}