Version 4.7.6
This commit is contained in:
1 parent
8ba7d43898
commit
7b7404e741
1085 files changed
+30590
-7837
No files matched your search
+104
-24
@@ -289,7 +289,15 @@ abstract class _Comment extends \IPS\Content
|
||||
$obj->postCreate();
|
||||
|
||||
/* This needs to happen before the index is added */
|
||||
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
|
||||
if( $item::$firstCommentRequired and $obj->isFirst() )
|
||||
{
|
||||
$container = $obj->container();
|
||||
$parameters = array_merge( array( 'newContentItem-' . $item::$application . '/' . $item::$module . '-' . ( $container ? $container->_id : 0 ) ), $obj->attachmentIds() );
|
||||
}
|
||||
else
|
||||
{
|
||||
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
|
||||
}
|
||||
\IPS\File::claimAttachments( ...$parameters );
|
||||
|
||||
/* Add to search index */
|
||||
@@ -418,7 +426,7 @@ abstract class _Comment extends \IPS\Content
|
||||
|
||||
$item->save();
|
||||
|
||||
if ( !$item->hidden() and ! $item->approvedButHidden() and $item->containerWrapper() and $item->container()->_comments !== NULL )
|
||||
if ( !$item->hidden() and ! $item->approvedButHidden() and $item->containerWrapper() and $item->container()->_comments !== NULL and !$item->isFutureDate() )
|
||||
{
|
||||
$item->container()->_comments = ( $item->container()->_comments + 1 );
|
||||
$item->container()->setLastComment( $this );
|
||||
@@ -459,7 +467,66 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
catch( \RedisException $e ) { }
|
||||
}
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $item->container() AND $item->container()->contentHeldForApprovalByNode( 'comment', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
/* Finally if the item itself requires moderation, that's why */
|
||||
if ( $foundReason === FALSE AND \is_array( $item::supportedMetaDataTypes() ) AND \in_array('core_ItemModeration', $item::supportedMetaDataTypes() ) AND (bool) \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->enabled( $item, $this->author() ) )
|
||||
{
|
||||
$log->held_reason = 'item';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,7 +1075,10 @@ abstract class _Comment extends \IPS\Content
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $item ), $item->$idColumn ) );
|
||||
}
|
||||
|
||||
|
||||
/* Update reports */
|
||||
\IPS\Db::i()->update( 'core_rc_index', array( 'item_id' => $item->$idColumn, 'node_id' => ( $item->containerWrapper() ? $item->containerWrapper()->_id : 0 ) ), array( 'class=? and content_id=?', \get_class( $this ), $oldItem->$idColumn ) );
|
||||
|
||||
if( $skip === FALSE )
|
||||
{
|
||||
$oldItem->rebuildFirstAndLastCommentData();
|
||||
@@ -1096,7 +1166,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Remove featured comment associations */
|
||||
if( $this->isFeatured() AND $item )
|
||||
if( $item and $this->isFeatured() )
|
||||
{
|
||||
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $item, $this );
|
||||
}
|
||||
@@ -1154,7 +1224,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Solvable' ) and isset( $itemClass::$databaseColumnMap['solved_comment_id'] ) )
|
||||
if ( $item AND \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Solvable' ) and isset( $itemClass::$databaseColumnMap['solved_comment_id'] ) )
|
||||
{
|
||||
/* Reset item */
|
||||
if ( $item->mapped('solved_comment_id') and $item->mapped('solved_comment_id') == $this->$idColumn )
|
||||
@@ -1165,7 +1235,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
if ( $item AND \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$item->clearCachedStatistics();
|
||||
}
|
||||
@@ -1806,28 +1876,38 @@ abstract class _Comment extends \IPS\Content
|
||||
public function webhookFilters()
|
||||
{
|
||||
$filters = parent::webhookFilters();
|
||||
$item = $this->item();
|
||||
try
|
||||
{
|
||||
$item = $this->item();
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$item = NULL;
|
||||
}
|
||||
|
||||
/* This is kept for legacy compatibility, but starting with 4.6.13 the Content and Item class handle this inside their own webhookFilter methods */
|
||||
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item->hidden() ) );
|
||||
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item and $item->hidden() ) );
|
||||
|
||||
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
|
||||
if( $item )
|
||||
{
|
||||
$filters['locked'] = $item->locked();
|
||||
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['locked'] = $item->locked();
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['pinned'] = (bool) $item->mapped('pinned');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Featurable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['featured'] = (bool) $item->mapped('featured');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Polls', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['hasPoll'] = (bool) $item->mapped('poll');
|
||||
}
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['pinned'] = (bool) $item->mapped('pinned');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Featurable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['featured'] = (bool) $item->mapped('featured');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Polls', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['hasPoll'] = (bool) $item->mapped('poll');
|
||||
}
|
||||
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
|
||||
+234
-32
@@ -404,6 +404,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
|
||||
|
||||
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_delete', $this, $this->webhookFilters() );
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
|
||||
parent::delete();
|
||||
|
||||
@@ -455,9 +461,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
|
||||
* @param string|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 array|NULL $filtersMatched What matched, passed by reference
|
||||
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
|
||||
*/
|
||||
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE )
|
||||
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE, array &$filtersMatched = array() )
|
||||
{
|
||||
/* Set our content */
|
||||
$content = $content ?: $this->content();
|
||||
@@ -528,11 +535,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$binds = array_merge( $binds, $_autoSaveKeys );
|
||||
}
|
||||
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
if ( $this instanceof \IPS\Content\Item OR ( $first AND $item::$firstCommentRequired ) )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
|
||||
$attachmentsMapWhere = array_merge( array( 'location_key=? AND ( ' . implode( ' OR ', $clauses ) . ' )', $autoSaveLocation ), $binds );
|
||||
@@ -559,11 +578,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
if ( $this instanceof \IPS\Content\Item OR ( $first AND $item::$firstCommentRequired ) )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
|
||||
$attachmentsMapWhere = \IPS\Db::i()->in( 'temp', array_map( 'md5', $autoSaveKeys ) );
|
||||
@@ -602,6 +633,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $hiddenByFilter OR $itemHiddenByFilter )
|
||||
{
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
$log->held_reason = 'image';
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,22 +652,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
}
|
||||
if ( !$itemHiddenByFilter and $title )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
}
|
||||
if ( !$hiddenByFilter and $title )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,13 +690,20 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
|
||||
*/
|
||||
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[] )
|
||||
{
|
||||
{
|
||||
/* Check this content type is hideable */
|
||||
if ( !( $this instanceof \IPS\Content\Hideable ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Check it */
|
||||
$hiddenByFilter = FALSE;
|
||||
$itemHiddenByFilter = FALSE;
|
||||
$sendNotifications = FALSE;
|
||||
$filtersMatched = array();
|
||||
$item = ( $this instanceof \IPS\Content\Item ) ? $this : $this->item();
|
||||
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter );
|
||||
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter, $filtersMatched );
|
||||
|
||||
/* If we need to hide the item, then do that */
|
||||
if ( $itemHiddenByFilter )
|
||||
@@ -698,6 +745,54 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->$column = ( $itemHiddenByFilter and $this instanceof \IPS\Content\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 \IPS\Content\Comment AND $item::$firstCommentRequired AND $this->isFirst() )
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_class( $item ), $item->$itemColumnId );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
if ( isset( $filtersMatched['type'] ) AND isset( $filtersMatched['match'] ) )
|
||||
{
|
||||
$log = new \IPS\core\Approval;
|
||||
if ( $this instanceof \IPS\Content\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 */
|
||||
@@ -706,8 +801,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
$item->save();
|
||||
|
||||
if ( $container = $item->containerWrapper() )
|
||||
|
||||
if ( $first AND $container = $item->containerWrapper() )
|
||||
{
|
||||
$container->resetCommentCounts();
|
||||
$container->save();
|
||||
@@ -959,7 +1054,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->$column = $val;
|
||||
$this->save();
|
||||
|
||||
if ( $action == 'feature' )
|
||||
if ( $action == 'feature' and $this->author()->member_id )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_content_featured', [
|
||||
'feature_content_id' => $this->$idColumn,
|
||||
@@ -1092,6 +1187,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
if ( $this->container() AND !$this->skipContainerRebuild )
|
||||
{
|
||||
$this->container()->resetCommentCounts();
|
||||
@@ -1109,10 +1206,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
else
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->container()->rebuildPostedIn( array($this->$idColumn) );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
@@ -1256,9 +1354,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->save();
|
||||
$this->onHide( $member );
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) );
|
||||
\IPS\Db::i()->insert( 'core_soft_delete_log', array(
|
||||
'sdl_obj_id' => $this->$idColumn,
|
||||
@@ -1283,6 +1381,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1346,10 +1450,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
$this->save();
|
||||
$this->onUnhide( ( $approving OR ( $pbr AND $this->hidden() === 0 ) ), $member );
|
||||
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
|
||||
}
|
||||
|
||||
@@ -1405,6 +1509,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1443,7 +1553,42 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
/* If this is requiring approval and has a logged reason */
|
||||
$hidden = $this->hidden();
|
||||
$item = NULL;
|
||||
if ( ( $this instanceof \IPS\Content\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 ( $hidden === 1 )
|
||||
{
|
||||
/* 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 = \IPS\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 = \IPS\Member::loggedIn()->language()->addToStack( 'hidden_awaiting_approval' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1482,6 +1627,33 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reason for requiring approval
|
||||
*/
|
||||
protected $_approvalQueueReason = NULL;
|
||||
|
||||
/**
|
||||
* Approval Queue Reason
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function approvalQueueReason()
|
||||
{
|
||||
if ( $this->_approvalQueueReason === NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->_approvalQueueReason = \IPS\core\Approval::loadFromContent( \get_class( $this ), $this->$idColumn )->reason();
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
$this->_approvalQueueReason = NULL;
|
||||
}
|
||||
}
|
||||
return $this->_approvalQueueReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can promote this comment/item?
|
||||
*
|
||||
@@ -1615,6 +1787,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Find or create an index */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$item = ( $this instanceof \IPS\Content\Comment ) ? $this->item() : $this;
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
|
||||
try
|
||||
{
|
||||
$index = \IPS\core\Reports\Report::load( $this->$idColumn, 'content_id', array( 'class=?', \get_called_class() ) );
|
||||
@@ -1633,6 +1808,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$index->num_reports = 1;
|
||||
$index->num_comments = 0;
|
||||
$index->auto_moderation_exempt = 0;
|
||||
$index->item_id = $item->$itemIdColumn;
|
||||
$index->node_id = $item->containerWrapper() ? $item->containerWrapper()->_id : 0;
|
||||
}
|
||||
|
||||
/* Only set this to a new report if it is not already under review */
|
||||
@@ -1658,7 +1835,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Run automatic moderation */
|
||||
$index->runAutomaticModeration();
|
||||
|
||||
|
||||
/* Send notification to mods */
|
||||
$moderators = array( 'm' => array(), 'g' => array() );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $mod )
|
||||
@@ -1671,21 +1848,38 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
if ( $canView === FALSE )
|
||||
{
|
||||
$perms = json_decode( $mod['perms'], TRUE );
|
||||
|
||||
|
||||
if ( isset( $perms['can_view_reports'] ) AND $perms['can_view_reports'] === TRUE )
|
||||
{
|
||||
$canView = TRUE;
|
||||
}
|
||||
|
||||
/* Got nodes? */
|
||||
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 )
|
||||
{
|
||||
$moderators[ $mod['type'] ][] = $mod['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) );
|
||||
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 $member )
|
||||
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 )
|
||||
{
|
||||
$notification->recipients->attach( \IPS\Member::constructFromData( $member ) );
|
||||
$memberObj = \IPS\Member::constructFromData( $mem );
|
||||
if( $this->canView( $memberObj ) )
|
||||
{
|
||||
$notification->recipients->attach( $memberObj );
|
||||
}
|
||||
}
|
||||
$notification->send();
|
||||
|
||||
@@ -1696,7 +1890,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Return */
|
||||
return $index;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change IP Address
|
||||
* @param string $ip The new IP address
|
||||
@@ -2433,10 +2627,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'unapproved_content', $this, array( $this, $this->author() ) );
|
||||
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 $member )
|
||||
{
|
||||
/* We don't need to notify the author of the content */
|
||||
if( $this->author()->member_id != $member['member_id'] )
|
||||
$member = \IPS\Member::constructFromData( $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(\IPS\Member::constructFromData($member));
|
||||
$notification->recipients->attach( $member );
|
||||
}
|
||||
}
|
||||
$notification->send();
|
||||
@@ -2607,7 +2802,14 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
try
|
||||
{
|
||||
$item = $this->item();
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+166
-18
@@ -252,7 +252,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
/* Score by timestamp for trending */
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
catch( \RedisException $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
@@ -648,7 +648,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
catch ( \BadMethodCallException $e ) {}
|
||||
|
||||
/* Make sure we can edit and tag */
|
||||
if ( ( !$item->canEdit() OR !$item::canTag( NULL, $container ) ) )
|
||||
if ( !$item->canEdit() OR ( !$item::canTag( NULL, $container ) AND !\count( $item->tags() ) ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
@@ -953,9 +953,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'hide_reason' ) );
|
||||
$this->moderationAlertField($form, $item);
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->modAction( \IPS\Request::i()->action, NULL, $values['hide_reason'] );
|
||||
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -969,6 +975,30 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
if( \IPS\Request::i()->action === 'lock' )
|
||||
{
|
||||
if ( \IPS\Member::loggedIn()->modPermission('can_manage_alerts') AND $item->author()->member_id )
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$this->moderationAlertField($form, $item);
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$item->modAction( \IPS\Request::i()->action );
|
||||
}
|
||||
|
||||
@@ -1076,6 +1106,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
},
|
||||
'clubs' => TRUE
|
||||
) ) );
|
||||
|
||||
$this->moderationAlertField( $form, $item);
|
||||
|
||||
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
@@ -1107,6 +1139,10 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$item->markRead( NULL, NULL, NULL, TRUE );
|
||||
}
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'] )
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
|
||||
\IPS\Session::i()->modLog( 'modlog__action_move', array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped( 'title' ) ?: ( method_exists( $item, 'item' ) ? $item->item()->mapped( 'title' ) : NULL ) => FALSE ), $item );
|
||||
|
||||
@@ -1369,19 +1405,32 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$item->react( \IPS\Content\Reaction::load( \IPS\Request::i()->reaction ) );
|
||||
|
||||
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->reaction );
|
||||
$item->react( $reaction );
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->json( array(
|
||||
$output = array(
|
||||
'status' => 'ok',
|
||||
'count' => \count( $item->reactions() ),
|
||||
'score' => $item->reactionCount(),
|
||||
'blurb' => ( \IPS\Settings::i()->reaction_count_display == 'count' ) ? '' : \IPS\Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $item )
|
||||
));
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$output['datalayer'] = array_replace( $item->getDataLayerProperties(), ['reaction_type' => $reaction->_title] );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( $output );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
\IPS\core\DataLayer::i()->addEvent( 'content_react', array_replace( $item->getDataLayerProperties(), [ 'reaction_type' => $reaction->_title ] ) );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
}
|
||||
@@ -2218,7 +2267,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$comment = NULL;
|
||||
if ( mb_substr( $method, -7 ) === 'Comment' )
|
||||
{
|
||||
if ( isset( $class::$commentClass ) )
|
||||
if ( isset( $class::$commentClass ) and ( isset( \IPS\Request::i()->comment ) OR mb_substr( $method, 0, 8 ) === 'multimod' ) )
|
||||
{
|
||||
$class = $class::$commentClass;
|
||||
$method = '_' . mb_substr( $method, 0, mb_strlen( $method ) - 7 );
|
||||
@@ -2228,7 +2277,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
elseif ( mb_substr( $method, -6 ) === 'Review' )
|
||||
{
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
if ( isset( $class::$reviewClass ) and ( isset( \IPS\Request::i()->review ) OR mb_substr( $method, 0, 8 ) === 'multimod' ) )
|
||||
{
|
||||
$class = $class::$reviewClass;
|
||||
$method = '_' . mb_substr( $method, 0, mb_strlen( $method ) - 6 );
|
||||
@@ -2243,7 +2292,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( !$comment or !method_exists( $this, $method ) )
|
||||
{
|
||||
\IPS\Output::i()->error( 'page_not_found', '2S136/B', 404, '' );
|
||||
if ( mb_substr( $method, 0, 4 ) === 'find' )
|
||||
{
|
||||
/* Nothing found, redirect to main URL */
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->error( 'page_not_found', '2S136/B', 404, '' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2381,9 +2438,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'hide_reason' ) );
|
||||
$this->moderationAlertField( $form, $comment);
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$comment->modAction( 'hide', NULL, $values['hide_reason'] );
|
||||
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $comment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2662,6 +2726,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$currentPageCount = $item->commentPageCount();
|
||||
|
||||
$valueField = $commentClass::$databaseColumnMap['content'];
|
||||
$idField = $commentClass::$databaseColumnId;
|
||||
|
||||
if ( $item::$firstCommentRequired and $comment->mapped( 'first' ) )
|
||||
{
|
||||
@@ -2700,6 +2765,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->error( 'node_noperm_delete', '3S136/1K', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset best answer */
|
||||
if( $item->topic_answered_pid and $item->topic_answered_pid == $comment->$idField )
|
||||
{
|
||||
$item->topic_answered_pid = 0;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
@@ -2759,7 +2831,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$oldItem = $item;
|
||||
|
||||
/* Construct a form */
|
||||
$form = $this->_splitForm( $item );
|
||||
$form = $this->_splitForm( $item, $comment );
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
@@ -2855,6 +2927,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$oldItem->url()->__toString() => FALSE,
|
||||
$oldItem->mapped( 'title' ) => FALSE
|
||||
), $item );
|
||||
|
||||
if( isset( $values['moderation_alert_content'] ) AND $values['moderation_alert_content'] )
|
||||
{
|
||||
$this->sendModerationAlert( $values, $comment );
|
||||
}
|
||||
|
||||
/* Redirect to it */
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
@@ -3007,20 +3084,38 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
try
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
$comment->react( \IPS\Content\Reaction::load( \IPS\Request::i()->reaction ) );
|
||||
|
||||
|
||||
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->reaction );
|
||||
$comment->react( $reaction );
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->json( array(
|
||||
$output = array(
|
||||
'status' => 'ok',
|
||||
'count' => \count( $comment->reactions() ),
|
||||
'score' => $comment->reactionCount(),
|
||||
'blurb' => ( \IPS\Settings::i()->reaction_count_display == 'count' ) ? '' : \IPS\Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $comment )
|
||||
));
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$item = $item ?? $comment->item();
|
||||
$object = ( !isset( $item::$databaseColumnMap['content'] ) AND ( $item::$commentClass ) AND $comment->isFirst() ) ? $item : $comment;
|
||||
$output['datalayer'] = array_replace( $object->getDataLayerProperties(), ['reaction_type' => $reaction->_title] );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( $output );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Data Layer Event */
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$properties = $comment->getDataLayerProperties();
|
||||
$properties['reaction_type'] = $reaction->_title;
|
||||
|
||||
\IPS\core\DataLayer::i()->addEvent( 'content_react', $properties );
|
||||
}
|
||||
\IPS\Output::i()->redirect( $comment->url() );
|
||||
}
|
||||
}
|
||||
@@ -3058,9 +3153,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$form = new \IPS\Helpers\Form( 'form', 'add_recommend_content' );
|
||||
$form->class = 'ipsForm_vertical ipsForm_fullWidth';
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'feature_mod_note', NULL, FALSE ) );
|
||||
$this->moderationAlertField($form, $comment);
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->featureComment( $comment, $values['feature_mod_note'] );
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
|
||||
\IPS\Session::i()->modLog( 'modlog__featured_comment', array(
|
||||
(string) $comment->url() => FALSE,
|
||||
@@ -3291,7 +3391,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$classToCheck = $item;
|
||||
$checkAgainst = 'split_merge';
|
||||
}
|
||||
|
||||
|
||||
if ( !$classToCheck::modPermission( $checkAgainst, NULL, $item->containerWrapper() ) )
|
||||
{
|
||||
throw new \DomainException;
|
||||
@@ -3693,7 +3793,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Helpers\Form
|
||||
*/
|
||||
protected function _splitForm( \IPS\Content\Item $item )
|
||||
protected function _splitForm( \IPS\Content\Item $item, ?\IPS\Content\Comment $comment = NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -3808,7 +3908,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
}, NULL, NULL, 'split_into_url' ) );
|
||||
|
||||
|
||||
/* multi mod doesn't support moderation alerts , so if $comment isn't set, we can't use it */
|
||||
if( $comment )
|
||||
{
|
||||
$this->moderationAlertField($form, $comment );
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
@@ -4062,4 +4168,46 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function finishManage( $item )
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the moderation alert fields
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form
|
||||
* @return void
|
||||
*/
|
||||
protected function moderationAlertField( \IPS\Helpers\Form $form, \IPS\Content $item )
|
||||
{
|
||||
if( \IPS\Member::loggedIn()->modPermission('can_manage_alerts') AND $item->author()->member_id )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'moderation_alert', FALSE, FALSE, array( 'togglesOn' => array( 'moderation_alert_title', 'moderation_alert_content','moderation_alert_anonymous','moderation_alert_reply' ) ) ) );
|
||||
\IPS\Member::loggedIn()->language()->words[ 'moderation_alert'] = \IPS\Member::loggedIn()->language()->addToStack('moderation_alert_name', FALSE, ['sprintf' => [ $item->author()->name ] ] );
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'moderation_alert_title', NULL, TRUE, array( 'maxLength' => 255 ), NULL, NULL, NULL, 'moderation_alert_title' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'moderation_alert_content', NULL, TRUE, array( 'app' => 'core', 'key' => 'Alert', 'autoSaveKey' => 'createAlert', 'attachIds' => NULL ), NULL, NULL, NULL, 'moderation_alert_content' ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'moderation_alert_anonymous', FALSE, TRUE, array( 'togglesOff' => array( 'moderation_alert_reply') ), NULL, NULL, NULL, 'moderation_alert_anonymous' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'moderation_alert_reply', 0, TRUE, array( 'disabled' => (bool) \IPS\Member::loggedIn()->members_disable_pm, 'options' => array( '0' => 'alert_no_reply', '1' => 'alert_can_reply', '2' => 'alert_must_reply' ) ), NULL, NULL, NULL, 'moderation_alert_reply' ) );
|
||||
|
||||
if ( \IPS\Member::loggedIn()->members_disable_pm )
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['alert_reply_desc'] = \IPS\Member::loggedIn()->language()->get('alert_reply__nopm_desc');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the alert
|
||||
*
|
||||
* @param array $values
|
||||
* @param Item|Comment $content
|
||||
* @return \IPS\core\Alerts\Alert
|
||||
*/
|
||||
protected function sendModerationAlert( array $values, \IPS\Content\Item|\IPS\Content\Comment $content ) : \IPS\core\Alerts\Alert
|
||||
{
|
||||
$values['alert_title'] = $values['moderation_alert_title'];
|
||||
$values['alert_content'] = $values['moderation_alert_content'];
|
||||
$values['alert_recipient_type'] = 'user';
|
||||
$values['alert_recipient_user'] = $content->author()->member_id;
|
||||
$values['alert_anonymous'] = $values['moderation_alert_anonymous'];
|
||||
$values['alert_reply'] = $values['moderation_alert_reply'];
|
||||
return \IPS\core\Alerts\Alert::_createFromForm( $values, NULL );
|
||||
}
|
||||
}
|
||||
+151
-22
@@ -223,14 +223,15 @@ abstract class _Item extends \IPS\Content
|
||||
* @param \IPS\DateTime $time The time
|
||||
* @param \IPS\Node\Model|NULL $container Container (e.g. forum), if appropriate
|
||||
* @param bool|NULL $hidden Hidden? (NULL to work our automatically)
|
||||
* @param \IPS\DateTime|NULL $futureDate Publish date for future items
|
||||
* @return static
|
||||
*/
|
||||
public static function createItem( \IPS\Member $author, $ipAddress, \IPS\DateTime $time, \IPS\Node\Model $container = NULL, $hidden=NULL )
|
||||
public static function createItem(\IPS\Member $author, $ipAddress, \IPS\DateTime $time, \IPS\Node\Model $container = NULL, $hidden=NULL)
|
||||
{
|
||||
/* Create the object */
|
||||
$obj = new static;
|
||||
|
||||
foreach ( array( 'date', 'updated', 'author', 'author_name', 'ip_address', 'last_comment', 'last_comment_by', 'last_comment_name', 'last_review', 'container', 'approved', 'hidden', 'locked', 'status', 'views', 'pinned', 'featured', 'is_future_entry', 'num_comments', 'num_reviews', 'unapproved_comments', 'hidden_comments', 'unapproved_reviews', 'hidden_reviews' ) as $k )
|
||||
foreach ( array( 'date', 'updated', 'author', 'author_name', 'ip_address', 'last_comment', 'last_comment_by', 'last_comment_name', 'last_review', 'container', 'approved', 'hidden', 'locked', 'status', 'views', 'pinned', 'featured', 'is_future_entry', 'num_comments', 'num_reviews', 'unapproved_comments', 'hidden_comments', 'unapproved_reviews', 'hidden_reviews', 'future_date' ) as $k )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
@@ -244,6 +245,8 @@ abstract class _Item extends \IPS\Content
|
||||
case 'last_comment':
|
||||
case 'last_review':
|
||||
case 'date':
|
||||
$val = $time->getTimestamp();
|
||||
break;
|
||||
case 'updated':
|
||||
$val = $time->getTimestamp();
|
||||
break;
|
||||
@@ -321,6 +324,9 @@ abstract class _Item extends \IPS\Content
|
||||
case 'is_future_entry':
|
||||
$val = ( $time->getTimestamp() > time() ) ? 1 : 0;
|
||||
break;
|
||||
case 'future_date':
|
||||
$val = ( $time->getTimestamp() > time() ) ? $time->getTimestamp() : time();
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( \is_array( static::$databaseColumnMap[ $k ] ) ? static::$databaseColumnMap[ $k ] : array( static::$databaseColumnMap[ $k ] ) as $column )
|
||||
@@ -329,7 +335,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Update the container */
|
||||
if ( $container )
|
||||
{
|
||||
@@ -399,10 +405,10 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Create the item */
|
||||
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
|
||||
|
||||
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
|
||||
|
||||
/* Create the item */
|
||||
$obj = static::createItem( $member, \IPS\Request::i()->ipAddress(), $time, $container );
|
||||
$obj = static::createItem( $member, \IPS\Request::i()->ipAddress(), $time, $container, NULL );
|
||||
$obj->processBeforeCreate( $values );
|
||||
$obj->processForm( $values );
|
||||
$obj->save();
|
||||
@@ -423,9 +429,6 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$commentIdColumn = $commentClass::$databaseColumnId;
|
||||
|
||||
$parameters = array_merge( array( 'newContentItem-' . static::$application . '/' . static::$module . '-' . ( $container ? $container->_id : 0 ) ), $comment->attachmentIds() );
|
||||
\IPS\File::claimAttachments( ...$parameters );
|
||||
|
||||
if ( isset( static::$databaseColumnMap['first_comment_id'] ) )
|
||||
{
|
||||
@@ -858,9 +861,9 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Future date */
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['date'];
|
||||
$column = static::$databaseColumnMap['future_date'];
|
||||
$return['date'] = new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => 0, 'unlimitedLang' => 'immediately'), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
|
||||
}
|
||||
|
||||
@@ -1071,6 +1074,58 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
|
||||
}
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $this->container() AND $this->container()->contentHeldForApprovalByNode( 'item', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1179,7 +1234,6 @@ 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 )
|
||||
@@ -1195,6 +1249,8 @@ abstract class _Item extends \IPS\Content
|
||||
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
|
||||
{
|
||||
/* Was not future, but now is */
|
||||
$dateColumn = static::$databaseColumnMap['future_date'];
|
||||
$this->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
|
||||
$this->unPublish();
|
||||
}
|
||||
}
|
||||
@@ -1980,6 +2036,9 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Update reports */
|
||||
\IPS\Db::i()->update( 'core_rc_index', array( 'node_id' => $container->_id ), array( 'class=? and content_id=?', \get_class( $this ), $oldContainer->_id ) );
|
||||
|
||||
/* Update caches */
|
||||
$this->expireWidgetCaches();
|
||||
|
||||
@@ -2153,6 +2212,13 @@ abstract class _Item extends \IPS\Content
|
||||
/* Update notifications */
|
||||
\IPS\Db::i()->update( 'core_notifications', array( 'item_id' => $this->$idColumn ), array( 'item_class=? and item_id=?', (string) \get_class( $item ), $item->$idColumn ) );
|
||||
|
||||
/* Follows */
|
||||
if ( $this instanceof \IPS\Content\Followable )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_follow', "`follow_id` = MD5( CONCAT( `follow_app`, ';', `follow_area`, ';', {$this->$idColumn}, ';', `follow_member_id` ) ), `follow_rel_id` = {$this->$idColumn}", array( "follow_id=MD5( CONCAT( `follow_app`, ';', `follow_area`, ';', {$item->$idColumn}, ';', `follow_member_id` ) )" ), array(), NULL, \IPS\Db::IGNORE );
|
||||
\IPS\Db::i()->delete( 'core_follow_count_cache', array( 'class=? AND id=?', \get_called_class(), (int) $this->$idColumn ) );
|
||||
}
|
||||
|
||||
/* Update moderation history */
|
||||
\IPS\Db::i()->update( 'core_moderator_logs', array( 'item_id' => $this->$idColumn ), array( 'item_id=? AND class=?', $item->$idColumn, (string) \get_class( $this ) ) );
|
||||
\IPS\Session::i()->modLog( 'modlog__action_merge', array( $item->mapped('title') => FALSE, $this->url()->__toString() => FALSE, $this->mapped('title') => FALSE ), $this );
|
||||
@@ -2927,13 +2993,26 @@ abstract class _Item extends \IPS\Content
|
||||
* @param bool $joinAuthor If true, will join the members table for the author
|
||||
* @param bool $joinLastCommenter If true, will join the members table for the last commenter
|
||||
* @param bool $showMovedLinks If true, moved item links are included in the results
|
||||
* @param array|null $location Array of item lat and long
|
||||
* @return \IPS\Patterns\ActiveRecordIterator|int
|
||||
*/
|
||||
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE )
|
||||
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE, $location=NULL )
|
||||
{
|
||||
/* Are we trying to improve count performance? */
|
||||
$countShortcut = FALSE;
|
||||
|
||||
$having = NULL;
|
||||
if ( isset( $location['lat'] ) and isset( $location['lon'] ) )
|
||||
{
|
||||
/* Make sure co-ordinates are in a valid format regardless of locale */
|
||||
$location['lat'] = number_format( $location['lat'], 6, '.', '' );
|
||||
$location['lon'] = number_format( $location['lon'], 6, '.', '' );
|
||||
|
||||
$where[] = array( static::$databasePrefix . 'latitude' . ' IS NOT NULL AND ' . static::$databasePrefix . 'longitude' . ' IS NOT NULL' );
|
||||
$having = array( 'distance < 500' );
|
||||
$order = 'distance ASC';
|
||||
}
|
||||
|
||||
/* Do we really need tags? */
|
||||
if ( $joinTags and ! \IPS\Settings::i()->tags_enabled )
|
||||
{
|
||||
@@ -3135,6 +3214,12 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
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 ] . "='*' )" ) );
|
||||
|
||||
/* 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' );
|
||||
}
|
||||
|
||||
if ( \count( $containerWhere ) )
|
||||
{
|
||||
@@ -3287,6 +3372,11 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
$selectClause = static::$databaseTable . '.*';
|
||||
|
||||
if ( isset( $location['lat'] ) and isset( $location['lon'] ) and is_numeric( $location['lat'] ) and is_numeric( $location['lon'] ) )
|
||||
{
|
||||
$selectClause .= ', ( 3959 * acos( cos( radians(' . $location['lat'] . ') ) * cos( radians( ' . static::$databasePrefix . 'latitude' . ' ) ) * cos( radians( ' . static::$databasePrefix . 'longitude' . ' ) - radians( ' . $location['lon'] . ' ) ) + sin( radians( ' . $location['lat'] . ' ) ) * sin( radians( ' . static::$databasePrefix . 'latitude' . ') ) ) ) AS distance';
|
||||
}
|
||||
|
||||
if( $joinAuthor and isset( static::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
$selectClause .= ', author.*';
|
||||
@@ -3318,7 +3408,7 @@ abstract class _Item extends \IPS\Content
|
||||
$selectClause .= ', core_tags_cache.tag_cache_text';
|
||||
}
|
||||
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, $groupBy, NULL, $queryFlags );
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, $groupBy, $having, $queryFlags );
|
||||
}
|
||||
|
||||
/* Join stuff */
|
||||
@@ -3410,15 +3500,15 @@ abstract class _Item extends \IPS\Content
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
$where = array( array( static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
$where = array( array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
if ( $container )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id );
|
||||
}
|
||||
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
}
|
||||
|
||||
return static::getItemsWithPermission( $where, $order, $limit );
|
||||
@@ -4876,7 +4966,9 @@ abstract class _Item extends \IPS\Content
|
||||
'app' => static::$application,
|
||||
'key' => mb_ucfirst( static::$module ),
|
||||
'autoSaveKey' => 'reply-' . static::$application . '/' . static::$module . '-' . $this->$idColumn,
|
||||
'minimize' => isset( \IPS\Request::i()->$submitted ) ? NULL : static::$formLangPrefix . '_comment_placeholder'
|
||||
'minimize' => isset( \IPS\Request::i()->$submitted ) ? NULL : static::$formLangPrefix . '_comment_placeholder',
|
||||
'contentClass' => \get_called_class(),
|
||||
'contentId' => $this->$idColumn
|
||||
), function() use( $self ) {
|
||||
if ( !$self->mergeConcurrentComment() )
|
||||
{
|
||||
@@ -8405,7 +8497,7 @@ abstract class _Item extends \IPS\Content
|
||||
public function unpublish( $member=NULL )
|
||||
{
|
||||
/* Now do the actual stuff */
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$future = static::$databaseColumnMap['is_future_entry'];
|
||||
|
||||
@@ -8440,15 +8532,21 @@ abstract class _Item extends \IPS\Content
|
||||
public function publish( $member=NULL )
|
||||
{
|
||||
/* Now do the actual stuff */
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$date = static::$databaseColumnMap['date'];
|
||||
$date = static::$databaseColumnMap['future_date'];
|
||||
$future = static::$databaseColumnMap['is_future_entry'];
|
||||
|
||||
$this->$date = time();
|
||||
$this->$future = 0;
|
||||
}
|
||||
|
||||
if ( isset( static::$databaseColumnMap['date'] ) )
|
||||
{
|
||||
$dateField = \is_array( static::$databaseColumnMap['date'] ) ? static::$databaseColumnMap['date'][0] : static::$databaseColumnMap['date'];
|
||||
$this->$dateField = time();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
$this->onPublish( $member );
|
||||
|
||||
@@ -8458,10 +8556,41 @@ abstract class _Item extends \IPS\Content
|
||||
\IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
|
||||
}
|
||||
|
||||
/* Update the first comment */
|
||||
if ( static::$firstCommentRequired )
|
||||
{
|
||||
$comment = $this->firstComment();
|
||||
$date = $comment::$databaseColumnMap['date'];
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark this item as read for the author */
|
||||
$this->markRead( $this->author() );
|
||||
|
||||
/* Update search index */
|
||||
if ( $this instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
\IPS\Content\Search\Index::i()->index( ( static::$firstCommentRequired ) ? $this->firstComment() : $this );
|
||||
}
|
||||
|
||||
/* Send notifications if necessary */
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Facebook Promotion
|
||||
* @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 10 Feb 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Promotion
|
||||
*/
|
||||
class _Facebook extends PromoteAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'facebook';
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array(
|
||||
'token' => NULL,
|
||||
'owner' => NULL,
|
||||
'page_name' => NULL, //
|
||||
'permissions' => NULL,
|
||||
'members' => NULL,
|
||||
'page' => NULL, //
|
||||
'tags' => NULL,
|
||||
'image' => NULL,
|
||||
'last_sync' => 0,
|
||||
'shareable' => array(),
|
||||
'member_token' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
|
||||
{
|
||||
/* Fetch again */
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large&appsecret_proof=" . static::appSecretProof( $this->settings['token'] ) )->request()->get();
|
||||
|
||||
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
|
||||
$newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
|
||||
|
||||
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
|
||||
}
|
||||
|
||||
return $this->settings['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
public function getName( $serviceId=NULL )
|
||||
{
|
||||
if ( $serviceId )
|
||||
{
|
||||
$realId = mb_substr( $serviceId, 2 );
|
||||
if ( mb_substr( $serviceId, 0, 2 ) == 'p_' )
|
||||
{
|
||||
return isset( $this->settings['shareable']['pages'][ $realId ]['name'] ) ? $this->settings['shareable']['pages'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
else
|
||||
{
|
||||
return isset( $this->settings['shareable']['groups'][ $realId ]['name'] ) ? $this->settings['shareable']['groups'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check publish permissions
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
$facebook = static::getLoginHandler();
|
||||
|
||||
if ( $facebook === NULL )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$authorizedScopes = $facebook->authorizedScopes( $this->member );
|
||||
|
||||
if( $authorizedScopes === NULL )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach( array( 'manage_pages', 'publish_pages' ) as $perm )
|
||||
{
|
||||
if ( ! \in_array( $perm, $authorizedScopes ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the appSecretProof key
|
||||
*
|
||||
* @param string $token The Token
|
||||
* @return string
|
||||
*/
|
||||
protected static function appSecretProof( $token )
|
||||
{
|
||||
$facebook = static::getLoginHandler();
|
||||
return hash_hmac( 'sha256', $token, $facebook->settings['client_secret'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
* @param string $text Text for the text entry
|
||||
* @param string $link Short or full link (short when available)
|
||||
* @param string $content Additional text content (usually a comment, or the item content)
|
||||
*
|
||||
* @return array of form elements
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
$shareable = array();
|
||||
$_text = '';
|
||||
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
$_text = $text;
|
||||
|
||||
if ( isset( $this->promote->form_data['facebook'] ) AND $settings = $this->promote->form_data['facebook'] )
|
||||
{
|
||||
$shareable = $settings['shareable'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \count( $this->settings['tags'] ) )
|
||||
{
|
||||
$_text .= '#' . implode( ' #', $this->settings['tags'] );
|
||||
}
|
||||
}
|
||||
|
||||
$names = array();
|
||||
$options = array();
|
||||
if ( isset( $this->settings['shareable'] ) )
|
||||
{
|
||||
if( isset( $this->settings['shareable']['pages'] ) )
|
||||
{
|
||||
foreach( $this->settings['shareable']['pages'] as $id => $data )
|
||||
{
|
||||
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if( isset( $this->settings['shareable']['groups'] ) )
|
||||
{
|
||||
foreach( $this->settings['shareable']['groups'] as $id => $data )
|
||||
{
|
||||
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ),
|
||||
new \IPS\Helpers\Form\CheckboxSet( 'promote_facebook_shareable', $shareable, FALSE, array( 'options' => $options ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow for any extra processing
|
||||
*
|
||||
* @param array $values Values from the form isn't it though
|
||||
* @return mixed
|
||||
*/
|
||||
public function processPromoteForm( $values )
|
||||
{
|
||||
if ( isset( $values['promote_facebook_shareable'] ) )
|
||||
{
|
||||
return array( 'shareable' => $values['promote_facebook_shareable'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to Facebook
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @return void
|
||||
*/
|
||||
public function post( $promote )
|
||||
{
|
||||
$photos = $promote->imageObjects();
|
||||
|
||||
$settings = $promote->form_data['facebook'];
|
||||
$shareable = $this->settings['shareable'];
|
||||
$responses = array();
|
||||
|
||||
foreach( $settings['shareable'] as $id )
|
||||
{
|
||||
$feedId = mb_substr( $id, 2 );
|
||||
|
||||
if ( mb_substr( $id, 0, 2 ) == 'p_' )
|
||||
{
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $shareable['pages'][ $feedId ]['token'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, isset( $this->settings['member_token']['access_token'] ) ? $this->settings['member_token']['access_token'] : $this->settings['token'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the post, but not post post as that would be too late
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @param array $photos The photos
|
||||
* @param int $feedId The feed ID to post to. Honestly, it's not rocket science.
|
||||
* @param string $token I'll give you one guess. Does anyone actually read this?
|
||||
* @return stuff
|
||||
*/
|
||||
protected function _post( $promote, $photos, $feedId, $token=NULL )
|
||||
{
|
||||
/* Get the last 20 items to see if we have a duplicate */
|
||||
try
|
||||
{
|
||||
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->setQueryString( $this->_addToken( array(
|
||||
'limit' => 20
|
||||
), $token ) )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $items['data'] ) )
|
||||
{
|
||||
foreach( $items['data'] as $item )
|
||||
{
|
||||
$time = new \IPS\DateTime( ( isset( $item['created_time'] ) ? $item['created_time'] : $item['updated_time' ] ) );
|
||||
$now = new \IPS\DateTime();
|
||||
|
||||
/* Only look back past the last 30 mins */
|
||||
if ( \intval( $now->diff( $time )->format('%i') ) > 30 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
|
||||
{
|
||||
/* Duplicate */
|
||||
return $item['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
if ( ! $photos or ( \is_array( $photos ) and \count( $photos ) === 1 ) )
|
||||
{
|
||||
/* Simple message */
|
||||
try
|
||||
{
|
||||
if ( $photos )
|
||||
{
|
||||
$thePhoto = array_pop( $photos );
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ),
|
||||
'link' => (string) $promote->short_link
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'link' => (string) $promote->short_link
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
\IPS\Log::log( $response['error']['message'], 'facebook' );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We have multiple photos so we first need to create an album, and then upload into that */
|
||||
try
|
||||
{
|
||||
/* The item may be a comment, which uses a language string for objectTitle - make sure that is parsed before sending */
|
||||
$title = $promote->objectTitle;
|
||||
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $title );
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/albums" )->request()->post( $this->_addToken( array(
|
||||
'name' => $title,
|
||||
), $token ) )->decodeJson();
|
||||
|
||||
if ( ! isset( $response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException('Could not create album');
|
||||
}
|
||||
|
||||
$newAlbumId = $response['id'];
|
||||
|
||||
foreach( $photos as $photo )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $photo->url ),
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds 'access_token' to query string if token defined (it is for pages, but not for groups
|
||||
*
|
||||
* @param array $values The values of the array
|
||||
* @param string|NULL $token The token obvs
|
||||
* @return array
|
||||
*/
|
||||
protected function _addToken( $values, $token )
|
||||
{
|
||||
return ( $token ) ? array_merge( $values, array( 'access_token' => $token, 'appsecret_proof' => static::appSecretProof( $token ) ) ) : $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
if ( \is_array( $data ) and \count( $data ) )
|
||||
{
|
||||
/* Get a random URL from the many it was shared to */
|
||||
$array = $data[ array_rand( $data ) ];
|
||||
$data = array_pop( $array );
|
||||
}
|
||||
|
||||
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( 'https://facebook.com/' . $data );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this member connected to the Facebook app?
|
||||
*
|
||||
* @param \IPS\Member $member A member object
|
||||
* @return boolean
|
||||
*/
|
||||
public function isConnected( $member )
|
||||
{
|
||||
if ( \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
|
||||
{
|
||||
/* Standard handler */
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
return $facebook->canProcess( $member );
|
||||
}
|
||||
|
||||
/* Custom */
|
||||
if ( isset( $this->settings['member_token']['access_token'] ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the URL is not protocol relative
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function returnUrlWithProtocol( \IPS\Http\Url $url )
|
||||
{
|
||||
if ( ! $url->data['scheme'] )
|
||||
{
|
||||
$url = $url->setScheme( ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) ? 'https' : 'http' );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to Facebook log in handler with the correct settings
|
||||
*
|
||||
* @return \IPS\Login object
|
||||
*/
|
||||
public static function getLoginHandler()
|
||||
{
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
|
||||
if ( $facebook === NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* We are using the default app here */
|
||||
if ( ! \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
|
||||
{
|
||||
$facebook->settings = json_decode( \IPS\Settings::i()->promote_facebook_auth, TRUE );
|
||||
}
|
||||
|
||||
return $facebook;
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,11 @@ trait Reactable
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
$itemId = $item->$itemIdColumn;
|
||||
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
|
||||
}
|
||||
catch( \RedisException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,65 +523,89 @@ trait Reactable
|
||||
/**
|
||||
* React Blurb
|
||||
*
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function reactBlurb()
|
||||
public function reactBlurb(): array
|
||||
{
|
||||
if ( $this->reactBlurb === NULL )
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
|
||||
if ( \count( $this->reactions() ) )
|
||||
|
||||
/*
|
||||
If we have lots of rows, then use a more efficient way of getting the data (but does increase query count and uses a group by).
|
||||
I know this is an artibrary number, but it should mean that most communities never need to run this code, and just those with topics with more than 4,000 pages
|
||||
*/
|
||||
if ( ( $this instanceof \IPS\Content\Item ) and isset( $this::$databaseColumnMap['num_comments'] ) and $this->mapped( 'num_comments' ) >= 100000 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( \is_array( $this->_reactions ) )
|
||||
$enabledReactions = [];
|
||||
foreach( \IPS\Content\Reaction::enabledReactions() as $reaction )
|
||||
{
|
||||
foreach( $this->_reactions AS $memberId => $reactions )
|
||||
{
|
||||
foreach( $reactions as $reaction )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $reaction ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $reaction ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $reaction ]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $rep ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $rep ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $rep ]++;
|
||||
}
|
||||
$enabledReactions[] = $reaction->id;
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
|
||||
@uksort( $this->reactBlurb, function( $a, $b ) use( $enabledReactions ) {
|
||||
$positionA = $enabledReactions[ $a ]->position;
|
||||
$positionB = $enabledReactions[ $b ]->position;
|
||||
|
||||
if ( $positionA == $positionB )
|
||||
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, reaction', 'core_reputation_index', $this->getReactionWhereClause( NULL , FALSE ), 'count DESC', NULL, 'reaction' ) as $row )
|
||||
{
|
||||
if( in_array( needle: $row['reaction'], haystack: $enabledReactions ) )
|
||||
{
|
||||
return 0;
|
||||
$this->reactBlurb[ $row['reaction'] ] = $row['count'];
|
||||
}
|
||||
|
||||
return ( $positionA < $positionB ) ? -1 : 1;
|
||||
} );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
if ( \count( $this->reactions() ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( \is_array( $this->_reactions ) )
|
||||
{
|
||||
foreach ( $this->_reactions as $memberId => $reactions )
|
||||
{
|
||||
foreach ( $reactions as $reaction )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[$reaction] ) )
|
||||
{
|
||||
$this->reactBlurb[$reaction] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[$reaction]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[$rep] ) )
|
||||
{
|
||||
$this->reactBlurb[$rep] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[$rep]++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
|
||||
@uksort( $this->reactBlurb, function ( $a, $b ) use ( $enabledReactions ) {
|
||||
$positionA = $enabledReactions[$a]->position;
|
||||
$positionB = $enabledReactions[$b]->position;
|
||||
|
||||
if ( $positionA == $positionB )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ( $positionA < $positionB ) ? -1 : 1;
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->reactBlurb;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,65 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
}
|
||||
|
||||
$item->save();
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $item->container() AND $item->container()->contentHeldForApprovalByNode( 'review', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
/* Finally if the item itself requires moderation, that's why */
|
||||
if ( $foundReason === FALSE AND \is_array( $item::supportedMetaDataTypes() ) AND \in_array('core_ItemModeration', $item::supportedMetaDataTypes() ) AND (bool) \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->enabled( $item, $this->author() ) )
|
||||
{
|
||||
$log->held_reason = 'item';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,80 +51,87 @@ class _Index extends \IPS\Content\Search\Index
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
|
||||
$settings = array(
|
||||
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
|
||||
);
|
||||
if ( $analyzer === 'custom' )
|
||||
{
|
||||
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
|
||||
$analyzer = key( $settings['analysis']['analyzer'] );
|
||||
}
|
||||
|
||||
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
|
||||
|
||||
$definition = array(
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'_doc' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
|
||||
$settings = array(
|
||||
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
|
||||
);
|
||||
if ( $analyzer === 'custom' )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
|
||||
$analyzer = key( $settings['analysis']['analyzer'] );
|
||||
}
|
||||
|
||||
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
|
||||
|
||||
$definition = array(
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'_doc' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,13 +321,20 @@ class _Index extends \IPS\Content\Search\Index
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
$participants = array_values( array_unique( $participants ) );
|
||||
|
||||
|
||||
$isSolved = 0;
|
||||
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
|
||||
{
|
||||
$isSolved = 1;
|
||||
}
|
||||
|
||||
return array(
|
||||
'index_comments' => $item->mapped('num_comments'),
|
||||
'index_reviews' => $item->mapped('num_reviews'),
|
||||
'index_participants' => $participants,
|
||||
'index_tags' => $item->tags(),
|
||||
'index_prefix' => $item->prefix(),
|
||||
'index_item_solved' => $isSolved
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,10 +390,24 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/* Some container types cannot be cached */
|
||||
$classes = [];
|
||||
$conditions = [];
|
||||
$noSimilarContentClasses = [];
|
||||
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
|
||||
if ( ! $object->similarContent )
|
||||
{
|
||||
foreach( $object->classes as $class )
|
||||
{
|
||||
$noSimilarContentClasses[] = $class;
|
||||
|
||||
if ( is_subclass_of( $class, 'IPS\Content\Item' ) and isset( $class::$commentClass ) )
|
||||
{
|
||||
$noSimilarContentClasses[] = $class::$commentClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Some nodes have complex permissions, so avoid any permission issues by not selecting these */
|
||||
@@ -440,6 +454,20 @@ class _Query extends \IPS\Content\Search\Query
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent some things from being in similar content widget */
|
||||
if ( \count( $noSimilarContentClasses ) )
|
||||
{
|
||||
$this->mustNot[] = array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array( 'index_class' => $noSimilarContentClasses )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Only show non-hidden and approved items */
|
||||
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
|
||||
}
|
||||
|
||||
@@ -565,8 +565,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
if ( $cutoff )
|
||||
{
|
||||
$prefix = \IPS\Db::i()->prefix;
|
||||
|
||||
\IPS\Db::i()->delete( array( 'core_search_index_item_map', 'core_search_index' ), array( "core_search_index_item_map.index_item_id=core_search_index.index_item_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_item_map' );
|
||||
\IPS\Db::i()->delete( array( 'core_search_index_tags', 'core_search_index' ), array( "core_search_index_tags.index_id=core_search_index.index_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_tags' );
|
||||
\IPS\Db::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
|
||||
|
||||
@@ -1263,15 +1263,18 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$order = $this->order;
|
||||
|
||||
/* This forces MySQL to strongly hint a better index to the optimiser for MySQL 5.7+, even though sorting will not be affected */
|
||||
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
|
||||
if ( !\IPS\CIC )
|
||||
{
|
||||
$order .= ", index_date_updated";
|
||||
}
|
||||
|
||||
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
|
||||
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
|
||||
{
|
||||
$this->forceIndex = 'index_date_updated';
|
||||
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
|
||||
{
|
||||
$order .= ", index_date_updated";
|
||||
}
|
||||
|
||||
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
|
||||
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
|
||||
{
|
||||
$this->forceIndex = 'index_date_updated';
|
||||
}
|
||||
}
|
||||
|
||||
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
|
||||
|
||||
@@ -114,6 +114,13 @@ trait Solvable
|
||||
$notification->recipients->attach( $comment->author() );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'item' => $this,
|
||||
'comment' => $comment,
|
||||
'markedBy' => $member
|
||||
];
|
||||
\IPS\Api\Webhook::fire( 'content_marked_solved', $payload );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -182,12 +189,30 @@ trait Solvable
|
||||
{
|
||||
return ( ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() ) and $this->mapped('solved_comment_id') );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is this a non-admin/mod but can solve this item?
|
||||
*
|
||||
* @param \IPS\Member|null $member The member (null for currently logged in member)
|
||||
* @return boolean
|
||||
*/
|
||||
public function isNotModeratorButCanSolve( \IPS\Member $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if ( $this->canSolve( $member ) and $member === $this->author() and $this->containerAllowsMemberSolvable() and ! $member->modPermissions() )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user solve this item?
|
||||
*
|
||||
* @param \IPS\Member $member The member (null for currently logged in member)
|
||||
* @return bool
|
||||
* @param \IPS\Member|null $member The member (null for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canSolve( \IPS\Member $member = NULL )
|
||||
{
|
||||
@@ -200,7 +225,7 @@ trait Solvable
|
||||
|
||||
if ( $this->containerAllowsSolvable() )
|
||||
{
|
||||
if ( $member == $this->author() and $this->containerAllowsMemberSolvable() )
|
||||
if ( $member === $this->author() and $this->containerAllowsMemberSolvable() )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,58 @@ trait Statistics
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Most Recent Participans
|
||||
*
|
||||
* @param string|NULL $name If we are looking for a specific user.
|
||||
* @param int $limit The amount of results to return.
|
||||
* @return \IPS\Patterns\ActiveRecordIterator
|
||||
*/
|
||||
public function mostRecent( ?string $name = NULL, int $limit = 10 )
|
||||
{
|
||||
$commentClass = static::$commentClass;
|
||||
|
||||
if ( !isset( $commentClass::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
if ( $name )
|
||||
{
|
||||
$cacheKey = 'mostRecent_' . $limit . '_' . $name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$cacheKey = 'mostRecent_' . $limit;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$members = $this->_getCached( $cacheKey );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* Get the ten most recent posters in this content that match input. */
|
||||
$where = $this->_getVisibleWhere();
|
||||
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 ];
|
||||
}
|
||||
|
||||
$members = iterator_to_array( \IPS\Db::i()->select(
|
||||
$commentClass::$databaseColumnMap['author'],
|
||||
$commentClass::$databaseTable,
|
||||
$where,
|
||||
NULL,
|
||||
$limit ) );
|
||||
|
||||
$this->_storeCached( $cacheKey, $members );
|
||||
}
|
||||
|
||||
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'member_id', $members ) ) ), 'IPS\Member' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Members with most posts
|
||||
|
||||
@@ -182,7 +182,17 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
/* Rating */
|
||||
if ( \in_array( 'IPS\Content\Ratings', class_implements( $class ) ) and isset( $class::$databaseColumnMap['rating_average'] ) )
|
||||
{
|
||||
$return['rating'] = new \IPS\Helpers\Form\Rating( 'widget_feed_min_rating', isset( $this->configuration['widget_feed_min_rating'] ) ? $this->configuration['widget_feed_min_rating'] : 0, FALSE, array() );
|
||||
$ratingOptions = array(
|
||||
0 => 'any'
|
||||
);
|
||||
for( $i=1; $i<=\IPS\Settings::i()->reviews_rating_out_of; $i++ )
|
||||
{
|
||||
$ratingOptions[$i] = $i;
|
||||
}
|
||||
|
||||
$return['rating'] = new \IPS\Helpers\Form\Select( 'widget_feed_min_rating', isset( $this->configuration['widget_feed_min_rating'] ) ? $this->configuration['widget_feed_min_rating'] : 0, FALSE, array(
|
||||
'options' => $ratingOptions
|
||||
) );
|
||||
}
|
||||
|
||||
/* Number to show */
|
||||
|
||||
@@ -405,6 +405,12 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['rating_average'] . '>?', (int) $this->configuration['widget_feed_item_min_rating'] );
|
||||
}
|
||||
|
||||
/* Future */
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['is_future_entry'] . '=0' );
|
||||
}
|
||||
|
||||
/* Start date */
|
||||
if ( isset( $this->configuration['widget_feed_comment_date'] ) and isset( $this->configuration['widget_feed_comment_date'] ) and $this->configuration['widget_feed_comment_date'] > 0 )
|
||||
|
||||
Reference in new issue
Block a user