Files
IPB/system/Content/Hideable.php
T
2025-12-19 19:04:57 -08:00

1968 lines
59 KiB
PHP

<?php
/**
* @brief Hidebale Trait for Content Models/Comments
* @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 6 Nov 2013
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DateInvalidOperationException;
use Exception;
use IPS\Application;
use IPS\Content\Search\SearchContent;
use IPS\Platform\Bridge;
use RuntimeException;
use BadMethodCallException;
use OutOfRangeException;
use UnderflowException;
use IPS\core\Approval;
use IPS\core\DeletionLog;
use IPS\core\IndexNow;
use IPS\core\Profanity;
use IPS\Api\Webhook;
use IPS\DateTime as IPSDateTime;
use IPS\Login;
use IPS\Member;
use IPS\Content\Search\Index;
use IPS\Db;
use IPS\IPS;
use IPS\Node\Model;
use IPS\Notification;
use IPS\Settings;
use IPS\Theme;
use IPS\Events\Event;
use function count;
use function defined;
use function get_called_class;
use function get_class;
use function in_array;
use function intval;
use function is_array;
use function substr;
use function ucfirst;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0') . ' 403 Forbidden' );
exit;
}
/**
* Hidebale Trait for Content Models/Comments
*
* @note Content classes will gain special functionality by implementing this interface
*/
trait Hideable
{
/**
* Content is hidden?
*
* @return int
* @li -3 is a post made by a guest using the "post before register" feature
* @li -2 is pending deletion
* @li -1 is hidden having been hidden by a moderator
* @li 0 is unhidden
* @li 1 is hidden needing approval
* @note The actual column may also contain 2 which means the item is hidden because the parent is hidden, but it is not hidden in itself. This method will return -1 in that case.
*
* @note A piece of content (item and comment) can have an alias for hidden OR approved.
* With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden, -2=pending deletion, -3=guest post before register
* With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator, -2=pending deletion, -3=guest post before register
*
* User posting has moderator approval set: When adding an unapproved ITEM (approved=0, hidden=1) you should *not* increment container()->_comments but you should update container()->_unapprovedItems
* User posting has moderator approval set: When adding an unapproved COMMENT (approved=0, hidden=1) you should *not* increment item()->num_comments in item or container()->_comments but you should update item()->unapproved_comments and container()->_unapprovedComments
*
* User post is hidden by moderator (approved=-1, hidden=0) you should decrement item()->num_comments and decrement container()->_comments but *not* increment item()->unapproved_comments or container()->_unapprovedComments
* User item is hidden by a moderator (approved=-1, hidden=0) you should decrement container()->comments and subtract comment count from container()->_comments, but *not* increment container()->_unapprovedComments
*
* Moderator hides item (approved=-1, hidden=-1) you should substract num_comments from container()->_comments. Comments inside item are flagged as approved=-1, hidden=2 but item()->num_comments should not be substracted from
*
* Comments with a hidden value of 2 should increase item()->num_comments but not container()->_comments
* @throws RuntimeException
*/
public function hiddenStatus(): int
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
return ( $this->$column == 2 ) ? -1 : intval( $this->$column );
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
if ( $this->$column == -2 or $this->$column == -3 )
{
return intval( $this->$column );
}
return $this->$column == -1 ? intval( $this->$column ) : intval( !$this->$column );
}
else
{
throw new RuntimeException;
}
}
/**
* Log for deletion later
*
* @param Member|false|null $member The member, NULL for currently logged in, or FALSE for no member
* @return void
*/
public function logDelete( Member|false|null $member = NULL ): void
{
if( $member === NULL )
{
$member = Member::loggedIn();
}
$originalState = $this->hidden();
/* Log it! */
$log = new DeletionLog;
$log->setContentAndMember( $this, $member );
$log->save();
$column = static::$databaseColumnMap[ 'hidden' ] ?? static::$databaseColumnMap[ 'approved' ];
$this->$column = -2;
$this->save();
$item = ( $this instanceof Comment ) ? $this->item() : $this;
if ( $this instanceof Comment )
{
$item = $this->item();
/* Update last comment stuff */
$item->resyncLastComment();
/* Update last review stuff */
$item->resyncLastReview();
/* Update number of comments */
$item->resyncCommentCounts();
/* Update number of reviews */
$item->resyncReviewCounts();
/* Save*/
$item->save();
/* Now the container */
try
{
$containerColumn = ( $originalState === 1 ) ? '_unapprovedComments' : '_comments';
if( $this instanceof Review )
{
$containerColumn = ( $originalState === 1 ) ? '_unapprovedReviews' : '_reviews';
}
if( $item->container()->$containerColumn !== null )
{
$item->container()->$containerColumn = ( $item->container()->$containerColumn > 0 ) ? ( $item->container()->$containerColumn - 1 ) : 0;
$item->container()->save();
}
}
catch( BadMethodCallException ){}
}
else
{
/* If this is an item, just update the container counts */
try
{
$containerColumn = ( $originalState === 1 ) ? '_unapprovedItems' : '_items';
if( $item->container()->$containerColumn !== null )
{
$item->container()->$containerColumn = ( $item->container()->$containerColumn > 0 ) ? ( $item->container()->$containerColumn - 1 ) : 0;
$item->container()->save();
}
}
catch( BadMethodCallException ){}
}
if ( IPS::classUsesTrait( $this, 'IPS\Content\Taggable' ) )
{
Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 0 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
}
try
{
$idColumn = static::$databaseColumnId;
if ( $this->container() AND !$this->skipContainerRebuild )
{
$this->container()->resetCommentCounts();
$this->container()->setLastComment();
$this->container()->setLastReview();
$this->container()->save();
}
/* Update mappings */
if ( $container = $this->container() and IPS::classUsesTrait( $container, 'IPS\Node\Statistics' ) )
{
if ( $this instanceof Comment )
{
$container->rebuildPostedIn( array( $this->mapped('item') ) );
}
else
{
$container->rebuildPostedIn( array($this->$idColumn) );
}
}
Approval::loadFromContent( get_called_class(), $this->$idColumn )->delete();
}
catch( BadMethodCallException|OutOfRangeException ) {}
/* Fire an event so that we know this was soft-deleted */
Event::fire( 'onStatusChange', $this, array( 'softDelete' ) );
if ( $this instanceof Review )
{
$this->_recalculateRating();
}
}
/**
* Restore Content
*
* @param bool $hidden Restore as hidden?
* @return void
*/
public function restore( bool $hidden = FALSE ): void
{
try
{
$idColumn = static::$databaseColumnId;
$log = DeletionLog::constructFromData( Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() );
}
catch( UnderflowException )
{
/* There's no deletion log record, but this shouldn't stop us from restoring */
}
/* Restoring as hidden? */
if ( $hidden )
{
$column = static::$databaseColumnMap['hidden'] ?? static::$databaseColumnMap['approved'];
$this->$column = -1;
}
else
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
$this->$column = 0;
}
else if ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = 1;
}
}
if ( IPS::classUsesTrait( $this, 'IPS\Content\Taggable' ) AND !$hidden )
{
Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
}
/* Save the changes */
$this->save();
/* Reindex the now hidden content - if this is a content item with comments or reviews, then make sure to do those too. */
if ( $this instanceof Item AND ( isset( static::$commentClass ) OR isset( static::$reviewClass ) ) )
{
Index::i()->index( ( static::$firstCommentRequired ) ? $this->firstComment() : $this );
Index::i()->indexSingleItem( $this );
}
else
{
/* Either this is a comment / review, or the item doesn't support comments or reviews, so we can just reindex it now. */
Index::i()->index( $this );
}
/* Delete the log */
if ( isset( $log ) )
{
$log->delete();
}
/* Recount the container counters */
if( $this->container() AND !$this->skipContainerRebuild )
{
$this->container()->resetCommentCounts();
$this->container()->setLastComment();
$this->container()->setLastReview();
$this->container()->save();
}
/* Fire an event so we know the content was restored */
Event::fire( 'onStatusChange', $this, array( 'restore' ) );
if ( $this instanceof Review )
{
$this->_recalculateRating();
}
}
/**
* Can restore?*
*
* @param Member|NULL $member The member, or currently logged in member
* @return bool
*/
public function canRestore( ?Member $member=NULL ): bool
{
/* Extensions go first */
if( $permCheck = Permissions::can( 'restore', $this, $member ) )
{
return ( $permCheck === Permissions::PERM_ALLOW );
}
if( !$this->actionEnabled( 'restore', $member ) )
{
return false;
}
$member = $member ?: Member::loggedIn();
return $member->modPermission('can_manage_deleted_content');
}
/**
* Hide
*
* @param Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @param string|NULL $reason Reason
* @return void
* @throws Exception
*/
public function hide( Member|null|bool $member, string|null $reason = NULL ): void
{
/* Hide any moved links that point to this item - moved_on>? is for query optimisation purposes */
if( $this instanceof Item and isset( static::$databaseColumnMap['moved_to'] ) and isset( static::$databaseColumnMap['moved_on'] ) )
{
$idColumn = static::$databaseColumnId;
$where = array(
array( static::$databasePrefix . static::$databaseColumnMap['moved_on'] . '>?', 0 ),
array( static::$databasePrefix . static::$databaseColumnMap['moved_to'] . " LIKE CONCAT( ?, '%')", $this->$idColumn . '&' )
);
if( isset( static::$databaseColumnMap['state'] ) )
{
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['state'] . '=?', 'link' );
}
foreach( Db::i()->select( '*', static::$databaseTable, $where ) as $link )
{
static::constructFromData( $link )->hide( $member, $reason );
}
}
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
}
else
{
throw new RuntimeException;
}
/* Already hidden? */
if( $this->$column == -1 )
{
return;
}
if ( $this instanceof Item )
{
$idColumn = static::$databaseColumnId;
Db::i()->delete( 'core_notifications', array( 'item_class=? AND item_id=?', get_class( $this ), (int) $this->$idColumn ) );
if( SearchContent::isSearchable( $this ) )
{
if ( isset( static::$commentClass ) OR isset( static::$reviewClass ) )
{
if ( isset( static::$commentClass ) )
{
$commentClass = static::$commentClass;
if ( IPS::classUsesTrait( $commentClass, 'IPS\Content\Hideable' ) AND isset( $commentClass::$databaseColumnMap['hidden'] ) )
{
Db::i()->update( $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] => 2 ), array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=? AND ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . '=?', $this->$idColumn, 0 ) );
}
}
if ( isset( static::$reviewClass ) )
{
$reviewClass = static::$reviewClass;
if ( IPS::classUsesTrait( $reviewClass, 'IPS\Content\Hideable' ) AND isset( $reviewClass::$databaseColumnMap['hidden'] ) )
{
Db::i()->update( $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['hidden'] => 2 ), array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=? AND ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['hidden'] . '=?', $this->$idColumn, 0 ) );
}
}
$firstComment = NULL;
if ( static::$firstCommentRequired AND isset( static::$commentClass ) AND $firstComment = $this->firstComment() )
{
$commentClass = static::$commentClass;
if( isset( $commentClass::$databaseColumnMap['hidden'] ) )
{
$firstColumn = $commentClass::$databaseColumnMap['hidden'];
$firstComment->$firstColumn = 2;
}
Index::i()->index( $firstComment );
}
}
}
}
$this->$column = -1;
$this->save();
$this->onHide( $member );
$idColumn = static::$databaseColumnId;
if ( static::$hideLogKey )
{
Db::i()->delete( 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) );
Db::i()->insert( 'core_soft_delete_log', array(
'sdl_obj_id' => $this->$idColumn,
'sdl_obj_key' => static::$hideLogKey,
'sdl_obj_member_id' => $member === FALSE ? 0 : intval( $member ? $member->member_id : Member::loggedIn()->member_id ),
'sdl_obj_date' => time(),
'sdl_obj_reason' => $reason,
) );
}
if ( IPS::classUsesTrait( $this, 'IPS\Content\Taggable' ) )
{
Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 0 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
}
/* Update the solved index */
$idColumn = static::$databaseColumnId;
$solvedWhere = array(
array( 'app=?', static::$application )
);
if ( $this instanceof Item )
{
$item = $this;
if( isset( static::$commentClass ) )
{
$solvedWhere[] = array( 'comment_class=?', static::$commentClass );
}
$solvedWhere[] = array( 'item_id=?', $this->$idColumn );
}
else
{
$item = $this->item();
$solvedWhere[] = array( 'comment_class=?', get_called_class() );
$solvedWhere[] = array( 'comment_id=?', $this->$idColumn );
}
Db::i()->update( 'core_solved_index', [ 'hidden' => 1 ], $solvedWhere );
if( IPS::classUsesTrait( $item, Helpful::class ) )
{
$item->recountHelpfuls();
}
/* Update search index */
if( SearchContent::isSearchable( $this ) )
{
Index::i()->index( $this );
if ( $this instanceof Item )
{
/* Now re-index all the reviews and comments */
Index::i()->indexSingleItem( $this );
}
}
/* Clear the item stats */
if ( IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
{
try
{
$item->clearCachedStatistics( TRUE );
}
catch ( Exception )
{
/* Sssh */
}
}
$this->expireWidgetCaches();
$this->adjustSessions();
try
{
Approval::loadFromContent( get_called_class(), $this->$idColumn )->delete();
}
catch( OutOfRangeException ) { }
}
/**
* Unhide
*
* @param 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|null|bool $member ): void
{
/* Update our comments first - this is so that when onUnhide is called in the parent, then these posts will be accounted for when comment counts are reset */
if ( $this instanceof Item )
{
$idColumn = static::$databaseColumnId;
/* Unhide any moved links that point to this item - moved_on>? is for query optimisation purposes */
if( isset( static::$databaseColumnMap['moved_to'] ) and isset( static::$databaseColumnMap['moved_on'] ) )
{
$where = array(
array( static::$databasePrefix . static::$databaseColumnMap['moved_on'] . '>?', 0 ),
array( static::$databasePrefix . static::$databaseColumnMap['moved_to'] . " LIKE CONCAT( ?, '%')", $this->$idColumn . '&' )
);
if( isset( static::$databaseColumnMap['state'] ) )
{
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['state'] . '=?', 'link' );
}
foreach( Db::i()->select( '*', static::$databaseTable, $where ) as $link )
{
static::constructFromData( $link )->unhide( $member );
}
}
foreach ( array( 'commentClass', 'reviewClass' ) as $class )
{
if ( isset( static::$$class ) )
{
$className = static::$$class;
if ( IPS::classUsesTrait( $className, 'IPS\Content\Hideable' ) AND isset( $className::$databaseColumnMap['hidden'] ) )
{
Db::i()->update( $className::$databaseTable, array( $className::$databasePrefix . $className::$databaseColumnMap['hidden'] => 0 ), array( $className::$databasePrefix . $className::$databaseColumnMap['item'] . '=? AND ' . $className::$databasePrefix . $className::$databaseColumnMap['hidden'] . '=?', $this->$idColumn, 2 ) );
}
}
}
}
/* If we're approving, we have to do extra stuff */
$approving = FALSE;
$pbr = FALSE;
if ( $this->hidden() === 1 )
{
$approving = TRUE;
if ( isset( static::$databaseColumnMap['approved_by'] ) and $member !== FALSE )
{
$column = static::$databaseColumnMap['approved_by'];
$this->$column = $member ? $member->member_id : Member::loggedIn()->member_id;
}
if ( isset( static::$databaseColumnMap['approved_date'] ) )
{
$column = static::$databaseColumnMap['approved_date'];
$this->$column = time();
}
}
elseif( $this->hidden() === -3 )
{
$pbr = TRUE;
}
/* Now do the actual stuff */
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
/* Already approved? */
if( $this->$column == 0 )
{
return;
}
$this->$column = 0;
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
/* Already approved? */
if( $this->$column == 1 )
{
return;
}
$this->$column = 1;
}
else
{
throw new RuntimeException;
}
$this->save();
$this->onUnhide( ( $approving OR ( $pbr AND $this->hidden() === 0 ) ), $member );
/* Update container if needed */
if( $this instanceof Item )
{
try
{
if ( $this->container()->_comments !== NULL )
{
$this->container()->setLastComment( null, $this );
$this->container()->save();
}
if ( $this->container()->_reviews !== NULL )
{
$this->container()->setLastReview();
$this->container()->save();
}
} catch ( BadMethodCallException ) {}
}
$idColumn = static::$databaseColumnId;
if ( static::$hideLogKey )
{
Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
}
/* And update the tags perm cache */
if ( IPS::classUsesTrait( $this, 'IPS\Content\Taggable' ) )
{
Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
}
/* Update report center stuff */
if ( IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
{
$this->moderated( 'unhide' );
}
/* Send notifications if necessary */
if ( ( $approving OR ( $pbr AND $this->hidden() === 0 ) ) )
{
$this->sendApprovedNotification();
}
/* Award points */
$itemClass = in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) ? static::$itemClass : get_called_class();
if ( $this instanceof Item )
{
$this->author()->achievementAction( 'core', 'NewContentItem', $this );
}
elseif ( $this instanceof Review )
{
$this->author()->achievementAction( 'core', 'Review', $this );
}
elseif ( $this instanceof Comment and ( ( $itemClass::$firstCommentRequired AND ! $this->isFirst() ) OR ( ! $itemClass::$firstCommentRequired ) ) )
{
$this->author()->achievementAction( 'core', 'Comment', $this );
}
/* Update solved index */
if ( $this instanceof Item )
{
$item = $this;
if( isset( static::$commentClass ) )
{
$solvedWhere[] = array( 'comment_class=?', static::$commentClass );
}
$solvedWhere[] = array( 'item_id=?', $this->$idColumn );
}
else
{
$item = $this->item();
$solvedWhere[] = array( 'comment_class=?', get_called_class() );
$solvedWhere[] = array( 'comment_id=?', $this->$idColumn );
}
Db::i()->update( 'core_solved_index', [ 'hidden' => 0 ], $solvedWhere );
if( IPS::classUsesTrait( $item, Helpful::class ) )
{
$item->recountHelpfuls();
}
/* Clear the item stats */
if ( IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
{
try
{
$item->clearCachedStatistics();
}
catch ( Exception )
{
/* Sssh */
}
}
/* Send webhook if necessary */
if ( $approving )
{
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_create', $this, $this->webhookFilters() );
if ( $this instanceof Item and $itemClass::$firstCommentRequired === TRUE and $firstComment = $this->firstComment() )
{
Webhook::fire( str_replace( '\\', '', substr( get_class( $firstComment ), 3 ) ) . '_create', $firstComment, $firstComment->webhookFilters() );
}
}
$this->expireWidgetCaches();
$this->adjustSessions();
try
{
Approval::loadFromContent( get_called_class(), $this->$idColumn )->delete();
}
catch( OutOfRangeException ) { }
if ( $this instanceof Item )
{
/* And then update the search index */
if( SearchContent::isSearchable( $this ) )
{
if ( isset( static::$commentClass ) OR isset( static::$reviewClass ) )
{
if( isset( static::$databaseColumnMap['state'] ) )
{
$stateColumn = static::$databaseColumnMap['state'];
if ( $this->$stateColumn == 'link' )
{
return;
}
}
$firstComment = NULL;
if ( static::$firstCommentRequired AND isset( static::$commentClass ) AND $firstComment = $this->firstComment() )
{
$className = static::$commentClass;
if( isset( $className::$databaseColumnMap['hidden'] ) )
{
$column = $className::$databaseColumnMap['hidden'];
$firstComment->$column = 0;
}
Index::i()->index( $firstComment );
}
}
}
}
/* Update search index */
if( SearchContent::isSearchable( $this ) )
{
Index::i()->index( $this );
if ( $this instanceof Item )
{
/* Now re-index all the reviews and comments */
Index::i()->indexSingleItem( $this );
}
}
}
/**
* @brief Hidden blurb cache
*/
protected ?string $hiddenBlurb = NULL;
/**
* Blurb for when/why/by whom this content was hidden
*
* @return string
*/
public function hiddenBlurb(): string
{
if( $this->hiddenBlurb === NULL )
{
try
{
$idColumn = static::$databaseColumnId;
$log = Db::i()->select( '*', 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) )->first();
if ( $log['sdl_obj_member_id'] )
{
$this->hiddenBlurb = Member::loggedIn()->language()->addToStack('hidden_blurb', FALSE, array( 'sprintf' => array( Member::load( $log['sdl_obj_member_id'] )->name, IPSDateTime::ts( $log['sdl_obj_date'] )->relative(), $log['sdl_obj_reason'] ?: Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
}
else
{
$this->hiddenBlurb = Member::loggedIn()->language()->addToStack('hidden_blurb_no_member', FALSE, array( 'sprintf' => array( IPSDateTime::ts( $log['sdl_obj_date'] )->relative(), $log['sdl_obj_reason'] ?: Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
}
}
catch ( UnderflowException )
{
/* If this is requiring approval and has a logged reason */
$hidden = $this->hidden();
$item = NULL;
if ( ( $this instanceof Comment ) )
{
$item = $this->item();
/* If this is the first comment, and it's required, then we need to check the items hidden status instead */
if ( $item::$firstCommentRequired AND $this->isFirst() AND $hidden !== 1 )
{
$hidden = $this->item()->hidden();
}
}
if ( $item and $hidden === 1 AND IPS::classUsesTrait( $item, 'IPS\Content\Hideable' ) )
{
/* If moderator, show the reason */
$reason = NULL;
if( $this->modPermission( 'unhide' ) )
{
$reason = ( $item and $item::$firstCommentRequired AND $this->isFirst() ) ? $item->approvalQueueReason() : $this->approvalQueueReason();
}
if ( $reason )
{
$this->hiddenBlurb = Member::loggedIn()->language()->addToStack( 'hidden_with_reason', FALSE, array( 'sprintf' => array( $reason ) ) );
}
else
{
/* Otherwise show a message that the content requires approval before it can be edited. */
$this->hiddenBlurb = Member::loggedIn()->language()->addToStack( 'hidden_awaiting_approval' );
}
}
else
{
$this->hiddenBlurb = Member::loggedIn()->language()->addToStack('hidden');
}
}
}
return $this->hiddenBlurb;
}
/**
* Blurb for when/why/by whom this content was deleted
*
* @return string
* @throws BadMethodCallException
*/
public function deletedBlurb(): string
{
try
{
$idColumn = static::$databaseColumnId;
$log = DeletionLog::constructFromData( Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() );
if( $log->_deleted_by )
{
return Member::loggedIn()->language()->addToStack( 'deletion_blurb', FALSE, array( 'sprintf' => array( $log->_deleted_by->name, $log->deleted_date->fullYearLocaleDate(), $log->deletion_date->fullYearLocaleDate() ) ) );
}
else
{
return Member::loggedIn()->language()->addToStack( 'deletion_blurb_no_member', FALSE, array( 'sprintf' => array( $log->deletion_date->fullYearLocaleDate() ) ) );
}
}
catch( UnderflowException )
{
return Member::loggedIn()->language()->addToStack('deleted');
}
}
/**
* @brief Reason for requiring approval
*/
protected ?string $_approvalQueueReason = NULL;
/**
* Approval Queue Reason
*
* @return NULL|bool|string
*/
public function approvalQueueReason(): null|bool|string
{
if ( $this->_approvalQueueReason === NULL )
{
try
{
$idColumn = static::$databaseColumnId;
$this->_approvalQueueReason = Approval::loadFromContent( get_class( $this ), $this->$idColumn )->reason();
}
catch( OutOfRangeException )
{
$this->_approvalQueueReason = NULL;
}
}
return $this->_approvalQueueReason;
}
/**
* Get HTML for search result display
*
* @param NULL|string $ref Referrer
* @param Model $container Container
* @param string $title Title
* @return callable
*/
public function approvalQueueHtml( ?string $ref, Model $container, string $title ): string
{
return Theme::i()->getTemplate( 'modcp', 'core', 'front' )->approvalQueueItem( $this, $ref, $container, $title );
}
/**
* Log a row in core_post_before_registering
*
* @param string $guestEmail Guest email address
* @param string|null $key User's existing post_before_register cookie value
* @return string The new key, if one wasn't provided
*/
public function _logPostBeforeRegistering( string $guestEmail, ?string $key = NULL ): string
{
$key = $key ?: Login::generateRandomString();
$idColumn = static::$databaseColumnId;
Db::i()->insert( 'core_post_before_registering', array(
'email' => $guestEmail,
'class' => get_class( $this ),
'id' => $this->$idColumn,
'timestamp' => time(),
'secret' => $key,
'language' => Member::loggedIn()->language()->id
) );
/* enable followup task */
Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), "`key`='postBeforeRegisterFollowup'" );
return $key;
}
/**
* Send Approved Notification
*
* @return void
*/
public function sendApprovedNotification(): void
{
$this->sendNotifications();
$this->sendAuthorApprovalNotification();
}
/**
* Send Author Approval Notification
*
* @return void
*/
public function sendAuthorApprovalNotification(): void
{
/* Tell the author their content has been approved */
$member = Member::load( $this->mapped('author') );
$notification = new Notification( Application::load('core'), 'approved_content', $this, array( $this ), array(), FALSE );
$notification->recipients->attach( $member );
$notification->send();
}
/**
* Send Unapproved Notification
*
* @return void
* @throws DateInvalidOperationException
*/
public function sendUnapprovedNotification(): void
{
/* Check the bridge */
if ( ! Bridge::i()->sendUnapprovedNotification( $this ) )
{
return;
}
$moderators = array( 'g' => array(), 'm' => array() );
foreach(Db::i()->select( '*', 'core_moderators' ) AS $mod )
{
$canView = FALSE;
$canApprove = FALSE;
if ( $mod['perms'] == '*' )
{
$canView = TRUE;
$canApprove = TRUE;
}
else
{
$perms = json_decode( $mod['perms'], TRUE );
foreach ( array( 'canView' => 'can_view_hidden_', 'canApprove' => 'can_unhide_' ) as $varKey => $modPermKey )
{
if ( isset( $perms[ $modPermKey . 'content' ] ) AND $perms[ $modPermKey . 'content' ] )
{
$$varKey = TRUE;
}
else
{
try
{
$container = ( $this instanceof Comment ) ? $this->item()->container() : $this->container();
$containerClass = get_class( $container );
$title = static::$title;
if
(
isset( $containerClass::$modPerm )
and
(
$perms[ $containerClass::$modPerm ] === -1
or
(
is_array( $perms[ $containerClass::$modPerm ] )
and
in_array( $container->_id, $perms[ $containerClass::$modPerm ] )
)
)
and
isset( $perms[ $modPermKey . $title ] )
and
$perms[ $modPermKey . $title ]
)
{
$$varKey = TRUE;
}
}
catch ( BadMethodCallException ) { }
}
}
}
if ( $canView === TRUE and $canApprove === TRUE )
{
$moderators[ $mod['type'] ][] = $mod['id'];
}
}
$notification = new Notification( Application::load('core'), 'unapproved_content', $this, array( $this, $this->author() ) );
foreach (Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
{
$member = Member::constructFromData( $member );
/* @var $member Member */
/* We don't need to notify the author of the content or when member cannot view this content*/
if( $this->author()->member_id != $member->member_id AND $this->canView( $member ) )
{
$notification->recipients->attach( $member );
}
}
$notification->send();
}
/**
* Check comment against profanity filters but do not act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|null $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string|null $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param array|null $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @param bool $hiddenByFilter
* @param bool $itemHiddenByFilter
* @param array $filtersMatched What matched, passed by reference
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function shouldTriggerProfanityFilters( bool $first=FALSE, bool $edit=TRUE, ?string $content=NULL, string|null|bool $title=NULL, ?string $autoSaveLocation=NULL, ?array $autoSaveKeys=NULL, array $imageUploads=[], bool &$hiddenByFilter = FALSE, bool &$itemHiddenByFilter = FALSE, array &$filtersMatched = array() ): bool
{
/* Set our content */
$content = $content ?: $this->content();
/* We need our item */
$item = $this;
if ( $this instanceof Comment )
{
$item = $this->item();
if ( $item::$firstCommentRequired AND $first AND $title !== FALSE )
{
$title = $title ?: $item->mapped('title');
}
}
else
{
if ( $title !== FALSE )
{
$title = $title ?: $this->mapped('title');
}
}
/* If this content is "post before register", skip this check (we'll do it after registration is complete) */
if ( $this->hidden() === -3 or ( $item::$firstCommentRequired and $first and $item->hidden() === -3 ) )
{
return FALSE;
}
/* Check the author cannot bypass */
$member = $this->author();
if ( $member->group['g_bypass_badwords'] )
{
return FALSE;
}
/* First, check the image scanner */
$idColumn = static::$databaseColumnId;
$itemIdColumn = $item::$databaseColumnId;
if ( Settings::i()->ips_imagescanner_enable )
{
if ( $edit )
{
$autoSaveLocation = $autoSaveLocation ?: ( $item::$application . '_' . ucfirst( $item::$module ) );
if ( !$autoSaveKeys )
{
if ( $this instanceof Comment and ( !( $this instanceof Review ) or !$first or !$item::$firstCommentRequired ) )
{
$autoSaveKeys = [ $this->attachmentIds() ];
}
else
{
$autoSaveKeys = [ [ $item->$itemIdColumn ] ];
}
}
$clauses = [];
$binds = [];
foreach ( $autoSaveKeys as $_autoSaveKeys )
{
$idLookups = [];
foreach ( range( 1, count( $_autoSaveKeys ) ) as $i )
{
$idLookups[]= "id$i=?";
}
$clauses[] = '( ' . implode( ' AND ', $idLookups ) . ' )';
$binds = array_merge( $binds, $_autoSaveKeys );
}
$attachmentsMapWhere = array_merge( array( 'location_key=? AND ( ' . implode( ' OR ', $clauses ) . ' )', $autoSaveLocation ), $binds );
}
else
{
if ( $autoSaveKeys === NULL )
{
if ( $this instanceof Item or ( $first and $item::$firstCommentRequired ) )
{
$container = $item->containerWrapper();
$autoSaveKeys[] = 'newContentItem-' . $item::$application . '/' . $item::$module . '-' . ( $container ? $container->_id : 0 );
}
else
{
if ( $this instanceof Review )
{
$autoSaveKeys[] = 'review-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
else
{
$autoSaveKeys[] = 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
}
}
$attachmentsMapWhere = Db::i()->in( 'temp', array_map( 'md5', $autoSaveKeys ) );
}
if ( $this instanceof Item OR ( $first AND $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = Profanity::hiddenByFilters( $content, $member, $filtersMatched );
if ( $title AND !$itemHiddenByFilter )
{
$itemHiddenByFilter = Profanity::hiddenByFilters( $title, $member, $filtersMatched );
}
}
else
{
$hiddenByFilter = Profanity::hiddenByFilters( $content, $member, $filtersMatched );
if ( $title AND !$hiddenByFilter )
{
$hiddenByFilter = Profanity::hiddenByFilters( $title, $member, $filtersMatched );
}
}
foreach (Db::i()->select( 'attach_moderation_status', 'core_attachments', [ 'attach_id IN (?)', Db::i()->select( 'attachment_id', 'core_attachments_map', $attachmentsMapWhere ) ] ) as $attachModerationStatus )
{
if ( $attachModerationStatus === 'pending' )
{
if ( $this instanceof Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
}
if ( !$hiddenByFilter and !$itemHiddenByFilter )
{
foreach ( $imageUploads as $file )
{
if ( $file->requiresModeration )
{
if ( $this instanceof Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
}
if ( $hiddenByFilter OR $itemHiddenByFilter )
{
$log = new Approval;
$log->content_class = get_called_class();
$log->content_id = $this->$idColumn;
$log->held_reason = 'image';
$log->save();
}
}
}
else
{
/* Then pass this through our profanity and link filters */
try
{
if ( $this instanceof Item or ( $first and $item::$firstCommentRequired ) )
{
if ( !$itemHiddenByFilter )
{
$itemHiddenByFilter = Profanity::hiddenByFilters( $content, $member, $filtersMatched );
}
if ( !$itemHiddenByFilter and $title )
{
$itemHiddenByFilter = Profanity::hiddenByFilters( $title, $member, $filtersMatched );
}
}
else
{
if ( !$hiddenByFilter )
{
$hiddenByFilter = Profanity::hiddenByFilters( $content, $member, $filtersMatched );
}
if ( !$hiddenByFilter and $title )
{
$hiddenByFilter = Profanity::hiddenByFilters( $title, $member, $filtersMatched );
}
}
}
catch( BadMethodCallException ) { }
}
/* Return */
return ( $hiddenByFilter or $itemHiddenByFilter );
}
/**
* Check comment against profanity filters AND act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|null $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string|null $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param array|null $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function checkProfanityFilters( bool $first=FALSE, bool $edit=TRUE, ?string $content=NULL, string|null|bool $title=NULL, ?string $autoSaveLocation=NULL, array|null $autoSaveKeys=NULL, array $imageUploads=[] ): bool
{
/* Check it */
$hiddenByFilter = FALSE;
$itemHiddenByFilter = FALSE;
$sendNotifications = FALSE;
$filtersMatched = array();
$item = ( $this instanceof Item ) ? $this : $this->item();
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter, $filtersMatched );
/* If we need to hide the item, then do that */
if ( $itemHiddenByFilter )
{
$sendNotifications = $edit;
/* 'approved' is easy, clear and concise */
if ( isset( $item::$databaseColumnMap['approved'] ) )
{
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
}
/* 'hidden' is backwards */
elseif ( isset( $item::$databaseColumnMap['hidden'] ) )
{
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
}
}
/* If we need to hide this, then do that */
if ( $hiddenByFilter or $itemHiddenByFilter )
{
$sendNotifications = TRUE;
/* 'approved' is easy, clear and concise */
if ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = 0;
$this->save();
}
/* 'hidden' is backwards */
else if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
$this->$column = ( $itemHiddenByFilter and $this instanceof Comment ) ? 2 : 1; # We use the special 2 flag to note it is only hidden because the parent is
$this->save();
}
$idColumn = static::$databaseColumnId;
$itemColumnId = $item::$databaseColumnId;
try
{
if ( $this instanceof Comment AND $item::$firstCommentRequired AND $this->isFirst() )
{
Approval::loadFromContent( get_class( $item ), $item->$itemColumnId );
}
else
{
Approval::loadFromContent( get_called_class(), $this->$idColumn );
}
}
catch( OutOfRangeException )
{
if ( isset( $filtersMatched['type'] ) AND isset( $filtersMatched['match'] ) )
{
$log = new Approval;
if ( $this instanceof Comment AND $item::$firstCommentRequired AND $this->isFirst() )
{
$log->content_class = get_class( $item );
$log->content_id = $item->$itemColumnId;
}
else
{
$log->content_class = get_called_class();
$log->content_id = $this->$idColumn;
}
$log->held_reason = $filtersMatched['type'];
switch( $filtersMatched['type'] )
{
case 'profanity':
$log->held_data = array( 'word' => $filtersMatched['match'] );
break;
case 'url':
$log->held_data = array( 'url' => $filtersMatched['match'] );
break;
case 'email':
$log->held_data = array( 'email' => $filtersMatched['match'] );
break;
}
$log->save();
}
}
}
/* If we did either, then recount number of comments */
if ( $itemHiddenByFilter or $hiddenByFilter )
{
$item->resyncCommentCounts();
$item->resyncLastComment();
$item->save();
if ( $first AND $container = $item->containerWrapper() )
{
$container->resetCommentCounts();
$container->save();
}
}
/* Return */
return $sendNotifications;
}
/**
* Can hide?
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canHide( ?Member $member=NULL ): bool
{
/* Extensions go first */
if( $permCheck = Permissions::can( 'hide', $this, $member ) )
{
return ( $permCheck === Permissions::PERM_ALLOW );
}
if( !$this->actionEnabled( 'hide', $member ) )
{
return false;
}
if ( $this->hidden() < 0 )
{
return FALSE;
}
$member = $member ?: Member::loggedIn();
if ( $this instanceof Comment )
{
return ( !$this->isFirst() and ( static::modPermission( 'hide', $member, $this->item()->containerWrapper() ) or ( $member->member_id and $member->member_id == $this->author()->member_id and ( $member->group['g_hide_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_hide_own_posts'] ) ) ) ) ) );
}
return ( $member->member_id and ( static::modPermission( 'hide', $member, $this->containerWrapper() ) or ( $member->member_id == $this->author()->member_id and ( $member->group['g_hide_own_posts'] == '1' or in_array( get_class( $this ), explode( ',', $member->group['g_hide_own_posts'] ) ) ) ) ) );
}
/**
* Can unhide?
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canUnhide( ?Member $member=NULL ): bool
{
/* Extensions go first */
if( $permCheck = Permissions::can( 'unhide', $this, $member ) )
{
return ( $permCheck === Permissions::PERM_ALLOW );
}
if( !$this->actionEnabled( 'unhide', $member ) )
{
return false;
}
if ( $this->hidden() === 0 )
{
return FALSE;
}
/* Check delayed deletes */
if ( $this->hidden() == -2 )
{
return FALSE;
}
$member = $member ?: Member::loggedIn();
if ( $this instanceof Comment )
{
$hiddenByItem = FALSE;
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
$hiddenByItem = ( $this->$column === 2 );
}
return ( !$this->isFirst() and ! $hiddenByItem and static::modPermission( 'unhide', $member, $this->item()->containerWrapper() ) );
}
return ( $member->member_id and ( static::modPermission( 'unhide', $member, $this->containerWrapper() ) ) );
}
/**
* Can view hidden items?
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @param Model|null $container Container
* @return bool
* @note If called without passing $container, this method falls back to global "can view hidden content" moderator permission which isn't always what you want - pass $container if in doubt or use canViewHiddenItemsContainers()
*/
public static function canViewHiddenItems( ?Member $member=NULL, ?Model $container = NULL ): bool
{
$member = $member ?: Member::loggedIn();
return $container ? static::modPermission( 'view_hidden', $member, $container ) : $member->modPermission( "can_view_hidden_content" );
}
/**
* Container IDs that the member can view hidden items in
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool|array TRUE means all, FALSE means none
*/
public static function canViewHiddenItemsContainers( ?Member $member=NULL ): bool|array
{
$member = $member ?: Member::loggedIn();
if ( $member->modPermission( "can_view_hidden_content" ) )
{
return TRUE;
}
elseif ( $member->modPermission( "can_view_hidden_" . static::$title ) )
{
if ( !isset( static::$containerNodeClass ) )
{
return TRUE;
}
$containerClass = static::$containerNodeClass;
if ( isset( $containerClass::$modPerm ) )
{
$containers = $member->modPermission( $containerClass::$modPerm );
if ( $containers === -1 )
{
return TRUE;
}
return $containers;
}
else
{
return TRUE;
}
}
return FALSE;
}
/**
* Can view hidden comments on this item?
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canViewHiddenComments( ?Member $member=NULL ): bool
{
/* Extensions go first */
if( $permCheck = Permissions::can( 'view_hidden', $this, $member ) )
{
return ( $permCheck === Permissions::PERM_ALLOW );
}
$commentClass = static::$commentClass;
return $commentClass::modPermission( 'view_hidden', $member, $this->containerWrapper() );
}
/**
* Can view hidden reviews on this item?
*
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canViewHiddenReviews( ?Member $member=NULL ): bool
{
/* Extensions go first */
if( $permCheck = Permissions::can( 'view_hidden', $this, $member ) )
{
return ( $permCheck === Permissions::PERM_ALLOW );
}
$reviewClass = static::$reviewClass;
return $reviewClass::modPermission( 'view_hidden', $member, $this->containerWrapper() );
}
/**
* Item is moderator hidden by a moderator
*
* @return boolean
* @throws RuntimeException
*/
public function approvedButHidden(): bool
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
return $this->$column == 2;
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
return $this->$column == -1;
}
else
{
throw new RuntimeException;
}
}
/**
* Should new items be moderated?
*
* @param Member $member The member posting
* @param Model|NULL $container The container
* @param bool $considerPostBeforeRegistering If TRUE, and $member is a guest, will check if a newly registered member would be moderated
* @return bool
*/
public static function moderateNewItems( Member $member, ?Model $container = NULL, bool $considerPostBeforeRegistering = FALSE ): bool
{
if ( $container and $container->checkAction( 'moderate_items' ) and !$member->group['g_avoid_q'] )
{
return !static::modPermission( 'approve', $member, $container );
}
return $member->moderateNewContent( $considerPostBeforeRegistering );
}
/**
* Should new comments be moderated?
*
* @param Member $member The member posting
* @param bool $considerPostBeforeRegistering If TRUE, and $member is a guest, will check if a newly registered member would be moderated
* @return bool
*/
public function moderateNewComments( Member $member, bool $considerPostBeforeRegistering = FALSE ): bool
{
if( ( $this instanceof Item and $this->containerWrapper() ) )
{
if ( $this->container()->checkAction( 'moderate_comments' ) and ! $member->group['g_avoid_q'] )
{
return true;
}
}
$return = $member->moderateNewContent( $considerPostBeforeRegistering );
if ( $return === false AND IPS::classUsesTrait( $this, 'IPS\Content\MetaData' ) AND static::supportedMetaDataTypes() !== NULL AND in_array( 'core_ItemModeration', static::supportedMetaDataTypes() ) )
{
$return = $this->itemModerationEnabled( $member );
}
return $return;
}
/**
* Should new reviews be moderated?
*
* @param Member $member The member posting
* @param bool $considerPostBeforeRegistering If TRUE, and $member is a guest, will check if a newly registered member would be moderated
* @return bool
*/
public function moderateNewReviews( Member $member, bool $considerPostBeforeRegistering = FALSE ): bool
{
if( ( $this instanceof Item and $this->containerWrapper() ) )
{
if( $this->container()->checkAction( 'moderate_reviews' ) and ! $member->group['g_avoid_q'] )
{
return true;
}
}
$return = $member->moderateNewContent( $considerPostBeforeRegistering );
if ( $return === FALSE AND IPS::classUsesTrait( $this, 'IPS\Content\MetaData' ) AND static::supportedMetaDataTypes() !== NULL AND in_array( 'core_ItemModeration', static::supportedMetaDataTypes() ) )
{
$return = $this->itemModerationEnabled( $member );
}
return $return;
}
/**
* Syncing to run when hiding
*
* @param Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @return void
*/
public function onHide( Member|null|bool $member ): void
{
if ( $this instanceof Item )
{
if ( method_exists( $this, 'container' ) )
{
try
{
$container = $this->container();
if( $container->_items !== null )
{
$container->_items = ( $container->_items >=0 ) ? ( $container->_items - 1 ) : 0;
}
if ( isset( static::$commentClass ) )
{
$container->setLastComment();
}
if ( isset( static::$reviewClass ) )
{
if( !$this->hidden() )
{
$container->_reviews = $container->_reviews - $this->mapped('num_reviews');
}
$container->setLastReview();
}
$container->resetCommentCounts();
$container->save();
}
catch ( BadMethodCallException ) { }
}
/* If this is a tagged class, remove it if it was pinned */
if( IPS::classUsesTrait( $this, Taggable::class ) )
{
$idColumn = static::$databaseColumnId;
Db::i()->delete( 'core_tags_pinned', [ 'pinned_item_class=? and pinned_item_id=?', get_called_class(), $this->$idColumn ] );
}
}
else if ( $this instanceof Comment )
{
if ( $this instanceof Review )
{
$item = $this->item();
if ( $item->container()->_reviews !== NULL )
{
$item->container()->_reviews = ( $item->container()->_reviews > 0 ) ? ( $item->container()->_reviews - 1 ) : 0;
$item->container()->setLastReview();
$item->container()->save();
}
if ( isset( $item::$databaseColumnMap['hidden_reviews'] ) )
{
$column = $item::$databaseColumnMap['hidden_reviews'];
$item->$column = $item->mapped('hidden_reviews') + 1;
}
if ( isset( $item::$databaseColumnMap['num_reviews'] ) )
{
$column = $item::$databaseColumnMap['num_reviews'];
$item->$column = $item->mapped('num_reviews') - 1;
}
if( isset( $item::$databaseColumnMap['rating'] ) )
{
$ratingField = $item::$databaseColumnMap['rating'];
$item->$ratingField = $item->averageReviewRating() ?: 0;
}
$item->resyncLastReview();
$item->save();
}
else
{
$item = $this->item();
$this->rebuildItemStats( $item );
/* Remove any notifications */
$idColumn = static::$databaseColumnId;
Db::i()->delete( 'core_notifications', array( 'item_sub_class=? AND item_sub_id=?', get_called_class(), (int) $this->$idColumn ) );
if ( $item->container()->_comments !== NULL )
{
$item->container()->setLastComment();
$item->container()->_comments = ( $item->container()->_comments > 0 ) ? ( $item->container()->_comments - 1 ) : 0;
$item->container()->resetCommentCounts();
$item->container()->save();
}
}
}
Event::fire( 'onStatusChange', $this, array( 'hide' ) );
/* If we have an ItemTopic we need ot keep it in synch */
if( IPS::classUsesTrait( $this, 'IPS\Content\ItemTopic' ) )
{
$this->itemHidden( $member );
}
}
/**
* Syncing to run when unhiding
*
* @param bool $approving If true, is being approved for the first time
* @param Member|NULL|FALSE $member The member doing the action (NULL for currently logged in member, FALSE for no member)
* @return void
*/
public function onUnhide( bool $approving, Member|null|bool $member ): void
{
if ( $this instanceof Item )
{
/* If approving, we may need to increase the post count */
if ( $approving AND isset( static::$commentClass ) )
{
$commentClass = static::$commentClass;
if ( ( IPS::classUsesTrait( $this, 'IPS\Content\Anonymous' ) AND !$this->isAnonymous() ) and ( static::$firstCommentRequired and $commentClass::incrementPostCount( $this->containerWrapper() ) ) or static::incrementPostCount( $this->containerWrapper() ) )
{
$this->author()->member_posts++;
$this->author()->save();
}
}
/* Update container */
if ( method_exists( $this, 'container' ) )
{
try
{
$container = $this->container();
if( $container->_items !== null )
{
$container->_items = ( $container->_items + 1 );
}
if( $approving and $container->_unapprovedItems !== null )
{
$container->_unapprovedItems = ( $container->_unapprovedItems >= 0 ) ? ( $container->_unapprovedItems - 1 ) : 0;
}
$resetCommentCounts = TRUE;
if ( IPS::classUsesTrait( $this, 'IPS\Content\FuturePublishing' ) AND $this->isFutureDate() )
{
$resetCommentCounts = FALSE;
}
if ( $resetCommentCounts )
{
$container->resetCommentCounts();
if ( isset( static::$commentClass ) )
{
$container->setLastComment( null, $this );
}
if ( isset( static::$reviewClass ) )
{
$container->_reviews = $container->_reviews + $this->mapped('num_reviews');
$container->setLastReview();
}
$container->save();
}
}
catch ( BadMethodCallException ) { }
}
}
else if ( $this instanceof Comment )
{
if ( $this instanceof Review )
{
$item = $this->item();
if ( $approving )
{
if ( isset( $item::$databaseColumnMap['unapproved_reviews'] ) )
{
$column = $item::$databaseColumnMap['unapproved_reviews'];
$item->$column = $item->mapped('unapproved_reviews') - 1;
}
if ( $item->container()->_unapprovedReviews !== NULL )
{
$item->container()->_unapprovedReviews = ( $item->container()->_unapprovedReviews > 0 ) ? ( $item->container()->_unapprovedReviews - 1 ) : 0;
$item->container()->setLastReview();
$item->container()->save();
}
}
else if ( isset( $item::$databaseColumnMap['hidden_reviews'] ) )
{
$column = $item::$databaseColumnMap['hidden_reviews'];
if ( $item->mapped('hidden_reviews') > 0 )
{
$item->$column = $item->mapped('hidden_reviews') - 1;
}
}
if ( isset( $item::$databaseColumnMap['num_reviews'] ) )
{
$column = $item::$databaseColumnMap['num_reviews'];
$item->$column = $item->mapped('num_reviews') + 1;
}
if ( $item->container()->_reviews !== NULL )
{
$item->container()->_reviews = $item->container()->_reviews + 1;
$item->container()->setLastReview();
$item->container()->save();
}
if( isset( $item::$databaseColumnMap['rating'] ) )
{
$ratingField = $item::$databaseColumnMap['rating'];
$item->$ratingField = $item->averageReviewRating() ?: 0;
}
$item->resyncLastReview();
$item->save();
}
else
{
$item = $this->item();
if ( $approving )
{
/* We should only do this if it is an actual account, and not a guest. */
if ( $this->author()->member_id )
{
try
{
if ( static::incrementPostCount( $item->container() ) )
{
$this->author()->member_posts++;
$this->author()->save();
}
}
catch( BadMethodCallException ) { }
}
}
$this->rebuildItemStats( $item );
if ( $item->container()->_comments !== NULL )
{
$item->container()->setLastComment( $this, $item );
$item->container()->_comments = ( $item->container()->_comments + 1 );
$item->container()->resetCommentCounts();
$item->container()->save();
}
if( $item->container()->_unapprovedComments !== NULL and $approving )
{
$item->container()->_unapprovedComments = ( $item->container()->_unapprovedComments > 0 ) ? ( $item->container()->_unapprovedComments - 1 ) : 0;
$item->container()->save();
}
}
}
Event::fire( 'onStatusChange', $this, array( 'unhide' ) );
/* If we use ItemTopic, keep it in synch */
if( IPS::classUsesTrait( $this, 'IPS\Content\ItemTopic' ) )
{
$this->itemUnhidden( $approving, $member );
}
}
/**
* Resynch and recount
*
* @param Item $item
* @return void
*/
protected function rebuildItemStats( Item $item ) : void
{
$itemIdColumn = $item::$databaseColumnId;
$item->resyncCommentCounts();
$item->resyncLastComment();
$item->save();
/* We have to do this *after* updating the last comment data for the item, because that uses the cached data from the item (i.e. topic) */
try
{
/* Update mappings */
if ( IPS::classUsesTrait( $item->container(), 'IPS\Node\Statistics' ) )
{
$item->container()->rebuildPostedIn( array( $item->$itemIdColumn ), array( $this->author() ) );
}
} catch ( BadMethodCallException ) {}
if ( IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
{
$item->clearCachedStatistics();
}
/* Send the content items URL to IndexNow if the guest can view it */
if( $item->canView( new Member ) )
{
IndexNow::addUrlToQueue( $item->url() );
}
}
}