Version 4.7.13

This commit is contained in:
Neo committed 2025-12-19 16:14:56 -08:00
1 parent fd22d99e69
commit 7124a02564
954 files changed
+29277 -59308

No files matched your search

+1 -1
View File
@@ -129,7 +129,7 @@ class _ItemController extends \IPS\Api\Controller
}
else
{
$where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['poll'] . "=0" );
$where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['poll'] . " IS NULL" );
}
}
+17 -5
View File
@@ -240,7 +240,10 @@ abstract class _Comment extends \IPS\Content
if ( !$obj->hidden() and ( !$first or !$item::$firstCommentRequired ) )
{
$obj->sendNotifications();
$member->achievementAction( 'core', 'Comment', $obj );
if( !$item instanceof \IPS\core\Messenger\Conversation )
{
$member->achievementAction( 'core', 'Comment', $obj );
}
}
else if( $obj->hidden() === 1 )
{
@@ -467,7 +470,7 @@ abstract class _Comment extends \IPS\Content
{
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
}
catch( \RedisException $e ) { }
catch( \BadMethodCallException | \RedisException $e ) { }
}
/* Was it moderated? Let's see why. */
@@ -626,7 +629,7 @@ abstract class _Comment extends \IPS\Content
return FALSE;
}
if ( $member->member_id )
if ( $member->member_id AND $this->canView( $member ) )
{
$item = $this->item();
@@ -1112,6 +1115,9 @@ abstract class _Comment extends \IPS\Content
{
\IPS\core\IndexNow::addUrlsToQueue( $noIndexUrls );
}
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
}
/**
@@ -1264,7 +1270,7 @@ abstract class _Comment extends \IPS\Content
/* Update last comment/review data for container and item */
try
{
if ( $item !== NULL AND \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
if ( $item !== NULL AND $item->container() !== NULL AND \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
{
if ( $item->container()->_reviews !== NULL )
{
@@ -1300,7 +1306,7 @@ abstract class _Comment extends \IPS\Content
}
/* Update mappings */
if ( \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Node\Statistics' ) )
if ( $item !== NULL AND $item->container() !== NULL AND \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Node\Statistics' ) )
{
$item->container()->rebuildPostedIn( array( $item->$itemIdColumn ), array( $this->author() ) );
}
@@ -1556,6 +1562,12 @@ abstract class _Comment extends \IPS\Content
}
}
/* Finally, we do not want posts from future items */
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( $itemClass ) ) )
{
$itemWhere[] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['is_future_entry'] . '=0' );
}
if ( $joinContainer AND isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
+40 -7
View File
@@ -411,6 +411,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
catch( \OutOfRangeException $e ) { }
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
parent::delete();
$this->expireWidgetCaches();
@@ -1210,10 +1213,13 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
}
catch( \BadMethodCallException $e ) {}
catch( \OutOfRangeException $e ) {}
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
}
/**
@@ -1387,6 +1393,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
}
catch( \OutOfRangeException $e ) { }
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
}
/**
@@ -1876,7 +1885,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( \count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $mem )
{
$memberObj = \IPS\Member::constructFromData( $mem );
if( $this->canView( $memberObj ) )
/* Members may have individual member level mod permissions, but are also in a group that has moderator permissions. In this case, the member level restrictions always win so we need to recheck those now that we have a member object. See \IPS\Member::modPermissions(). */
$perms = $memberObj->modPermissions();
$canView = $this->canView( $memberObj );
if ( $canView === TRUE and $container = $item->containerWrapper() and isset( $container::$modPerm ) )
{
if ( isset( $perms[ $container::$modPerm ] ) and $perms[ $container::$modPerm ] != '*' and $perms[ $container::$modPerm ] != -1 )
{
if ( empty( $perms[ $container::$modPerm ] ) or ! in_array( $item->mapped('container'), $perms[ $container::$modPerm ] ) )
{
$canView = FALSE;
}
}
}
if( $canView === TRUE )
{
$notification->recipients->attach( $memberObj );
}
@@ -2465,7 +2489,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/* Queue if there's lots, or just send them */
if ( $count > static::NOTIFICATIONS_PER_BATCH )
if ( $count > \IPS\NOTIFICATION_BACKGROUND_THRESHOLD )
{
$idColumn = $this::$databaseColumnId;
\IPS\Task::queue( 'core', 'Follow', array( 'class' => \get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo, 'followerCount' => $count ), 2 );
@@ -3030,11 +3054,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Log keyword usage, if any
*
* @param string $content Content/text of submission
* @param string|NULL $title Title of submission
* @param string $content Content/text of submission
* @param string|NULL $title Title of submission
* @param int|NULL $date Date of submission
* @return void
*/
public function checkKeywords( $content, $title=NULL )
public function checkKeywords( $content, $title=NULL, ?int $date = NULL )
{
/* Do we have any keywords to track? */
if( !\IPS\Settings::i()->stats_keywords )
@@ -3078,7 +3103,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
if( \in_array( $keyword, $words ) )
{
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
$date = $date ?: \IPS\DateTime::create()->getTimestamp();
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => $date, 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
}
}
}
@@ -3149,6 +3175,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
if ( $this instanceof \IPS\Content\EditHistory and $this->mapped('edit_time') and ( $this->mapped('edit_show') or \IPS\Member::loggedIn()->modPermission('can_view_editlog') ) and \IPS\Settings::i()->edit_log )
{
/* If anonymous and we can't see, it's a no. */
if ( $this->isAnonymous() AND !\IPS\Member::loggedIn()->modPermission('can_view_anonymous_posters') )
{
return NULL;
}
$template = static::$editLineTemplate[1];
return \IPS\Theme::i()->getTemplate( static::$editLineTemplate[0][0], static::$editLineTemplate[0][1], ( isset( static::$editLineTemplate[0][2] ) ) ? static::$editLineTemplate[0][2] : NULL )->$template( $this, ( isset( static::$databaseColumnMap['edit_reason'] ) and $this->mapped('edit_reason') ) );
}
@@ -3287,4 +3319,5 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return [];
}
}
+134 -49
View File
@@ -104,6 +104,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
catch( \OutOfRangeException $e ) { }
}
}
if( is_subclass_of( $class, 'IPS\Content\FuturePublishing' ) and isset( $class::$databaseColumnMap['is_future_entry'] ) AND isset( $class::$databaseColumnMap['future_date'] ) )
{
$futureColumn = $class::$databaseColumnMap['future_date'];
$futureEntry = $class::$databaseColumnMap['is_future_entry'];
if ( $item->$futureEntry == 1 and $item->$futureColumn <= time() )
{
$item->publish();
}
}
/* If this is an AJAX request (like the topic hovercard), we don't want to do any of the below like update views and mark read */
if ( \IPS\Request::i()->isAjax() and !$this->updateViewsAndMarkersOnAjax )
@@ -117,27 +127,33 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* We also want to update the views if we have a page parameter */
if( $item instanceof \IPS\Content\ReadMarkers AND isset( \IPS\Request::i()->page ) AND \IPS\Request::i()->page )
{
$idColumn = $class::$databaseColumnId;
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\ViewUpdates' ) )
{
$countUpdated = false;
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
$item->updateViews();
}
else
{
$idColumn = $class::$databaseColumnId;
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
{
try
$countUpdated = false;
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
try
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
}
catch( \Exception $e ) {}
}
if ( ! $countUpdated )
{
\IPS\Db::i()->insert( 'core_view_updates', array(
'classname' => $class,
'id' => $item->$idColumn
) );
}
catch( \Exception $e ) {}
}
if ( ! $countUpdated )
{
\IPS\Db::i()->insert( 'core_view_updates', array(
'classname' => $class,
'id' => $item->$idColumn
) );
}
}
}
@@ -231,36 +247,43 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Update Views */
$idColumn = $class::$databaseColumnId;
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\ViewUpdates' ) )
{
$countUpdated = false;
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
$item->updateViews();
}
else
{
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
{
try
$countUpdated = false;
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
try
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
}
catch( \Exception $e ) {}
}
catch( \Exception $e ) {}
}
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
try
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
/* Score by timestamp for trending */
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
try
{
/* Score by timestamp for trending */
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
}
catch( \BadMethodCallException | \RedisException $e ) {}
}
if ( ! $countUpdated )
{
\IPS\Db::i()->insert( 'core_view_updates', array(
'classname' => $class,
'id' => $item->$idColumn
) );
}
catch( \RedisException $e ) {}
}
if ( ! $countUpdated )
{
\IPS\Db::i()->insert( 'core_view_updates', array(
'classname' => $class,
'id' => $item->$idColumn
) );
}
}
@@ -361,7 +384,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->redirect( $class::getRedirectFrom( \IPS\Request::i()->id )->url(), '', 301 );
}
catch( \OutOfRangeException $e ) { }
return NULL;
}
}
@@ -1000,6 +1023,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
else
{
\IPS\Output::i()->bypassCsrfKeyCheck = TRUE;
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
return;
}
@@ -1628,7 +1652,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
/**
* Moderator Log
* Moderation Log
*
* @return void
*/
@@ -1643,11 +1667,65 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
/* Set up some stuff so we're not doing too much logic / assignment in the template */
$modlog = $item->moderationTable();
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->modLog( $item, $modlog );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/T', 404, '' );
}
catch( \DomainException $e )
{
\IPS\Output::i()->error( 'no_module_permission', '2S136/U', 403, '' );
}
}
/**
* Moderation Log
*
* @return void
*/
protected function analytics()
{
if( !\IPS\Member::loggedIn()->modPermission( 'can_view_moderation_log' ) )
{
\IPS\Output::i()->error( 'no_module_permission', '2S136/1F', 403, '' );
}
try
{
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
/* Set up some stuff so we're not doing too much logic / assignment in the template */
$lastCommenter = $members = $busy = $reacted = $images = NULL;
try
{
$lastCommenter = $item->lastCommenter();
if ( !$lastCommenter->member_id )
{
$lastCommenter = NULL;
}
}
catch( \BadMethodCallException $e ) { }
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
{
$members = $item->topPosters();
$busy = $item->popularDays();
$reacted = $item->topReactedPosts();
$images = $item->imageAttachments();
}
/* Set navigation */
$this->_setBreadcrumbAndTitle( $item );
\IPS\Output::i()->output = $item->moderationTable();
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'analytics_and_stats' ) );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->analytics( $item, $lastCommenter, $members, $busy, $reacted, $images );
}
catch ( \OutOfRangeException $e )
{
@@ -1967,9 +2045,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
if ( $values = $form->values() )
{
$review->author_response = $values['reviewResponse'];
$review->save();
$review->setResponse( $values['reviewResponse'] );
/* Claim attachments and clear the editor */
\IPS\File::claimAttachments( 'reviewResponse-' . $class::$application . '/' . $class::$module . '-' . \IPS\Request::i()->review, \IPS\Request::i()->id, \IPS\Request::i()->review, \get_class( $item ) );
\IPS\Output::i()->redirect( $review->url() );
}
@@ -4008,9 +4088,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
return NULL;
}
$class = static::$contentModel;
if( !\is_subclass_of( $class, 'IPS\Content\Item' ) )
{
return NULL;
}
try
{
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$items = [];
+110 -52
View File
@@ -41,6 +41,13 @@ abstract class _Item extends \IPS\Content
* @brief [Content\Item] Follower count
*/
public $followerCount = NULL;
/**
* Should IndexNow be skipped for this item? Can be used to prevent that Private Messages,
* Reports and other content which is never going to be visible to guests is triggering the requests.
* @var bool
*/
public static bool $skipIndexNow = FALSE;
/**
* @brief [Content\Item] If $firstCommentRequired is TRUE, when comments are split from an item or items are merged, the author
@@ -521,7 +528,7 @@ abstract class _Item extends \IPS\Content
}
/* Dish out points */
if ( !$obj->isFutureDate() and !$obj->hidden() )
if ( !$obj->isFutureDate() and !$obj->hidden() and !$obj instanceof \IPS\core\Messenger\Conversation )
{
$member->achievementAction( 'core', 'NewContentItem', $obj );
}
@@ -619,6 +626,12 @@ abstract class _Item extends \IPS\Content
{
$column = static::$databaseColumnMap['future_date'];
$this->$column = $time->getTimestamp();
if( isset( static::$databaseColumnMap['is_future_entry'] ) )
{
$column = static::$databaseColumnMap['is_future_entry'];
$this->$column = $time->getTimestamp() > time();
}
}
}
@@ -718,7 +731,12 @@ abstract class _Item extends \IPS\Content
/* Title */
if ( isset( static::$databaseColumnMap['title'] ) )
{
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ), function( $val ) {
if ( !\IPS\Member::loggedIn()->group['g_bypass_badwords'] AND $word = \IPS\core\Profanity::checkProfanityBlocks( $val ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( "form_err_word_blocked", FALSE, array( "sprintf" => array( $word ) ) ) );
}
} );
$return['title']->rowClasses[] = 'ipsFieldRow_primary';
$return['title']->rowClasses[] = 'ipsFieldRow_fullWidth';
}
@@ -1015,6 +1033,10 @@ abstract class _Item extends \IPS\Content
{
$options['autocomplete']['filterProfanity'] = TRUE;
}
if ( \IPS\Settings::i()->tags_alphabetical )
{
$options['autocomplete']['alphabetical'] = TRUE;
}
$options['autocomplete']['prefix'] = static::canPrefix( NULL, $container );
$options['autocomplete']['disallowedCharacters'] = array( '#' ); // @todo Pending \IPS\Http\Url rework, hashes cannot be used in URLs
@@ -1149,7 +1171,7 @@ abstract class _Item extends \IPS\Content
}
/* Send this URL to IndexNow if the guest can view it */
if ( $this->canView( new \IPS\Member ) )
if ( $this->canView( new \IPS\Member ) AND !static::$skipIndexNow )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
@@ -1330,6 +1352,14 @@ abstract class _Item extends \IPS\Content
$this->publish();
}
}
else if ( $container and ! $this->$column )
{
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
{
/* Was not future, now is */
$this->unpublish();
}
}
}
/* Post anonymously */
@@ -1903,7 +1933,7 @@ abstract class _Item extends \IPS\Content
}
}
if ( $this instanceof \IPS\Content\Views )
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\ViewUpdates' ) )
{
$return['num_views'] = (int) $this->mapped('views');
}
@@ -2129,6 +2159,9 @@ abstract class _Item extends \IPS\Content
{
$link->markRead();
}
/* Remove from Hive */
\IPS\core\Hive::i()->removeContent( $this );
}
/**
@@ -2275,7 +2308,7 @@ abstract class _Item extends \IPS\Content
}
/* Merge view counts */
if ( $this instanceof \IPS\Content\Views )
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\ViewUpdates' ) )
{
$views += $item->mapped('views');
\IPS\Db::i()->update( 'core_view_updates', array( 'id' => $this->$idColumn ), array( 'classname=? and id=?', (string) \get_class( $item ), $item->$idColumn ) );
@@ -2725,7 +2758,7 @@ abstract class _Item extends \IPS\Content
}
$idColumn = static::$databaseColumnId;
/* Don't do anything for shadow items */
if ( isset( static::$databaseColumnMap['moved_to'] ) )
{
@@ -2740,19 +2773,19 @@ abstract class _Item extends \IPS\Content
}
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
/* Remove any meta data */
if ( ( $this instanceof \IPS\Content\MetaData ) AND isset( static::$databaseColumnMap['meta_data'] ) )
{
$this->deleteAllMeta();
}
/* Unclaim attachments */
$this->unclaimAttachments();
/* Delete it from the database */
parent::delete();
/* Update count */
try
{
@@ -2772,7 +2805,7 @@ abstract class _Item extends \IPS\Content
}
}
} catch ( \BadMethodCallException $e ) {}
/* Delete comments */
if ( isset( static::$commentClass ) )
{
@@ -2783,7 +2816,7 @@ abstract class _Item extends \IPS\Content
{
$where = $commentClass::deleteWhereSql( $this->$idColumn );
}
/* Remove any deletion logs for comments */
$commentIds = array();
$commentIdColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnId;
@@ -2791,14 +2824,14 @@ abstract class _Item extends \IPS\Content
{
$commentIds[] = $commentId;
}
$this->deleteCommentOrReviewData( $commentIds, $commentClass );
if ( \is_array( $where ) and \count( $where ) )
{
\IPS\Db::i()->delete( $commentClass::$databaseTable, $where );
}
if( !$this->skipContainerRebuild )
{
try
@@ -2810,7 +2843,7 @@ abstract class _Item extends \IPS\Content
{
$this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') );
}
$this->container()->setLastComment();
}
if ( $this->container()->_unapprovedComments !== NULL )
@@ -2821,7 +2854,7 @@ abstract class _Item extends \IPS\Content
} catch ( \BadMethodCallException $e ) {}
}
}
/* Delete reviews */
if ( isset( static::$reviewClass ) )
{
@@ -2832,7 +2865,7 @@ abstract class _Item extends \IPS\Content
{
$where = $reviewClass::deleteWhereSql( $this->$idColumn );
}
/* Remove any deletion logs for reviews */
$reviewIds = array();
$reviewIdColumn = $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'];
@@ -2844,7 +2877,7 @@ abstract class _Item extends \IPS\Content
$this->deleteCommentOrReviewData( $reviewIds, $reviewClass );
\IPS\Db::i()->delete( $reviewClass::$databaseTable, $where );
if( !$this->skipContainerRebuild )
{
try
@@ -2856,7 +2889,7 @@ abstract class _Item extends \IPS\Content
{
$this->container()->_reviews = ( $this->container()->_reviews - $this->mapped('num_reviews') );
}
$this->container()->setLastReview();
}
if ( $this->container()->_unapprovedReviews !== NULL )
@@ -2867,7 +2900,7 @@ abstract class _Item extends \IPS\Content
} catch ( \BadMethodCallException $e ) {}
}
}
/* Delete tags */
if ( $this instanceof \IPS\Content\Tags )
{
@@ -2876,7 +2909,7 @@ abstract class _Item extends \IPS\Content
\IPS\Db::i()->delete( 'core_tags_cache', array( 'tag_cache_key=?', $aaiLookup ) );
\IPS\Db::i()->delete( 'core_tags_perms', array( 'tag_perm_aai_lookup=?', $aaiLookup ) );
}
/* Delete follows */
if ( $this instanceof \IPS\Content\Followable )
{
@@ -2884,7 +2917,7 @@ abstract class _Item extends \IPS\Content
\IPS\Db::i()->delete( 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', static::$application, $followArea, (int) $this->$idColumn ) );
\IPS\Db::i()->delete( 'core_follow_count_cache', array( 'class=? AND id=?', \get_called_class(), (int) $this->$idColumn ) );
}
/* Remove Notifications */
$memberIds = array();
@@ -2894,13 +2927,13 @@ abstract class _Item extends \IPS\Content
}
\IPS\Db::i()->delete( 'core_notifications', array( 'item_class=? AND item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
/* Delete from Our Picks */
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
/* Delete from redirect links */
\IPS\Db::i()->delete( 'core_item_redirect', array( 'redirect_class=? AND redirect_new_item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
/* Delete Polls */
if ( $this instanceof \IPS\Content\Polls and $this->getPoll() )
{
@@ -2912,19 +2945,21 @@ abstract class _Item extends \IPS\Content
{
\IPS\Db::i()->delete( 'core_ratings', array( 'class=? AND item_id=?', \get_called_class(), $this->$idColumn ) );
}
foreach( $memberIds as $member )
{
\IPS\Member::load( $member )->recountNotifications();
}
/** Item::url() can throw a LogicException exception in specific cases like when a Pages Record has no valid page */
try
if( !static::$skipIndexNow )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
try
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
catch( \LogicException $e ) {}
}
catch( \LogicException $e ) {}
}
/**
@@ -3174,7 +3209,7 @@ abstract class _Item extends \IPS\Content
if ( isset( static::$databaseColumnMap['approved'] ) )
{
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
if ( $member->member_id and $includeHiddenItems !== \IPS\Content\Hideable::FILTER_PUBLIC_ONLY )
if ( $member->member_id and $includeHiddenItems !== \IPS\Content\Hideable::FILTER_PUBLIC_ONLY and isset( static::$databaseColumnMap['author'] ) )
{
/* Only fetching a count, for a single container with a item cache count, no future publishing, and there is only one where clause (the container limitation) */
if( $countOnly === TRUE AND $skipPermission instanceof \IPS\Node\Model AND $skipPermission->_items !== NULL AND !\in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) AND \count( $where ) === 1 )
@@ -8069,7 +8104,13 @@ abstract class _Item extends \IPS\Content
*/
public static function definedTags( \IPS\Node\Model $container = NULL )
{
return \IPS\Settings::i()->tags_predefined ? explode( ',', \IPS\Settings::i()->tags_predefined ) : array();
$return = \IPS\Settings::i()->tags_predefined ? explode( ',', \IPS\Settings::i()->tags_predefined ) : array();
if ( \IPS\Settings::i()->tags_alphabetical )
{
natcasesort( $return );
}
return $return;
}
/**
@@ -8125,8 +8166,20 @@ abstract class _Item extends \IPS\Content
}
}
}
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : [] );
if ( isset( $this->tags['tags'] ) )
{
if ( \IPS\Settings::i()->tags_alphabetical )
{
natcasesort( $this->tags['tags'] );
}
return $this->tags['tags'];
}
else
{
return [];
}
}
else
{
@@ -8617,8 +8670,29 @@ abstract class _Item extends \IPS\Content
$this->$future = 0;
}
if ( isset( static::$databaseColumnMap['last_comment'] ) or isset( static::$databaseColumnMap['last_review'] ) )
{
$lastCommentField = static::$databaseColumnMap['last_comment'];
if ( \is_array( $lastCommentField ) )
{
foreach ( $lastCommentField as $column )
{
$this->$column = time();
}
}
else
{
$this->$lastCommentField = time();
}
if ( isset( static::$databaseColumnMap['last_review'] ) )
{
$lastReviewField = static::$databaseColumnMap['last_review'];
$this->$lastReviewField = time();
}
}
$this->save();
$this->onPublish( $member );
/* And update the tags perm cache */
if ( $this instanceof \IPS\Content\Tags )
@@ -8634,26 +8708,10 @@ abstract class _Item extends \IPS\Content
$comment->$date = time();
$comment->save();
if ( isset( static::$databaseColumnMap['last_comment'] ) )
{
$lastCommentField = static::$databaseColumnMap['last_comment'];
if ( \is_array( $lastCommentField ) )
{
foreach ( $lastCommentField as $column )
{
$this->$column = time();
}
}
else
{
$this->$lastCommentField = time();
}
$this->save();
}
}
$this->onPublish( $member );
/* Mark this item as read for the author */
$this->markRead( $this->author() );
+8 -1
View File
@@ -170,7 +170,7 @@ trait Reactable
{
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
}
catch( \RedisException $e ) {}
catch( \BadMethodCallException | \RedisException $e ) {}
}
}
@@ -270,6 +270,7 @@ trait Reactable
return FALSE;
}
/* Cannot react to guest content */
if ( !$owner->member_id )
{
return FALSE;
@@ -293,6 +294,12 @@ trait Reactable
return FALSE;
}
/* Unacknowledged warnings */
if ( $member->members_bitoptions['unacknowledged_warnings'] )
{
return FALSE;
}
/* Still here? All good. */
return TRUE;
}
+21
View File
@@ -650,4 +650,25 @@ abstract class _Review extends \IPS\Content\Comment
/* Nope, we cannot delete */
return FALSE;
}
/**
* Respond to a review
*
* @param string $response
* @return void
*/
public function setResponse( string $response )
{
$this->checkProfanityFilters( FALSE, (bool) $this->hasAuthorResponse(), $response );
$column = static::$databaseColumnMap['author_response'];
$this->$column = $response;
$this->save();
/* Reindex in case it was hidden by the profanity filters */
if ( $this instanceof \IPS\Content\Searchable )
{
\IPS\Content\Search\Index::i()->index( $this );
}
}
}
+1 -1
View File
@@ -396,7 +396,7 @@ class _Query extends \IPS\Content\Search\Query
{
$classes = array_merge( $object->classes, $classes );
if ( ! $object->similarContent )
if ( empty( $object->similarContent ) )
{
foreach( $object->classes as $class )
{
+6
View File
@@ -249,6 +249,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
$isLastComment = 1;
}
/* If this comment is hidden, don't actually mark as the last comment as that will cause this item to be hidden in search if we are getting only the last comment. */
if ( $isLastComment AND $object->hidden() )
{
$isLastComment = 0;
}
/* If we are re-indexing the first post of an item, which is triggered by a comment being added to a $itemClass::$firstCommentRequired,
then ensure that the isLastComment flag is not added as we index the comment first, which means the index_is_last_comment is incorrectly reset to 0 for the comment itself */
if ( $isForItem and $isLastComment )
+29 -6
View File
@@ -588,16 +588,39 @@ class _Index extends \IPS\Content\Search\Index
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL )
{
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexItemId ) );
try
{
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
/* Now reset the item index with the latest comment time */
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
}
catch( \Exception $ex ) { }
catch( \Exception )
{
/* Didn't find a latest? Was it because the entire thing is hidden? */
try
{
$index = \IPS\Db::i()->select( 'index_id, index_item_index_id, index_hidden', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
/* Is the root item hidden? */
if ( $index['index_id'] == $index['index_item_index_id'] and in_array( $index['index_hidden'], [ -1, 2 ] ) )
{
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=?', $indexItemId, $index['index_hidden'] ), 'index_date_created DESC', array( 0, 1 ) )->first();
}
else
{
/* No, so we can't find a latest item that is hidden, so skip this */
return FALSE;
}
}
catch( \Exception )
{
return FALSE;
}
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
/* Now reset the item index with the latest comment time */
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
}
/**
+6 -3
View File
@@ -922,7 +922,7 @@ class _Query extends \IPS\Content\Search\Query
/* Now, if we have 501 results, then we have to assume there are more, so the join is required */
if ( \count( $tagIds ) == 501 )
{
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN ( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) );
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN (' . \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) . ')' );
}
else
{
@@ -1031,7 +1031,7 @@ class _Query extends \IPS\Content\Search\Query
$where[] = $clause;
}
}
/* Return */
return $where;
}
@@ -1101,7 +1101,7 @@ class _Query extends \IPS\Content\Search\Query
search which is actually very efficient as MySQL performs a search on the FT index first to narrow down the results */
if ( static::termIsPhrase( $term ) )
{
$term = str_replace( array( "'", '"' ), '', $term );
$term = str_replace( array( "'", '"', '?' ), '', $term );
foreach ( static::termAsWordsArray( $term ) as $word )
{
@@ -1161,6 +1161,9 @@ class _Query extends \IPS\Content\Search\Query
/* Trailing + or -s are not allowed */
$word = rtrim( $word, '+-' );
/* ? are not allowed either */
$word = str_replace( '?', '', $word );
/* These rules only apply if we're not in a quoted phrase... */
if ( !static::termIsPhrase( $word ) )
+20 -4
View File
@@ -153,11 +153,11 @@ trait Statistics
if ( $name )
{
$subQuery = \IPS\Db::i()->select( 'core_members.member_id', 'core_members', \IPS\Db::i()->like( 'core_members.name', $name ) );
$where[] = [ $commentClass::$databaseTable . '.' . $commentClass::$databaseColumnMap['author'] . ' IN(?)', $subQuery ];
$where[] = [ $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . ' IN(?)', $subQuery ];
}
$members = iterator_to_array( \IPS\Db::i()->select(
$commentClass::$databaseColumnMap['author'],
$commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'],
$commentClass::$databaseTable,
$where,
NULL,
@@ -232,7 +232,7 @@ trait Statistics
$return = array();
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( \IPS\Db::i()->in( $commentIdField, $postIds ) ), "FIND_IN_SET( {$commentIdField}, '" . implode( ",", $postIds ) . "' )", array( 0, $count ) ), $commentClass ) as $comment )
{
if ( $comment->canView() and $comment->mapped('item') == $this->$idField )
if ( $comment->canView() and $comment->mapped('item') == $this->$idField and isset( $counts[ $comment->$commentIdField ] ) )
{
$return[] = array( 'comment' => $comment, 'count' => $counts[ $comment->$commentIdField ] );
}
@@ -402,7 +402,18 @@ trait Statistics
protected function _getVisibleWhere()
{
$commentClass = static::$commentClass;
$idField = static::$databaseColumnId;
$lookFor = 'IPS\cms\Records\CommentTopicSync';
if ( mb_substr( $commentClass, 0, mb_strlen( $lookFor ) ) === $lookFor )
{
$idField = 'record_topicid';
}
else
{
$idField = static::$databaseColumnId;
}
$commentItemField = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'];
$where = array();
@@ -417,6 +428,11 @@ trait Statistics
$where[] = array( "{$hiddenColumn} = 0" );
}
if ( $commentClass::commentWhere() !== NULL )
{
$where[] = $commentClass::commentWhere();
}
$where[] = array( $commentItemField . '=?', $this->$idField );
return $where;
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* @brief Views Trait
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 18 Feb 2013
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Views Trait
*/
trait ViewUpdates
{
/**
* Update View Count
*
* @return void
*/
public function updateViews()
{
$idColumn = static::$databaseColumnId;
$class = \get_called_class();
$countUpdated = false;
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
{
try
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $this->$idColumn );
$countUpdated = true;
}
catch( \Exception $e ) {}
}
if ( ! $countUpdated )
{
\IPS\Db::i()->insert( 'core_view_updates', array(
'classname' => $class,
'id' => $this->$idColumn
) );
}
}
}
+1
View File
@@ -22,5 +22,6 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*
* @note Content classes will gain special functionality by implementing this interface
* @note Originally we were just checking for a mapped 'views' column, but decided to use an interface for consistency
* @deprecated Use ViewUpdates trait instead.
*/
interface Views { }