Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -533,9 +533,9 @@ class CommentController extends Controller
|
||||
try
|
||||
{
|
||||
$index = Report::loadByClassAndId( get_class( $object ), $object->$idColumn );
|
||||
$report = Db::i()->select( '*', 'core_rc_reports', array( 'rid=? and report_by=? and date_reported > ?', $index->id, $member, time() - ( Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
|
||||
$report = Db::i()->select( '*', 'core_rc_reports', ['rid=? and report_by=? and date_reported > ?', $index->id, $member->member_id, time() - ( Settings::i()->automoderation_report_again_mins * 60 )] )->first();
|
||||
|
||||
/* They have aleady reported, so do nothing */
|
||||
/* They have already reported, so do nothing */
|
||||
throw new Exception( 'REPORTED_ALREADY', '1S425/C', 404 );
|
||||
}
|
||||
catch( \Exception $e )
|
||||
|
||||
@@ -231,10 +231,19 @@ class ItemController extends Controller
|
||||
$class = $this->class;
|
||||
|
||||
$item = $class::load( $id );
|
||||
if ( $this->member and !$item->can( 'read', $this->member ) )
|
||||
|
||||
if( $this->member )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
if ( method_exists($item, 'canView') and !$item->canView( $this->member ) )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
else if ( !$item->can( 'read', $this->member ) )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return new Response( 200, $item->apiOutput( $this->member ) );
|
||||
}
|
||||
@@ -571,9 +580,16 @@ class ItemController extends Controller
|
||||
* @var Comment $commentClass */
|
||||
$itemClass = $this->class;
|
||||
$item = $itemClass::load( $id );
|
||||
if ( $this->member and !$item->can( 'read', $this->member ) )
|
||||
if( $this->member )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
if ( method_exists($item, 'canView') and !$item->canView( $this->member ) )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
else if ( !$item->can( 'read', $this->member ) )
|
||||
{
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
}
|
||||
$itemIdColumn = $itemClass::$databaseColumnId;
|
||||
$where [] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$itemIdColumn );
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace IPS\Content;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DomainException;
|
||||
use IPS\Api\Webhook;
|
||||
use IPS\core\Assignments\Assignment;
|
||||
use IPS\Data\Cache;
|
||||
use IPS\Data\Store;
|
||||
@@ -94,6 +95,12 @@ trait Assignable
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'assign', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
return $this->containerAllowsAssignable() AND static::modPermission( 'assign', $member, $this->container() );
|
||||
}
|
||||
|
||||
@@ -290,6 +297,8 @@ trait Assignable
|
||||
$this->save();
|
||||
}
|
||||
|
||||
Webhook::fire( 'content_assigned', $assignment );
|
||||
|
||||
return $assignTo;
|
||||
}
|
||||
|
||||
@@ -302,7 +311,9 @@ trait Assignable
|
||||
{
|
||||
try
|
||||
{
|
||||
Assignment::loadByItem( $this )->close();
|
||||
$assignment = Assignment::loadByItem( $this );
|
||||
$assignment->close();
|
||||
Webhook::fire( 'content_unassigned', $assignment );
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ use IPS\Helpers\Form\Text;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Member\Club\Template;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Output;
|
||||
use IPS\Output\UI\UiExtension;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Task;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function count;
|
||||
@@ -107,8 +109,6 @@ trait ClubContainer
|
||||
*/
|
||||
public function club(): Club|NULL
|
||||
{
|
||||
if ( Settings::i()->clubs )
|
||||
{
|
||||
$clubIdColumn = $this->clubIdColumn();
|
||||
if ( $this->$clubIdColumn )
|
||||
{
|
||||
@@ -118,7 +118,6 @@ trait ClubContainer
|
||||
}
|
||||
catch ( OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -179,9 +178,10 @@ trait ClubContainer
|
||||
*
|
||||
* @param Club $club The club
|
||||
* @param array $values Values
|
||||
* @param Template|null $template
|
||||
* @return void
|
||||
*/
|
||||
public function saveClubForm( Club $club, array $values ): void
|
||||
public function saveClubForm( Club $club, array $values, ?Template $template = null ): void
|
||||
{
|
||||
/* @var Model $nodeClass */
|
||||
$nodeClass = get_called_class();
|
||||
@@ -219,11 +219,18 @@ trait ClubContainer
|
||||
'node_class' => $nodeClass,
|
||||
'node_id' => $this->_id,
|
||||
'name' => $values['club_node_name'],
|
||||
'public' => $values['club_node_public'] ?? 0,
|
||||
'creators' => $values['club_node_creators'] ?? 0
|
||||
'public' => $values['club_node_public'] ?? Club::NODE_PRIVATE,
|
||||
'creators' => $values['club_node_creators'] ?? 0,
|
||||
'template_id' => $template ? $template->id : 0
|
||||
) );
|
||||
|
||||
$needToUpdatePermissions = TRUE;
|
||||
|
||||
/* If auto-follow is enabled in the club, queue the task so all members will follow the node */
|
||||
if( $this->auto_follow )
|
||||
{
|
||||
Task::queue( 'core', 'AutoFollowClubs', [ 'club' => $this->club()->id, 'node' => $nodeClass, 'nodeId' => $this->_id ], 3, [ 'club', 'node', 'nodeId' ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -235,7 +242,7 @@ trait ClubContainer
|
||||
$this->save();
|
||||
Db::i()->update( 'core_clubs_node_map', array(
|
||||
'name' => $values['club_node_name'],
|
||||
'public' => isset( $values['club_node_public'] ) ? (int) $values['club_node_public'] : 0,
|
||||
'public' => isset( $values['club_node_public'] ) ? (int) $values['club_node_public'] : Club::NODE_PRIVATE,
|
||||
'creators' => isset( $values['club_node_creators'] ) ? (int) $values['club_node_creators'] : 0
|
||||
), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
|
||||
}
|
||||
@@ -313,10 +320,21 @@ trait ClubContainer
|
||||
$insert[ 'perm_' . $v ] = '*';
|
||||
break;
|
||||
case $club::TYPE_CLOSED:
|
||||
$insert[ 'perm_' . $v ] = ( $this->isPublic() ) ? "*" : "cm,c{$club->id}";
|
||||
switch( $this->isPublic() )
|
||||
{
|
||||
case Club::NODE_PRIVATE:
|
||||
$insert['perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
case Club::NODE_PUBLIC:
|
||||
$insert['perm_' . $v ] = '*';
|
||||
break;
|
||||
case Club::NODE_MODERATORS:
|
||||
$insert['perm_' . $v ] = 'cm';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case $club::TYPE_PRIVATE:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
$insert[ 'perm_' . $v ] = ( $this->isPublic() == Club::NODE_MODERATORS ) ? "cm" : "cm,c{$club->id}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +510,7 @@ trait ClubContainer
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
elseif ( $member->modPermission( "can_{$type}_{$title}" ) and is_array( $member->modPermission( static::$modPerm ) ) and $member->modPermission('can_access_all_clubs') )
|
||||
elseif ( $member->modPermission( "can_{$type}_{$title}" ) and ( is_array( $member->modPermission( static::$modPerm ) ) or $member->modPermission( static::$modPerm ) === true ) and $member->modPermission('can_access_all_clubs') )
|
||||
{
|
||||
if ( in_array( $type, explode( ',', Settings::i()->clubs_modperms ) ) )
|
||||
{
|
||||
@@ -518,7 +536,7 @@ trait ClubContainer
|
||||
}
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
return 0;
|
||||
return Club::NODE_PRIVATE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,4 +556,37 @@ trait ClubContainer
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Template|null
|
||||
*/
|
||||
public function template() : ?Template
|
||||
{
|
||||
try
|
||||
{
|
||||
$templateId = Db::i()->select( 'template_id', 'core_clubs_node_map', [ 'node_class=? and node_id=?', get_called_class(), $this->_id ] )->first();
|
||||
return Template::load( $templateId );
|
||||
}
|
||||
catch( UnderflowException | OutOfRangeException ){}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the club leader can manage this node
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canLeaderManage() : bool
|
||||
{
|
||||
if( $template = $this->template() )
|
||||
{
|
||||
if( array_key_exists( 'clubtemplates_leader_edit', $template->node_data ) )
|
||||
{
|
||||
return $template->node_data['clubtemplates_leader_edit'];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+73
-54
@@ -291,6 +291,31 @@ abstract class Comment extends Content
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* Send webhook */
|
||||
if ( IPS::classUsesTrait( $obj, 'IPS\Content\Hideable' ) and in_array( $obj->hidden(), array( -1, 0, 1 ) ) ) // i.e. not post before register or pending deletion
|
||||
{
|
||||
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_create', $obj, $obj->webhookFilters() );
|
||||
}
|
||||
|
||||
/* DataLayer Event */
|
||||
if ( DataLayer::enabled() and static::dataLayerEventActive( 'comment_create' ) and !( $item::$firstCommentRequired and $obj->isFirst() ) )
|
||||
{
|
||||
$obj->_dataLayerProperties = [];
|
||||
$event = 'comment_create';
|
||||
$properties = $obj->getDataLayerProperties();
|
||||
|
||||
/* todo this should probably be deleted as I'm not sure why it's here. Leaving here for a reference just in case */
|
||||
// if ( $properties['content_area'] === 'personal_messages' )
|
||||
// {
|
||||
// return NULL;
|
||||
// }
|
||||
|
||||
DataLayer::i()->addEvent( $event, $properties );
|
||||
}
|
||||
|
||||
/* Update item */
|
||||
$obj->postCreate();
|
||||
|
||||
/* Send notifications and dish out points */
|
||||
if ( !in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
@@ -307,34 +332,6 @@ abstract class Comment extends Content
|
||||
$obj->sendUnapprovedNotification();
|
||||
}
|
||||
}
|
||||
|
||||
/* Send webhook */
|
||||
if ( IPS::classUsesTrait( $obj, 'IPS\Content\Hideable' ) and in_array( $obj->hidden(), array( -1, 0, 1 ) ) ) // i.e. not post before register or pending deletion
|
||||
{
|
||||
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_create', $obj, $obj->webhookFilters() );
|
||||
}
|
||||
|
||||
/* DataLayer Event */
|
||||
if ( Settings::i()->core_datalayer_enabled and !( $item::$firstCommentRequired and $obj->isFirst() ) )
|
||||
{
|
||||
$event = 'comment_create';
|
||||
$properties = $obj->getDataLayerProperties();
|
||||
|
||||
if ( $properties['content_area'] === 'personal_messages' )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$properties = array_merge( $properties, array(
|
||||
'author_id' => $obj->author()->member_id,
|
||||
'author_name' => $obj->author()->name ?? 'Guest',
|
||||
) );
|
||||
|
||||
DataLayer::i()->addEvent( $event, $properties );
|
||||
}
|
||||
|
||||
/* Update item */
|
||||
$obj->postCreate();
|
||||
|
||||
/* This needs to happen before the index is added */
|
||||
if( $item::$firstCommentRequired and $obj->isFirst() )
|
||||
@@ -372,13 +369,7 @@ abstract class Comment extends Content
|
||||
{
|
||||
if ( empty( $this->_dataLayerProperties ) )
|
||||
{
|
||||
$item = $this->item();
|
||||
$this->_dataLayerProperties = $item->getDataLayerProperties( $this );
|
||||
if ( $this->_dataLayerProperties['content_type'] === 'question' )
|
||||
{
|
||||
$this->_dataLayerProperties['comment_type'] = 'answer';
|
||||
}
|
||||
$this->_dataLayerProperties = DataLayer::i()->filterProperties( $this->_dataLayerProperties );
|
||||
$this->_dataLayerProperties = $this->item()->getDataLayerProperties( $this, clearCache: true );
|
||||
}
|
||||
|
||||
return $this->_dataLayerProperties;
|
||||
@@ -728,6 +719,12 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function canView( Member|null $member= null ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'view', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( $member === NULL )
|
||||
{
|
||||
$member = Member::loggedIn();
|
||||
@@ -749,6 +746,12 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function canEdit( Member|null $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'edit', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'edit', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -805,6 +808,12 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function canDelete( Member|null $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'delete', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'delete', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -823,6 +832,12 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function canSplit( Member|null $member=null ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'split', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'split', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -869,14 +884,7 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function dateLine(): string
|
||||
{
|
||||
if( $this->mapped('first') )
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( static::$formLangPrefix . 'date_started', FALSE, array( 'htmlsprintf' => array( DateTime::ts( $this->mapped('date') )->html() ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( static::$formLangPrefix . 'date_replied', FALSE, array( 'htmlsprintf' => array( DateTime::ts( $this->mapped('date') )->html() ) ) );
|
||||
}
|
||||
return Member::loggedIn()->language()->addToStack( static::$formLangPrefix . 'date_replied', FALSE, array( 'htmlsprintf' => array( DateTime::ts( $this->mapped('date') )->html() ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1014,11 +1022,8 @@ abstract class Comment extends Content
|
||||
Db::i()->update( 'core_reputation_index', array( 'item_id' => $item->$idColumn ), array( 'class_type_id_hash=?', md5( get_class( $this ) . ':' . $oldItem->$idColumn ) ) );
|
||||
}
|
||||
|
||||
/* Update solved */
|
||||
if ( IPS::classUsesTrait( $this, 'IPS\Content\Solvable' ) )
|
||||
{
|
||||
Db::i()->update( 'core_solved_index', array( 'item_id' => $item->$idColumn ), array( 'comment_class=? and comment_id=?', get_class( $this ), $oldItem->$idColumn ) );
|
||||
}
|
||||
/* Update solved - do this without checking for the trait, because multiple traits use it */
|
||||
Db::i()->update( 'core_solved_index', array( 'item_id' => $item->$idColumn ), array( 'comment_class=? and comment_id=?', get_class( $this ), $this->$commentIdColumn ) );
|
||||
|
||||
/* Update member map */
|
||||
try
|
||||
@@ -1037,6 +1042,13 @@ abstract class Comment extends Content
|
||||
|
||||
if( $skip === FALSE )
|
||||
{
|
||||
/* Update Helpfuls */
|
||||
if( IPS::classUsesTrait( $this, Helpful::class ) and isset( $item::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$oldItem->recountHelpfuls();
|
||||
$item->recountHelpfuls();
|
||||
}
|
||||
|
||||
$oldItem->rebuildFirstAndLastCommentData();
|
||||
$item->rebuildFirstAndLastCommentData();
|
||||
|
||||
@@ -1119,7 +1131,7 @@ abstract class Comment extends Content
|
||||
}
|
||||
|
||||
/* Remove featured comment associations */
|
||||
if( $item and IPS::classUsesTrait( $item, 'IPS\Content\MetaData' ) and $this->isFeatured() )
|
||||
if( $item and $this->isFeaturedComment() )
|
||||
{
|
||||
Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $item, $this );
|
||||
}
|
||||
@@ -1186,16 +1198,23 @@ abstract class Comment extends Content
|
||||
if ( $item->mapped('solved_comment_id') and $item->mapped('solved_comment_id') == $this->$idColumn )
|
||||
{
|
||||
Db::i()->update( $itemClass::$databaseTable, array( $itemClass::$databaseColumnMap['solved_comment_id'] => 0 ), array( $itemIdColumn . '=?', $itemId ) );
|
||||
|
||||
Db::i()->delete( 'core_solved_index', array( 'app=? and comment_class=? and item_id=? and comment_id=?', static::$application, get_called_class(), $itemId, $this->$idColumn ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear the solved index regardless of trait; multiple traits use it */
|
||||
Db::i()->delete( 'core_solved_index', array( 'app=? and comment_class=? and item_id=? and comment_id=?', static::$application, get_called_class(), $itemId, $this->$idColumn ) );
|
||||
|
||||
if ( $item AND IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$item->clearCachedStatistics( TRUE );
|
||||
}
|
||||
|
||||
/* Update helpfuls */
|
||||
if( IPS::classUsesTrait( $this, Helpful::class ) and IPS::classUsesTrait( $item, Helpful::class ) )
|
||||
{
|
||||
$item->recountHelpfuls();
|
||||
}
|
||||
|
||||
/* Delete any notifications telling people about this */
|
||||
$memberIds = array();
|
||||
|
||||
@@ -1516,7 +1535,7 @@ abstract class Comment extends Content
|
||||
|
||||
/* @var array $databaseColumnMap */
|
||||
/* Finally, we do not want posts from future items */
|
||||
if ( in_array( 'IPS\Content\FuturePublishing', class_implements( $itemClass ) ) )
|
||||
if( IPS::classUsesTrait( $itemClass, FuturePublishing::class ) )
|
||||
{
|
||||
$itemWhere[] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['is_future_entry'] . '=0' );
|
||||
}
|
||||
@@ -1739,9 +1758,9 @@ abstract class Comment extends Content
|
||||
*/
|
||||
public function isFeaturedComment(): bool
|
||||
{
|
||||
if ( IPS::classUsesTrait( $this->item(), 'IPS\Content\MetaData' ) )
|
||||
if ( !( $this instanceof Review ) and IPS::classUsesTrait( $this->item(), 'IPS\Content\MetaData' ) )
|
||||
{
|
||||
return Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->isFeaturedComment( $this );
|
||||
return Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->isCommentShownAtTheTop( $this );
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
|
||||
@@ -25,7 +25,9 @@ use IPS\Content\Item;
|
||||
use IPS\Content\Review;
|
||||
use IPS\Content\Solvable;
|
||||
use IPS\core\Approval;
|
||||
use IPS\core\DataLayer;
|
||||
use IPS\core\Reports\Report;
|
||||
use IPS\core\Reports\Types;
|
||||
use IPS\Db\Select;
|
||||
use IPS\Events\Event;
|
||||
use IPS\Http\Url;
|
||||
@@ -76,6 +78,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
* @mixin \IPS\Content\EditHistory
|
||||
* @mixin \IPS\Content\Followable
|
||||
* @mixin \IPS\Content\FuturePublishing
|
||||
* @mixin \IPS\Content\Helpful
|
||||
* @mixin \IPS\Content\Hideable
|
||||
* @mixin \IPS\Content\ItemTopic
|
||||
* @mixin \IPS\Content\Lockable
|
||||
@@ -125,6 +128,12 @@ abstract class Content extends ActiveRecord
|
||||
*/
|
||||
public static bool $canBeModeratedFromFrontend = TRUE;
|
||||
|
||||
/**
|
||||
* An array containing data layer event keys to bypass. Optionally, this can be set to 'true' to bypass all events.
|
||||
* @var bool|string[]
|
||||
*/
|
||||
protected static array|bool $_bypassDataLayerEvents = [];
|
||||
|
||||
/**
|
||||
* Should posting this increment the poster's post count?
|
||||
*
|
||||
@@ -680,6 +689,7 @@ abstract class Content extends ActiveRecord
|
||||
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_modaction', ['action' => $action, 'item' => $this->apiOutput()], $this->webhookFilters() );
|
||||
|
||||
/* These ones just need a property setting */
|
||||
$dataLayerEvent = null;
|
||||
if ( in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'lock', 'unlock' ) ) )
|
||||
{
|
||||
$val = TRUE;
|
||||
@@ -714,6 +724,11 @@ abstract class Content extends ActiveRecord
|
||||
$this->$column = $val;
|
||||
$this->save();
|
||||
|
||||
if ( $action === 'pin' or $action === 'unpin' )
|
||||
{
|
||||
$dataLayerEvent = $action === 'pin' ? 'pin' : 'unpin';
|
||||
}
|
||||
|
||||
/* Fire events for these here, the others will be fired at a later point */
|
||||
Event::fire( 'onStatusChange', $this, array( $action ) );
|
||||
}
|
||||
@@ -721,10 +736,12 @@ abstract class Content extends ActiveRecord
|
||||
/* Hide is a tiny bit more complicated */
|
||||
elseif ( $action === 'hide' )
|
||||
{
|
||||
$dataLayerEvent = 'hide';
|
||||
$this->hide( $member, $reason );
|
||||
}
|
||||
elseif ( $action === 'unhide' )
|
||||
{
|
||||
$dataLayerEvent = 'unhide';
|
||||
$this->unhide( $member );
|
||||
}
|
||||
|
||||
@@ -737,8 +754,8 @@ abstract class Content extends ActiveRecord
|
||||
$this->logDelete( $member );
|
||||
return;
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
$dataLayerEvent = 'delete';
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
@@ -769,6 +786,11 @@ abstract class Content extends ActiveRecord
|
||||
$this->move( $args[2][0], $args[2][1] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $dataLayerEvent and DataLayer::enabled( 'analytics_full' ) and static::dataLayerEventActive( 'content_' . $dataLayerEvent ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_' . $dataLayerEvent, $this->getDataLayerProperties() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -902,6 +924,7 @@ abstract class Content extends ActiveRecord
|
||||
$item = ( $this instanceof Comment ) ? $this->item() : $this;
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
|
||||
$new = false;
|
||||
try
|
||||
{
|
||||
$index = Report::load( $this->$idColumn, 'content_id', array( 'class=?', get_called_class() ) );
|
||||
@@ -909,6 +932,7 @@ abstract class Content extends ActiveRecord
|
||||
}
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
$new = true;
|
||||
$index = new Report;
|
||||
$index->class = get_called_class();
|
||||
$index->content_id = $this->$idColumn;
|
||||
@@ -991,7 +1015,7 @@ abstract class Content extends ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
$notification = new Notification( Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) );
|
||||
$notification = new Notification( Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this, $member ) );
|
||||
foreach (Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $mem )
|
||||
{
|
||||
$memberObj = Member::constructFromData( $mem );
|
||||
@@ -1022,6 +1046,33 @@ abstract class Content extends ActiveRecord
|
||||
$this->alreadyReported = TRUE;
|
||||
$this->reportData = $reportInsert;
|
||||
|
||||
/* If this is new, we need to add a data layer event */
|
||||
if ( $index->status === 1 and DataLayer::enabled( 'analytics_full' ) and static::dataLayerEventActive( 'content_report' ) )
|
||||
{
|
||||
$properties = $this->getDataLayerProperties();
|
||||
try
|
||||
{
|
||||
$reportNode = ( ( $member->member_id or !Settings::i()->automoderation_enabled ) and $reportType > 0 ) ? Types::load( $reportType ) : null;
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
$reportNode = null;
|
||||
}
|
||||
|
||||
$properties['report_type_name'] = Lang::load( Lang::defaultLanguage() )->addToStack( $reportNode?->_titleLanguageKey ?: ( $this instanceof Content\Comment ? 'report_message_comment' : 'report_message_item' ) );
|
||||
$properties['report_type_id'] = $reportNode ? $reportType : 0;
|
||||
|
||||
Lang::load( Lang::defaultLanguage() )->parseOutputForDisplay( $properties['report_type_name'] );
|
||||
DataLayer::i()->addEvent( 'content_report', DataLayer::i()->filterProperties( $properties ) );
|
||||
}
|
||||
|
||||
if( $new )
|
||||
{
|
||||
Webhook::fire( 'content_reported', $index);
|
||||
|
||||
Event::fire( 'onReport', $this, [ $index ] );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $index;
|
||||
}
|
||||
@@ -2002,4 +2053,18 @@ abstract class Content extends ActiveRecord
|
||||
|
||||
return (string) $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an event is active for this content type based on the event key. For example, reports don't fire any content_* or comment_* events. This uses the $_bypassDataLayerEvents static property
|
||||
*
|
||||
* @see Content::$_bypassDataLayerEvents
|
||||
*
|
||||
* @param string $eventKey The data layer event key.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function dataLayerEventActive( string $eventKey ) : bool
|
||||
{
|
||||
return empty( static::$_bypassDataLayerEvents ) or ( is_array( static::$_bypassDataLayerEvents ) and !in_array( $eventKey, static::$_bypassDataLayerEvents ) );
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ class Controller extends CoverPhotoController
|
||||
|
||||
/* Ensure JS loaded for forms/content functions such as moderation */
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'front_core.js', 'core' ) );
|
||||
Output::i()->bodyAttributes['contentClass'] = static::$contentModel;
|
||||
|
||||
parent::execute();
|
||||
|
||||
@@ -152,6 +153,7 @@ class Controller extends CoverPhotoController
|
||||
try
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
/**@var Content $item */
|
||||
$item = $class::loadAndCheckPerms( Request::i()->id );
|
||||
|
||||
/* Have we moved? If a user has loaded a forum, and a moderator merges two topics leaving a link, then we need to account for this if the
|
||||
@@ -215,9 +217,8 @@ class Controller extends CoverPhotoController
|
||||
}
|
||||
|
||||
/* Get ready to store data layer info */
|
||||
$dataLayer = Settings::i()->core_datalayer_enabled;
|
||||
$dataLayerProperties = array();
|
||||
if ( $dataLayer )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
$dataLayerProperties = $item->getDataLayerProperties();
|
||||
}
|
||||
@@ -343,9 +344,9 @@ class Controller extends CoverPhotoController
|
||||
);
|
||||
|
||||
/* Data Layer */
|
||||
if ( $dataLayer )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
if ( !Request::i()->isAjax() )
|
||||
if ( !Request::i()->isAjax() and $item::dataLayerEventActive( 'content_view' ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_view', $dataLayerProperties );
|
||||
}
|
||||
@@ -414,9 +415,17 @@ class Controller extends CoverPhotoController
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( ! is_int( Request::i()->id ) )
|
||||
{
|
||||
throw new BadMethodCallException( $e );
|
||||
}
|
||||
Output::i()->redirect( $class::getRedirectFrom( Request::i()->id )->url() );
|
||||
}
|
||||
catch( OutOfRangeException $e ) { }
|
||||
catch( BadMethodCallException|UnderflowException $e )
|
||||
{
|
||||
/* There is no valid redirect, so we will just show an error */
|
||||
Output::i()->error( 'page_doesnt_exist', '2S136/1Z', 404, '' );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -460,9 +469,9 @@ class Controller extends CoverPhotoController
|
||||
/**
|
||||
* AJAX - check for new replies
|
||||
*
|
||||
* @return Item|NULL
|
||||
* @return void
|
||||
*/
|
||||
protected function checkForNewReplies(): Item|NULL
|
||||
protected function checkForNewReplies(): void
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
@@ -1610,7 +1619,7 @@ class Controller extends CoverPhotoController
|
||||
'blurb' => ( Settings::i()->reaction_count_display == 'count' ) ? '' : Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $item )
|
||||
);
|
||||
|
||||
if ( Settings::i()->core_datalayer_enabled )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
$output['datalayer'] = array_replace( $item->getDataLayerProperties(), ['reaction_type' => $reaction->_title] );
|
||||
}
|
||||
@@ -1619,7 +1628,7 @@ class Controller extends CoverPhotoController
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( Settings::i()->core_datalayer_enabled )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_react', array_replace( $item->getDataLayerProperties(), [ 'reaction_type' => $reaction->_title ] ) );
|
||||
}
|
||||
@@ -1750,10 +1759,12 @@ class Controller extends CoverPhotoController
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( Request::i()->id );
|
||||
|
||||
$reactionId = isset( Request::i()->reaction ) ? (int) Request::i()->reaction : null;
|
||||
|
||||
if ( Request::i()->isAjax() and isset( Request::i()->tooltip ) and isset( Request::i()->reaction ) )
|
||||
if ( Request::i()->isAjax() and isset( Request::i()->tooltip ) and !empty( $reactionId ) )
|
||||
{
|
||||
$reaction = Reaction::load( Request::i()->reaction );
|
||||
$reaction = Reaction::load( $reactionId );
|
||||
|
||||
$numberToShowInPopup = 10;
|
||||
$where = $item->getReactionWhereClause( $reaction );
|
||||
@@ -1779,12 +1790,8 @@ class Controller extends CoverPhotoController
|
||||
$tabs[ $reaction->id ] = array( 'title' => $reaction->_title, 'icon' => $reaction->_icon, 'count' => $blurb[$reaction->id] ?? 0 );
|
||||
}
|
||||
}
|
||||
|
||||
$activeTab = 'all';
|
||||
if ( isset( Request::i()->reaction ) )
|
||||
{
|
||||
$activeTab = Request::i()->reaction;
|
||||
}
|
||||
|
||||
$activeTab = $reactionId ?: 'all';
|
||||
|
||||
$url = $item->url('showReactions');
|
||||
$url = $url->setQueryString( 'changed', 1 );
|
||||
@@ -3017,7 +3024,13 @@ class Controller extends CoverPhotoController
|
||||
|
||||
/* Moderator log */
|
||||
Session::i()->modLog( 'modlog__comment_edit', array( $comment->url()->__toString() => FALSE, $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped( 'title' ) => FALSE ), $item );
|
||||
|
||||
|
||||
/* Add a data layer event indicating this was updated */
|
||||
if ( DataLayer::enabled( 'analytics_full' ) and $comment::dataLayerEventActive( 'content_edit' ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_edit', $comment->getDataLayerProperties() );
|
||||
}
|
||||
|
||||
/* If this is an AJAX request and the comment hidden status has not changed just output the comment HTML */
|
||||
if ( Request::i()->isAjax() AND $isHidden == $comment->hidden() AND $reloadPage === false )
|
||||
{
|
||||
@@ -3117,6 +3130,12 @@ class Controller extends CoverPhotoController
|
||||
$item->topic_answered_pid = 0;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
/* This doesn't go through modAction, so we will add the datalayer event here */
|
||||
if ( $comment and DataLayer::enabled( 'analytics_full' ) and $comment::dataLayerEventActive( 'content_delete' ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_delete', $comment->getDataLayerProperties() );
|
||||
}
|
||||
|
||||
if ( Request::i()->isAjax() )
|
||||
{
|
||||
@@ -3239,6 +3258,8 @@ class Controller extends CoverPhotoController
|
||||
|
||||
$comment->save();
|
||||
}
|
||||
|
||||
Event::fire( 'onItemSplit', $item, array( $oldItem ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3520,7 +3541,7 @@ class Controller extends CoverPhotoController
|
||||
'blurb' => ( Settings::i()->reaction_count_display == 'count' ) ? '' : Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $comment )
|
||||
);
|
||||
|
||||
if ( Settings::i()->core_datalayer_enabled )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
$item = $item ?? $comment->item();
|
||||
$object = ( !isset( $item::$databaseColumnMap['content'] ) AND ( $item::$commentClass ) AND $comment->isFirst() ) ? $item : $comment;
|
||||
@@ -3533,7 +3554,7 @@ class Controller extends CoverPhotoController
|
||||
else
|
||||
{
|
||||
/* Data Layer Event */
|
||||
if ( Settings::i()->core_datalayer_enabled )
|
||||
if ( DataLayer::enabled() )
|
||||
{
|
||||
$properties = $comment->getDataLayerProperties();
|
||||
$properties['reaction_type'] = $reaction->_title;
|
||||
@@ -3651,10 +3672,11 @@ class Controller extends CoverPhotoController
|
||||
{
|
||||
/* @var Comment $commentClass */
|
||||
$idColumn = $commentClass::$databaseColumnId;
|
||||
|
||||
if ( Request::i()->isAjax() and isset( Request::i()->tooltip ) and isset( Request::i()->reaction ) )
|
||||
|
||||
$reactionId = isset( Request::i()->reaction ) ? (int) Request::i()->reaction : null;
|
||||
if ( Request::i()->isAjax() and isset( Request::i()->tooltip ) and !empty( $reactionId ) )
|
||||
{
|
||||
$reaction = Reaction::load( Request::i()->reaction );
|
||||
$reaction = Reaction::load( $reactionId );
|
||||
|
||||
$numberToShowInPopup = 10;
|
||||
$where = $comment->getReactionWhereClause( $reaction );
|
||||
@@ -3680,11 +3702,7 @@ class Controller extends CoverPhotoController
|
||||
}
|
||||
}
|
||||
|
||||
$activeTab = 'all';
|
||||
if ( isset( Request::i()->reaction ) )
|
||||
{
|
||||
$activeTab = Request::i()->reaction;
|
||||
}
|
||||
$activeTab = $reactionId ?: 'all';
|
||||
|
||||
$url = $comment->url('showReactions');
|
||||
$url = $url->setQueryString( 'changed', 1 );
|
||||
@@ -3721,6 +3739,10 @@ class Controller extends CoverPhotoController
|
||||
Session::i()->csrfCheck();
|
||||
|
||||
$checkAgainst = Request::i()->modaction;
|
||||
if( !$checkAgainst )
|
||||
{
|
||||
throw new DomainException;
|
||||
}
|
||||
|
||||
$classToCheck = $commentClass;
|
||||
if( $checkAgainst == 'split' OR $checkAgainst == 'merge' )
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace IPS\Content;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Content\Item;
|
||||
use IPS\IPS;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
@@ -90,15 +91,23 @@ trait EditHistory
|
||||
{
|
||||
$logEdit = 'comment_log_edit';
|
||||
$editReason = 'comment_edit_reason';
|
||||
$column = 'comment_value';
|
||||
$column = static::$formLangPrefix . 'comment_value';
|
||||
|
||||
/* If this is the first comment and the first comment is required, the content will be in a different array element */
|
||||
/* @var Item $itemClass */
|
||||
$itemClass = static::$itemClass;
|
||||
if( $itemClass::$firstCommentRequired and $this->isFirst() )
|
||||
{
|
||||
$column = $itemClass::$formLangPrefix . 'content';
|
||||
}
|
||||
}
|
||||
|
||||
$editIsPublic = Member::loggedIn()->group['g_append_edit'] ? $values[ $logEdit ] : TRUE;
|
||||
$idField = static::$databaseColumnId;
|
||||
|
||||
if ( Settings::i()->edit_log == 2 and isset( $values[static::$formLangPrefix . $column] ) )
|
||||
if ( Settings::i()->edit_log == 2 and isset( $values[ $column ] ) )
|
||||
{
|
||||
$content = $values[static::$formLangPrefix . $column];
|
||||
$content = $values[ $column ];
|
||||
|
||||
Db::i()->insert( 'core_edit_history', array(
|
||||
'class' => get_class( $this ),
|
||||
|
||||
@@ -13,13 +13,13 @@ namespace IPS\Content;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\core\Feature;
|
||||
use IPS\Events\Event;
|
||||
use IPS\File;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use OutOfRangeException;
|
||||
use function class_implements;
|
||||
use function get_called_class;
|
||||
use function in_array;
|
||||
|
||||
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -37,6 +37,12 @@ trait Featurable
|
||||
*/
|
||||
public function canFeature( ?Member $member=NULL ) : bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'feature', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'feature', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -113,6 +119,9 @@ trait Featurable
|
||||
|
||||
$this->$featuredColumn = $featured;
|
||||
$this->save();
|
||||
|
||||
/* Fire an event */
|
||||
Event::fire( 'onStatusChange', $this, array( ( $featured ? 'feature' : 'unfeature' ) ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -130,7 +139,7 @@ trait Featurable
|
||||
{
|
||||
$where = array( array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
|
||||
if ( in_array( 'IPS\Content\FuturePublishing', class_implements( get_called_class() ) ) )
|
||||
if( IPS::classUsesTrait( get_called_class(), FuturePublishing::class ) )
|
||||
{
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ namespace IPS\Content;
|
||||
use ArrayObject;
|
||||
use BadMethodCallException;
|
||||
use IPS\Content;
|
||||
use IPS\core\Followed\Follow;
|
||||
use IPS\Db;
|
||||
use IPS\Events\Event;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Notification;
|
||||
use IPS\Application;
|
||||
use IPS\DateTime;
|
||||
@@ -22,6 +25,7 @@ use IPS\Node\Model;
|
||||
use IPS\Db\Select;
|
||||
use ArrayIterator;
|
||||
use Iterator;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function defined;
|
||||
use function header;
|
||||
@@ -44,6 +48,113 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
trait Followable
|
||||
{
|
||||
/**
|
||||
* Follow an object. Using a static method here
|
||||
* because Model does not use the Followable trait
|
||||
*
|
||||
* @param object $object
|
||||
* @param string $frequency
|
||||
* @param bool $public
|
||||
* @param Member|null $member
|
||||
* @return void
|
||||
*/
|
||||
public static function staticFollow( object $object, string $frequency, bool $public=true, ?Member $member=null ) : void
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
try
|
||||
{
|
||||
/* Are we already following this? */
|
||||
$follow = Follow::loadByObject( $object, $member );
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
$follow = new Follow;
|
||||
$follow->object = $object;
|
||||
$follow->member_id = $member->member_id;
|
||||
}
|
||||
|
||||
$follow->is_anon = !$public;
|
||||
$follow->notify_do = ( $frequency == 'none' ? 0 : 1 );
|
||||
$follow->notify_freq = $frequency;
|
||||
$follow->save();
|
||||
$followParams = Follow::getFollowParameters( $object );
|
||||
$idCol = $object::$databaseColumnId;
|
||||
$member->clearFollowingCache( $followParams['app'], $followParams['area'], $object->$idCol );
|
||||
|
||||
/* Fire the event */
|
||||
Event::fire( 'onFollow', $member, [ $object, $public ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow this object
|
||||
*
|
||||
* @param string $frequency ( 'none', 'immediate', 'daily', 'weekly' )
|
||||
* @param bool $public
|
||||
* @param Member|null $member
|
||||
* @return void
|
||||
*/
|
||||
public function follow( string $frequency, bool $public=true, ?Member $member=null ) : void
|
||||
{
|
||||
static::staticFollow( $this, $frequency, $public, $member );
|
||||
|
||||
/* If this is an item, handle achievements */
|
||||
if( $this instanceof Item )
|
||||
{
|
||||
Member::loggedIn()->achievementAction( 'core', 'FollowContentItem', [
|
||||
'item' => $this,
|
||||
'author' => $this->author()
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nodes don't use followable, so we have a static method on the item level
|
||||
*
|
||||
* @param Model $container
|
||||
* @param Member|null $member
|
||||
* @return void
|
||||
*/
|
||||
public static function containerUnfollow( Model $container, ?Member $member=null ) : void
|
||||
{
|
||||
try
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
Follow::loadByObject( $container, $member )->delete();
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfollow this object
|
||||
*
|
||||
* @param Member|null $member
|
||||
* @param string|null $followId
|
||||
* @return void
|
||||
*/
|
||||
public function unfollow( ?Member $member=null, ?string $followId=null ) : void
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
try
|
||||
{
|
||||
/* We might already have the follow ID (like in the notifications controller) */
|
||||
if( $followId === null )
|
||||
{
|
||||
Follow::loadByObject( $this, $member )->delete();
|
||||
|
||||
$followParams = Follow::getFollowParameters( $this );
|
||||
$idCol = $this::$databaseColumnId;
|
||||
$member->clearFollowingCache( $followParams['app'], $followParams['area'], $this->$idCol );
|
||||
}
|
||||
else
|
||||
{
|
||||
Follow::load( $followId )->delete();
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cache for current follow data, used on "My Followed Content" screen
|
||||
*/
|
||||
|
||||
@@ -75,6 +75,12 @@ trait FuturePublishing
|
||||
*/
|
||||
public function canPublish( ?Member $member=NULL, ?Model $container = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'publish', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'publish', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -85,6 +85,26 @@ trait Helpful
|
||||
'node_id' => $this->item()->container()->_id
|
||||
) );
|
||||
|
||||
/* If we have a mapping, increment that column */
|
||||
if( isset( static::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$helpfulCountColumn = static::$databaseColumnMap['num_helpful'];
|
||||
$this->$helpfulCountColumn++;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/* And if this is a comment, check if item has a mapping */
|
||||
if( $this instanceof Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
if( isset( $item::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$helpfulCountColumn = $item::$databaseColumnMap['num_helpful'];
|
||||
$item->$helpfulCountColumn++;
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
|
||||
/* Trending */
|
||||
if( Application::appIsEnabled( 'cloud' ) and Bridge::i()->featureIsEnabled( 'trending' ) )
|
||||
{
|
||||
@@ -141,6 +161,26 @@ trait Helpful
|
||||
unset( $this->hasMarkedHelpful[ $member->member_id ] );
|
||||
}
|
||||
|
||||
/* If we have a mapping, decrement that column */
|
||||
if( isset( static::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$helpfulCountColumn = static::$databaseColumnMap['num_helpful'];
|
||||
$this->$helpfulCountColumn--;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/* And if this is a comment, check if item has a mapping */
|
||||
if( $this instanceof Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
if( isset( $item::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$helpfulCountColumn = $item::$databaseColumnMap['num_helpful'];
|
||||
$item->$helpfulCountColumn--;
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
|
||||
if ( IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$this->item()->clearCachedStatistics();
|
||||
@@ -170,22 +210,36 @@ trait Helpful
|
||||
*/
|
||||
public function helpfulsRepliesCount(): int
|
||||
{
|
||||
if( isset( static::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
return $this->mapped( 'num_helpful' );
|
||||
}
|
||||
|
||||
if ( $this->_helpfulRepliesCount === NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
try
|
||||
{
|
||||
$this->_helpfulRepliesCount = (int)Db::i()->select( 'COUNT( DISTINCT(comment_id) )', 'core_solved_index', [["app=? AND item_id=? AND type=? AND hidden=0", static::$application, $this->$idColumn, 'helpful']], null, null, 'item_id' )->first();
|
||||
}
|
||||
catch( Exception )
|
||||
{
|
||||
$this->_helpfulRepliesCount = 0;
|
||||
}
|
||||
$this->_helpfulRepliesCount = (int) Db::i()->select( 'COUNT( DISTINCT(comment_id) )', 'core_solved_index', ["app=? AND item_id=? AND type=? AND hidden=0", static::$application, $this->$idColumn, 'helpful'] )->first();
|
||||
}
|
||||
|
||||
return $this->_helpfulRepliesCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recount the total helpful replies
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function recountHelpfuls() : void
|
||||
{
|
||||
if( isset( static::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$helpfulColumn = static::$databaseColumnMap['num_helpful'];
|
||||
$this->$helpfulColumn = (int) Db::i()->select( 'COUNT( DISTINCT(comment_id) )', 'core_solved_index', [ 'app=? and item_id=? and type=? and hidden=?', static::$application, $this->$idColumn, 'helpful', 0 ], flags: Db::SELECT_FROM_WRITE_SERVER )->first();
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpful Count
|
||||
*
|
||||
@@ -280,6 +334,12 @@ trait Helpful
|
||||
*/
|
||||
public function canMarkHelpful( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'helpful', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* It's a no for guests */
|
||||
@@ -320,7 +380,7 @@ trait Helpful
|
||||
public function authorIsAnExpert() : bool
|
||||
{
|
||||
/* Do they have it disabled? */
|
||||
if( !Bridge::i()->featureIsEnabled( 'experts' ) or $this->author()->members_bitoptions['expert_user_disabled'] or $this->author()->members_bitoptions['expert_user_blocked'] )
|
||||
if( !Bridge::i()->featureIsEnabled( 'experts' ) or $this->author()->members_bitoptions['expert_user_disabled'] or !$this->author()->isExpert() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -350,6 +410,11 @@ trait Helpful
|
||||
*/
|
||||
public static function itemHasHelpful( Item $item ): bool
|
||||
{
|
||||
if( isset( $item::$databaseColumnMap['num_helpful'] ) )
|
||||
{
|
||||
return (bool) $item->mapped( 'num_helpful' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (bool) $item->helpfulPosts(1);
|
||||
|
||||
@@ -317,6 +317,12 @@ trait Hideable
|
||||
*/
|
||||
public function canRestore( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'restore', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'restore', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -451,16 +457,27 @@ trait Hideable
|
||||
if ( $this instanceof Item )
|
||||
{
|
||||
$item = $this;
|
||||
if( isset( static::$commentClass ) )
|
||||
{
|
||||
$solvedWhere[] = array( 'comment_class=?', static::$commentClass );
|
||||
}
|
||||
|
||||
$solvedWhere[] = array( 'item_id=?', $this->$idColumn );
|
||||
}
|
||||
else
|
||||
{
|
||||
$item = $this->item();
|
||||
$solvedWhere[] = array( 'comment_class=?', get_called_class() );
|
||||
$solvedWhere[] = array( 'comment_id=?', $this->$idColumn );
|
||||
}
|
||||
|
||||
Db::i()->update( 'core_solved_index', [ 'hidden' => 1 ], $solvedWhere );
|
||||
|
||||
if( IPS::classUsesTrait( $item, Helpful::class ) )
|
||||
{
|
||||
$item->recountHelpfuls();
|
||||
}
|
||||
|
||||
/* Update search index */
|
||||
if( SearchContent::isSearchable( $this ) )
|
||||
{
|
||||
@@ -657,16 +674,27 @@ trait Hideable
|
||||
if ( $this instanceof Item )
|
||||
{
|
||||
$item = $this;
|
||||
if( isset( static::$commentClass ) )
|
||||
{
|
||||
$solvedWhere[] = array( 'comment_class=?', static::$commentClass );
|
||||
}
|
||||
|
||||
$solvedWhere[] = array( 'item_id=?', $this->$idColumn );
|
||||
}
|
||||
else
|
||||
{
|
||||
$item = $this->item();
|
||||
$solvedWhere[] = array( 'comment_class=?', get_called_class() );
|
||||
$solvedWhere[] = array( 'comment_id=?', $this->$idColumn );
|
||||
}
|
||||
|
||||
Db::i()->update( 'core_solved_index', [ 'hidden' => 0 ], $solvedWhere );
|
||||
|
||||
if( IPS::classUsesTrait( $item, Helpful::class ) )
|
||||
{
|
||||
$item->recountHelpfuls();
|
||||
}
|
||||
|
||||
/* Clear the item stats */
|
||||
if ( IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
@@ -1373,12 +1401,18 @@ trait Hideable
|
||||
*/
|
||||
public function canHide( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'hide', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'hide', $member ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->hidden() <> 0 )
|
||||
if ( $this->hidden() < 0 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
@@ -1400,6 +1434,12 @@ trait Hideable
|
||||
*/
|
||||
public function canUnhide( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'unhide', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'unhide', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -1492,7 +1532,13 @@ trait Hideable
|
||||
* @return bool
|
||||
*/
|
||||
public function canViewHiddenComments( ?Member $member=NULL ): bool
|
||||
{
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'view_hidden', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
$commentClass = static::$commentClass;
|
||||
|
||||
return $commentClass::modPermission( 'view_hidden', $member, $this->containerWrapper() );
|
||||
@@ -1506,6 +1552,12 @@ trait Hideable
|
||||
*/
|
||||
public function canViewHiddenReviews( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'view_hidden', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
$reviewClass = static::$reviewClass;
|
||||
|
||||
return $reviewClass::modPermission( 'view_hidden', $member, $this->containerWrapper() );
|
||||
|
||||
+243
-167
@@ -20,6 +20,7 @@ use IPS\Api\Webhook;
|
||||
use IPS\Application;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Content;
|
||||
use IPS\Content\Permissions as PermissionsExtension;
|
||||
use IPS\Content\Search\Index;
|
||||
use IPS\core\Achievements\Recognize;
|
||||
use IPS\core\Approval;
|
||||
@@ -84,6 +85,8 @@ use function is_null;
|
||||
use function is_numeric;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function mb_strrpos;
|
||||
use function mb_strtolower;
|
||||
use function substr;
|
||||
use const IPS\CIC;
|
||||
|
||||
@@ -195,12 +198,14 @@ abstract class Item extends Content
|
||||
* Build form to create
|
||||
*
|
||||
* @param Model|NULL $container Container (e.g. forum), if appropriate
|
||||
* @param bool $showError Bypass canCreate check? This will show the form even when the member is not allowed to create the item.
|
||||
* @return Form
|
||||
*/
|
||||
public static function create( Model|null $container=NULL ): Form
|
||||
public static function create( Model|null $container=NULL, bool $showError = true ): Form
|
||||
{
|
||||
/* Perform permission checks */
|
||||
static::canCreate( Member::loggedIn(), $container, TRUE );
|
||||
/* Perform permission checks */
|
||||
static::canCreate( Member::loggedIn(), $container, $showError );
|
||||
|
||||
|
||||
/* Build the form */
|
||||
$form = static::buildCreateForm( $container );
|
||||
@@ -566,26 +571,7 @@ abstract class Item extends Content
|
||||
/* Auto-follow */
|
||||
if( isset( $values[ static::$formLangPrefix . 'auto_follow'] ) AND $values[ static::$formLangPrefix . 'auto_follow'] )
|
||||
{
|
||||
$followArea = mb_strtolower( mb_substr( get_called_class(), mb_strrpos( get_called_class(), '\\' ) + 1 ) );
|
||||
|
||||
/* Insert */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$save = array(
|
||||
'follow_id' => md5( static::$application . ';' . $followArea . ';' . $obj->$idColumn . ';' . Member::loggedIn()->member_id ),
|
||||
'follow_app' => static::$application,
|
||||
'follow_area' => $followArea,
|
||||
'follow_rel_id' => $obj->$idColumn,
|
||||
'follow_member_id' => Member::loggedIn()->member_id,
|
||||
'follow_is_anon' => isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : 0,
|
||||
'follow_added' => time() + 1, // Make sure streams show follows after content is created
|
||||
'follow_notify_do' => 1,
|
||||
'follow_notify_meta' => '',
|
||||
'follow_notify_freq' => Member::loggedIn()->auto_follow['method'],
|
||||
'follow_notify_sent' => 0,
|
||||
'follow_visible' => 1
|
||||
);
|
||||
|
||||
Db::i()->insert( 'core_follow', $save );
|
||||
$obj->follow( Member::loggedIn()->auto_follow['method'], isset( $values['post_anonymously'] ) ? !$values['post_anonymously'] : true );
|
||||
}
|
||||
|
||||
/* Auto-share */
|
||||
@@ -609,7 +595,7 @@ abstract class Item extends Content
|
||||
}
|
||||
|
||||
/* Send notifications */
|
||||
if ( $sendNotification and ( IPS::classUsesTrait( $obj, 'IPS\Content\FuturePublishing' ) AND !$obj->isFutureDate() ) )
|
||||
if ( $sendNotification and ( !IPS::classUsesTrait( $obj, 'IPS\Content\FuturePublishing' ) or !$obj->isFutureDate() ) )
|
||||
{
|
||||
if ( !$obj->hidden() )
|
||||
{
|
||||
@@ -844,7 +830,12 @@ abstract class Item extends Content
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['author_name'] ) )
|
||||
{
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name'), 'htmlAutocomplete' => "username" ), NULL, NULL, NULL, 'guest_name' );
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name'), 'htmlAutocomplete' => "username" ), function( $val ){
|
||||
if( !empty( $val ) and filter_var( $val, FILTER_VALIDATE_EMAIL ) !== false )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_no_email_allowed' );
|
||||
}
|
||||
}, NULL, NULL, 'guest_name' );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
@@ -879,7 +870,7 @@ abstract class Item extends Content
|
||||
'key' => IPS::mb_ucfirst( static::$module ),
|
||||
'autoSaveKey' => ( $item === NULL ? ( 'newContentItem-' . static::$application . '/' . static::$module . '-' . ( $container ? $container->_id : 0 ) ) : ( 'contentEdit-' . static::$application . '/' . static::$module . '-' . $item->$idColumn ) ),
|
||||
'attachIds' => ( $item === NULL ? NULL : array( $item->$idColumn, $commentObj->$commentIdColumn ) )
|
||||
), '\IPS\Helpers\Form::floodCheck', NULL, NULL, static::$formLangPrefix . 'content_editor' );
|
||||
), ( $item ? null : '\IPS\Helpers\Form::floodCheck' ), NULL, NULL, static::$formLangPrefix . 'content_editor' );
|
||||
|
||||
if ( $item AND IPS::classUsesTrait( $commentClass, 'IPS\Content\EditHistory' ) and Settings::i()->edit_log )
|
||||
{
|
||||
@@ -1049,13 +1040,9 @@ abstract class Item extends Content
|
||||
}
|
||||
|
||||
/* Data Layer Event */
|
||||
if ( Settings::i()->core_datalayer_enabled )
|
||||
if ( DataLayer::enabled() and static::dataLayerEventActive( 'content_create' ) )
|
||||
{
|
||||
/* make sure it isn't a PM Conversation */
|
||||
if ( !isset( static::$title ) or static::$title !== Conversation::$title )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_create', $this->getDataLayerProperties() );
|
||||
}
|
||||
DataLayer::i()->addEvent( 'content_create', $this->getDataLayerProperties( createOrEditValues: $values ) );
|
||||
}
|
||||
|
||||
/* Send this URL to IndexNow if the guest can view it */
|
||||
@@ -1158,9 +1145,11 @@ abstract class Item extends Content
|
||||
/* Initial Comment */
|
||||
if ( isset( static::$commentClass ) and static::$firstCommentRequired )
|
||||
{
|
||||
/* @var Comment $commentClass */
|
||||
$commentClass = static::$commentClass;
|
||||
$commentObj = $this->firstComment();
|
||||
|
||||
/* @var Comment $commentClass */
|
||||
$commentClass = get_class( $commentObj );
|
||||
|
||||
/* @var $databaseColumnMap array */
|
||||
$column = $commentClass::$databaseColumnMap['content'];
|
||||
$idField = $commentClass::$databaseColumnId;
|
||||
@@ -1251,6 +1240,11 @@ abstract class Item extends Content
|
||||
$this->itemEdited();
|
||||
}
|
||||
|
||||
if ( DataLayer::enabled( 'analytics_full' ) and static::dataLayerEventActive( 'content_edit' ) )
|
||||
{
|
||||
DataLayer::i()->addEvent( 'content_edit', $this->getDataLayerProperties() );
|
||||
}
|
||||
|
||||
Event::fire( 'onCreateOrEdit', $this, array( $values ) );
|
||||
|
||||
$this->ui( 'formPostSave', array( $values ) );
|
||||
@@ -1517,7 +1511,7 @@ abstract class Item extends Content
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadMethodCallException;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2064,6 +2058,9 @@ abstract class Item extends Content
|
||||
{
|
||||
Index::i()->massUpdate( $commentClass, NULL, $item->$idColumn, $extension->searchIndexPermissions(), $this->hidden() ? 2 : NULL, $extension->searchIndexContainer(), NULL, $this->$idColumn, $this->author()->member_id );
|
||||
}
|
||||
|
||||
/* Solved Index */
|
||||
Db::i()->update( 'core_solved_index', [ 'item_id' => $this->$idColumn ], [ 'app=? and comment_class=? and item_id=?', static::$application, $commentClass, $item->$idColumn ] );
|
||||
}
|
||||
if ( isset( static::$reviewClass ) )
|
||||
{
|
||||
@@ -2194,6 +2191,12 @@ abstract class Item extends Content
|
||||
$viewColumn = $item::$databaseColumnMap['views'];
|
||||
$this->$viewColumn = $this->mapped('views') + $views;
|
||||
}
|
||||
|
||||
/* Recount helpfuls */
|
||||
if( IPS::classUsesTrait( $this, Helpful::class ) )
|
||||
{
|
||||
$this->recountHelpfuls();
|
||||
}
|
||||
|
||||
$this->rebuildFirstAndLastCommentData();
|
||||
|
||||
@@ -2576,6 +2579,12 @@ abstract class Item extends Content
|
||||
Db::i()->delete( 'core_ratings', ['class=? AND item_id=?', get_called_class(), $this->$idColumn] );
|
||||
}
|
||||
|
||||
/* Pending view updates */
|
||||
if( IPS::classUsesTrait( $this, ViewUpdates::class ) )
|
||||
{
|
||||
Db::i()->delete( 'core_view_updates', [ 'classname=? and id=?', get_called_class(), $this->$idColumn ] );
|
||||
}
|
||||
|
||||
foreach ( $memberIds as $member )
|
||||
{
|
||||
Member::load( $member )->recountNotifications();
|
||||
@@ -2615,6 +2624,7 @@ abstract class Item extends Content
|
||||
|
||||
Db::i()->delete( 'core_reputation_index', array('rep_class=? AND ' . Db::i()->in( 'type_id', $ids ), $class) );
|
||||
Db::i()->delete( 'core_solved_index', array('comment_class=? AND ' . Db::i()->in( 'comment_id', $ids ), $class) );
|
||||
Db::i()->delete( 'core_approval_queue', array( 'approval_content_class=? AND ' . Db::i()->in( 'approval_content_id', $ids ), $class ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2691,7 +2701,7 @@ abstract class Item extends Content
|
||||
try
|
||||
{
|
||||
$permissions = $this->container()->permissions();
|
||||
return $permissions['perm_id'];
|
||||
return is_array( $permissions ) ? $permissions['perm_id'] : null;
|
||||
}
|
||||
catch( BadMethodCallException )
|
||||
{
|
||||
@@ -2948,7 +2958,7 @@ abstract class Item extends Content
|
||||
{
|
||||
$authorCol = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['author'];
|
||||
/* Only fetching a count, for a single container with a item cache count, no future publishing, and there is only one where clause (the container limitation) */
|
||||
if( $countOnly === TRUE AND $skipPermission instanceof Model AND $skipPermission->_items !== NULL AND !in_array( 'IPS\Content\FuturePublishing', class_implements( get_called_class() ) ) AND count( $where ) === 1 )
|
||||
if( $countOnly === TRUE AND $skipPermission instanceof Model AND $skipPermission->_items !== NULL AND !IPS::classUsesTrait( get_called_class(), FuturePublishing::class ) AND count( $where ) === 1 )
|
||||
{
|
||||
$countShortcut = TRUE;
|
||||
$where[] = array( "({$col}=0 AND ( {$authorCol}={$member->member_id}{$extra} ) )" );
|
||||
@@ -3062,47 +3072,13 @@ abstract class Item extends Content
|
||||
{
|
||||
$containerClass = static::$containerNodeClass;
|
||||
$member = $member ?: Member::loggedIn();
|
||||
$categories = array();
|
||||
$lookupKey = md5( $containerClass::$permApp . $containerClass::$permType . $permissionKey . json_encode( $member->groups ) );
|
||||
|
||||
/* EME: Switch to use exclusion instead of inclusion */
|
||||
/* @var $permissionMap array */
|
||||
if( !isset( static::$permissionSelect[ $lookupKey ] ) )
|
||||
{
|
||||
static::$permissionSelect[ $lookupKey ] = array();
|
||||
$permQueryJoinContainer = (bool) ( count( $containerWhere ) );
|
||||
$clubWhere = "";
|
||||
|
||||
/* If we cannot access clubs, skip them */
|
||||
if ( IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) AND !$member->canAccessModule( Module::get( 'core', 'clubs', 'front' ) ) )
|
||||
{
|
||||
$permQueryWhere = array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND (" . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::clubIdColumn() . " IS NOT NULL OR !(" . Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' ))" );
|
||||
$permQueryJoinContainer = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$permQueryWhere = array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND !(" . Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' )" );
|
||||
}
|
||||
|
||||
$permQuery = Db::i()->select( 'perm_type_id', 'core_permission_index', $permQueryWhere );
|
||||
|
||||
if ( $permQueryJoinContainer )
|
||||
{
|
||||
$permQuery->join( $containerClass::$databaseTable, array_merge( $containerWhere, array( 'core_permission_index.perm_type_id=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
|
||||
foreach( $permQuery as $result )
|
||||
{
|
||||
static::$permissionSelect[ $lookupKey ][] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$categories = static::$permissionSelect[ $lookupKey ];
|
||||
|
||||
if( count( $categories ) )
|
||||
{
|
||||
$where[] = array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['container'] . ' NOT IN (' . implode( ',', $categories ) . ')' );
|
||||
}
|
||||
$permQueryWhere = PermissionsExtension::nodePermissionClause( $permissionKey, $containerClass, $member );
|
||||
$permQueryWhere[] = [ 'core_permission_index.app=?', $containerClass::$permApp ];
|
||||
$permQueryWhere[] = [ 'core_permission_index.perm_type=?', $containerClass::$permType ];
|
||||
$where[] = array(
|
||||
'(' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . ' IN( ' . Db::i()->select( 'perm_type_id', 'core_permission_index', $permQueryWhere )->returnFullQuery() . ') )',
|
||||
);
|
||||
}
|
||||
|
||||
$groupBy = ( $joinComments ? static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnId : NULL );
|
||||
@@ -4639,21 +4615,23 @@ abstract class Item extends Content
|
||||
}
|
||||
|
||||
/* Data Layer stuff for comments here, not in Comment class. We track user actions, and the user action is handled here */
|
||||
$dataLayer = Settings::i()->core_datalayer_enabled ? $this->getDataLayerProperties( $comment ) : array();
|
||||
|
||||
Output::i()->json(
|
||||
array(
|
||||
'type' => 'add',
|
||||
'id' => $comment->$commentIdColumn,
|
||||
'page' => $newPageCount,
|
||||
'total' => $this->mapped('num_comments'),
|
||||
'content' => $output,
|
||||
'message' => $message,
|
||||
'postedByLoggedInMember' => true,
|
||||
/* This is used on the front end */
|
||||
"dataLayer" => $dataLayer,
|
||||
)
|
||||
$json = array(
|
||||
'type' => 'add',
|
||||
'id' => $comment->$commentIdColumn,
|
||||
'page' => $newPageCount,
|
||||
'total' => $this->mapped('num_comments'),
|
||||
'content' => $output,
|
||||
'message' => $message,
|
||||
'postedByLoggedInMember' => true,
|
||||
);
|
||||
|
||||
/* This is used on the front end */
|
||||
if ( DataLayer::enabled() and $commentClass::dataLayerEventActive( 'comment_create' ) )
|
||||
{
|
||||
$json['dataLayer'] = $this->getDataLayerProperties( $comment );
|
||||
}
|
||||
|
||||
Output::i()->json( $json );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -4727,21 +4705,25 @@ abstract class Item extends Content
|
||||
* Most, if not all of these are the same for different events, so we can just have one method
|
||||
*
|
||||
* @param Comment|null $comment A comment item, leave null for these keys to be omitted
|
||||
* @param array $createOrEditValues=[] Values from the create or edit form, if applicable.
|
||||
* @param bool $clearCache=false Whether to clear the cached properties and regenerate them from scratch
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDataLayerProperties( ?Comment $comment = null ): array
|
||||
public function getDataLayerProperties( ?Comment $comment = null, array $createOrEditValues=[], bool $clearCache=false ): array
|
||||
{
|
||||
$commentIdColumn = $comment ? $comment::$databaseColumnId : null;
|
||||
$index = $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : 0;
|
||||
$index = "idx_" . ( $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : -1 );
|
||||
|
||||
if ( !isset( $this->_dataLayerProperties[$index] ) )
|
||||
if ( $clearCache OR !isset( $this->_dataLayerProperties[$index] ) )
|
||||
{
|
||||
$app = static::$application ?? null;
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$dataLayer = array(
|
||||
'author_id' => DataLayer::i()->getSsoId( $this->author()->member_id ),
|
||||
'author_name' => $this->author()->real_name ?: null,
|
||||
$anonymous = IPS::classUsesTrait( $comment ?? $this,'IPS\\Content\\Anonymous' ) and ($comment ?? $this)->isAnonymous();
|
||||
$author = $comment ? $comment->author() : $this->author();
|
||||
$dataLayer = $comment ? $this->getDataLayerProperties() : array(
|
||||
'author_id' => $anonymous ? 0 : DataLayer::i()->getSsoId( $author->member_id ),
|
||||
'author_name' => $anonymous ? null : ( $author->real_name ?: null ),
|
||||
'content_age' => $this->mapped( 'date' ) ? intval( floor( ( time() - $this->mapped( 'date' ) ) / 86400 ) ) : null,
|
||||
'content_container_id' => null,
|
||||
'content_container_name' => null,
|
||||
@@ -4751,11 +4733,20 @@ abstract class Item extends Content
|
||||
'content_title' => $this->mapped( 'title' ),
|
||||
'content_type' => static::$contentType ?? null,
|
||||
'content_url' => (string) $this->url(),
|
||||
'content_anonymous' => $anonymous,
|
||||
'content_is_followed' => ( isset( $createOrEditValues[ static::$formLangPrefix . 'auto_follow'] ) AND $createOrEditValues[ static::$formLangPrefix . 'auto_follow'] ) or (bool) (
|
||||
Member::loggedIn()->member_id and
|
||||
$this->$idColumn and
|
||||
IPS::classUsesTrait( $this::class, 'IPS\Content\Followable' ) and
|
||||
Member::loggedIn()->following( $app, mb_strtolower( mb_substr( $this::class, mb_strrpos( $this::class, '\\' ) + 1 ) ), $this->$idColumn )
|
||||
),
|
||||
);
|
||||
|
||||
if ( $comment AND ( $commentIdColumn = $comment::$databaseColumnId ?? null ) )
|
||||
if ( $comment )
|
||||
{
|
||||
$dataLayer = array_replace( $dataLayer, array(
|
||||
'author_id' => $anonymous ? 0 : DataLayer::i()->getSsoId( $comment->author()->member_id ),
|
||||
'author_name' => $anonymous ? null : ( $comment->author()->real_name ?: null ),
|
||||
'comment_id' => $comment->$commentIdColumn,
|
||||
'comment_type' => $comment::$commentType ?? null,
|
||||
'comment_url' => (string) $comment->url()
|
||||
@@ -4763,12 +4754,18 @@ abstract class Item extends Content
|
||||
}
|
||||
|
||||
/* For QA forums, the comment_type and content_type is an exception */
|
||||
if ( Settings::i()->core_datalayer_distinguish_qa AND
|
||||
$this instanceof Topic AND
|
||||
$this->container()->_forum_type === 'qa' )
|
||||
static $isQA = null;
|
||||
if ( $isQA === null )
|
||||
{
|
||||
$isQA = ( Settings::i()->core_datalayer_distinguish_qa AND
|
||||
$this instanceof Topic AND
|
||||
$this->container()->_forum_type === 'qa');
|
||||
}
|
||||
|
||||
if ( $isQA )
|
||||
{
|
||||
$dataLayer['content_type'] = 'question';
|
||||
if ( isset( $comment ) )
|
||||
if ( $comment and $comment instanceof Topic\Post )
|
||||
{
|
||||
$dataLayer['comment_type'] = 'answer';
|
||||
}
|
||||
@@ -4776,7 +4773,7 @@ abstract class Item extends Content
|
||||
/* If we didn't find a Comment or Content type, try pulling that info from the static title fields */
|
||||
elseif ( ( !isset( $dataLayer['content_type'] ) AND isset( static::$title ) ) OR ( !isset( $dataLayer['comment_type'] ) AND isset( $comment, $comment::$title ) ) )
|
||||
{
|
||||
$contentType = ( $app ? preg_replace( "/[{$app}_]?(.*)/", '$1', static::$title ?? "" ) : ( static::$title ?? null ) ) ?: null;
|
||||
$contentType = ( $app ? preg_replace( "/^" . preg_quote( $app . "_" ) . "/", '', static::$title ?? "" ) : ( static::$title ?? null ) ) ?: null;
|
||||
if ( $contentType AND !isset( $dataLayer['content_type'] ) )
|
||||
{
|
||||
$dataLayer['content_type'] = $contentType;
|
||||
@@ -4784,7 +4781,7 @@ abstract class Item extends Content
|
||||
|
||||
if ( $comment AND isset( $comment::$title ) AND !isset( $dataLayer['comment_type'] ) )
|
||||
{
|
||||
$commentType = ($app or $contentType) ? preg_replace( "/" . ( $app ? "[{$app}_]?" : '' ) . ( $contentType ? "[{$contentType}_]?" : '' ) . "(.*)/", "$1", $comment::$title ) : $comment::$title;
|
||||
$commentType = ($app or $contentType) ? preg_replace( "/^" . ( $app ? ( '(?:' . preg_quote( $app . '_' ) . ')?' ) : '' ) . ( $contentType ? ( '(?:' . preg_quote( $contentType . '_' ) . ')?' ) : '' ) . "/", "", $comment::$title ) : $comment::$title;
|
||||
$dataLayer['comment_type'] = $commentType;
|
||||
}
|
||||
}
|
||||
@@ -4795,19 +4792,25 @@ abstract class Item extends Content
|
||||
$dataLayer['comment_type'] = $comment instanceof Review ? 'review' : 'comment';
|
||||
}
|
||||
|
||||
try
|
||||
/* Since the comment properties is based on the item's properties (the recursive call above), we only need this for non-comment properties */
|
||||
if ( !$comment )
|
||||
{
|
||||
$container = $this->container();
|
||||
$dataLayer = array_replace( $dataLayer, $container->getDataLayerProperties() );
|
||||
}
|
||||
catch ( OutOfRangeException | BadMethodCallException $e ) {}
|
||||
|
||||
if ( !isset( $dataLayer['content_area'] ) AND $app )
|
||||
{
|
||||
$lang = Lang::load( Lang::defaultLanguage() );
|
||||
if ( $lang->checkKeyExists( '__app_' . static::$application ) )
|
||||
try
|
||||
{
|
||||
$dataLayer['content_area'] = $lang->addToStack( '__app_' . static::$application );
|
||||
$container = $this->container();
|
||||
$dataLayer = array_replace( $dataLayer, $container->getDataLayerProperties() );
|
||||
}
|
||||
catch ( OutOfRangeException|BadMethodCallException $e )
|
||||
{
|
||||
}
|
||||
|
||||
if ( !isset( $dataLayer['content_area'] ) and $app )
|
||||
{
|
||||
$lang = Lang::load( Lang::defaultLanguage() );
|
||||
if ( $lang->checkKeyExists( '__app_' . static::$application ) )
|
||||
{
|
||||
$dataLayer['content_area'] = $lang->addToStack( '__app_' . static::$application );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4897,7 +4900,12 @@ abstract class Item extends Content
|
||||
{
|
||||
if ( isset( $commentClass::$databaseColumnMap['author_name'] ) )
|
||||
{
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ) );
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ), function( $val ){
|
||||
if( !empty( $val ) and filter_var( $val, FILTER_VALIDATE_EMAIL ) !== false )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_no_email_allowed' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
@@ -4957,7 +4965,7 @@ abstract class Item extends Content
|
||||
$commentClass = static::$commentClass;
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$commentIdColumn = $commentClass::$databaseColumnId;
|
||||
$followArea = mb_strtolower( mb_substr( get_called_class(), mb_strrpos( get_called_class(), '\\' ) + 1 ) );
|
||||
$followArea = mb_strtolower( mb_substr( $this::class, mb_strrpos( $this::class, '\\' ) + 1 ) );
|
||||
|
||||
/* Moderator wants to hide the comment */
|
||||
if( isset( $values['hide'] ) AND $values['hide'] )
|
||||
@@ -4969,41 +4977,24 @@ abstract class Item extends Content
|
||||
$hidden = NULL;
|
||||
}
|
||||
|
||||
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, $values['guest_name'] ?? NULL, NULL, NULL, NULL, NULL, $hidden, ( isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : NULL ) );
|
||||
|
||||
Event::fire( 'onCreateOrEdit', $comment, array( $values, TRUE ) );
|
||||
|
||||
/* Auto-follow - If posted anonymously we should set follow to anonymous as well */
|
||||
if( isset( $values[ static::$formLangPrefix . 'auto_follow' ] ) )
|
||||
{
|
||||
if ( $values[ static::$formLangPrefix . 'auto_follow' ] and !Member::loggedIn()->following( static::$application, $followArea, $this->$idColumn ) )
|
||||
{
|
||||
/* Insert */
|
||||
$save = array(
|
||||
'follow_id' => md5( static::$application . ';' . $followArea . ';' . $this->$idColumn . ';' . Member::loggedIn()->member_id ),
|
||||
'follow_app' => static::$application,
|
||||
'follow_area' => $followArea,
|
||||
'follow_rel_id' => $this->$idColumn,
|
||||
'follow_member_id' => Member::loggedIn()->member_id,
|
||||
'follow_is_anon' => isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : 0,
|
||||
'follow_added' => time() + 1, // Make sure streams show follows after content is created
|
||||
'follow_notify_do' => 1,
|
||||
'follow_notify_meta' => '',
|
||||
'follow_notify_freq' => Member::loggedIn()->auto_follow['method'],
|
||||
'follow_notify_sent' => 0,
|
||||
'follow_visible' => 1
|
||||
);
|
||||
|
||||
Db::i()->insert( 'core_follow', $save );
|
||||
Db::i()->delete( 'core_follow_count_cache', array( 'class=? AND id=?', get_called_class(), (int) $this->$idColumn ) );
|
||||
$this->follow( Member::loggedIn()->auto_follow['method'], isset( $values['post_anonymously'] ) ? !$values['post_anonymously'] : true );
|
||||
}
|
||||
else if ( $values[ static::$formLangPrefix . 'auto_follow' ] === false AND Member::loggedIn()->following( static::$application, $followArea, $this->$idColumn ) )
|
||||
{
|
||||
Db::i()->delete( 'core_follow', array( 'follow_id=?', md5( static::$application . ';' . $followArea . ';' . $this->$idColumn . ';' . Member::loggedIn()->member_id ) ) );
|
||||
Db::i()->delete( 'core_follow_count_cache', array( 'id=? AND class=?', $this->$idColumn, get_called_class() ) );
|
||||
$this->unfollow();
|
||||
}
|
||||
}
|
||||
|
||||
$this->_dataLayerProperties = [];
|
||||
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, $values['guest_name'] ?? NULL, NULL, NULL, NULL, NULL, $hidden, ( isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : NULL ) );
|
||||
|
||||
Event::fire( 'onCreateOrEdit', $comment, array( $values, TRUE ) );
|
||||
|
||||
/* Update the search index (note: we already index the comment in Comment::create()) */
|
||||
if( Content\Search\SearchContent::isSearchable( $this ) )
|
||||
{
|
||||
@@ -5059,7 +5050,12 @@ abstract class Item extends Content
|
||||
{
|
||||
if ( isset( $reviewClass::$databaseColumnMap['author_name'] ) )
|
||||
{
|
||||
$form->add( new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ) ) );
|
||||
$form->add( new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ), function( $val ){
|
||||
if( !empty( $val ) and filter_var( $val, FILTER_VALIDATE_EMAIL ) !== false )
|
||||
{
|
||||
throw new InvalidArgumentException( 'form_no_email_allowed' );
|
||||
}
|
||||
} ) );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
@@ -5229,6 +5225,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canComment( ?Member $member=NULL, bool $considerPostBeforeRegistering = TRUE ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'reply', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( $this->containerWrapper() and !$this->container()->checkAction( 'comment' ) )
|
||||
{
|
||||
return FALSE;
|
||||
@@ -5246,6 +5248,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canReview( ?Member $member=NULL, bool $considerPostBeforeRegistering = TRUE ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'review', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( $this->containerWrapper() and !$this->container()->checkAction( 'review' ) )
|
||||
{
|
||||
return FALSE;
|
||||
@@ -6510,6 +6518,12 @@ abstract class Item extends Content
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( $permission, $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
/* If we can find the node... */
|
||||
try
|
||||
{
|
||||
@@ -6553,6 +6567,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canView( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'view', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Check it isn't hidden */
|
||||
@@ -6586,6 +6606,11 @@ abstract class Item extends Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if this is club content and if clubs are enabled */
|
||||
if ( $this->containerWrapper() and IPS::classUsesTrait( $this->container(), 'IPS\Content\ClubContainer' ) and $this->container()->club() and !Settings::i()->clubs )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
/* Check node */
|
||||
return $this->containerWrapper() ? $this->container()->can( 'read', $member ) : true;
|
||||
}
|
||||
@@ -6598,6 +6623,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canChangeAuthor( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'edit', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
return static::modPermission( 'edit', $member, $this->container() );
|
||||
}
|
||||
|
||||
@@ -6611,6 +6642,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canEdit( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'edit', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'edit', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -6656,6 +6693,12 @@ abstract class Item extends Content
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'edit', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
/* Are we restricted from posting or have an unacknowledged warning? */
|
||||
if ( $member->restrict_post or ( $member->members_bitoptions['unacknowledged_warnings'] and Settings::i()->warn_on and Settings::i()->warnings_acknowledge ) )
|
||||
{
|
||||
@@ -6704,6 +6747,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canMove( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'move', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'move', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -6729,6 +6778,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canMerge( Member|null $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'merge', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'merge', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -6750,6 +6805,12 @@ abstract class Item extends Content
|
||||
*/
|
||||
public function canDelete( Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'delete', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'delete', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -7150,29 +7211,29 @@ abstract class Item extends Content
|
||||
|
||||
if( $this->canEdit( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( $this->url()->setQueryString( 'do', 'edit' ), 'edit', identifier: 'edit' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->setQueryString( 'do', 'edit' ), 'edit', identifier: 'edit', icon: 'fa-solid fa-pen-to-square' ) );
|
||||
}
|
||||
|
||||
if( $member->modPermission('can_manage_deleted_content') AND $this->hidden() == -2 )
|
||||
{
|
||||
if( IPS::classUsesTrait( $this, Hideable::class ) and $this->canRestore() )
|
||||
{
|
||||
$restore = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restore' ) ), languageString: 'restore_as_visible', identifier: 'restore' );
|
||||
$restore = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restore' ) ), languageString: 'restore_as_visible', identifier: 'restore', icon: 'fa-solid fa-eye' );
|
||||
$restore->requiresConfirm( 'restore_as_visible_desc' );
|
||||
$menu->add( $restore );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restoreAsHidden' ) ), 'restore_as_hidden', identifier: 'restore_hidden' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restoreAsHidden' ) ), 'restore_as_hidden', identifier: 'restore_hidden', icon: 'fa-solid fa-eye-slash' ) );
|
||||
}
|
||||
|
||||
if( $this->canDelete() )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'delete', 'immediate' => 1 ) ), 'delete_immediately', identifier: 'delete_immediately' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'delete', 'immediate' => 1 ) ), 'delete_immediately', identifier: 'delete_immediately', icon: 'fa-solid fa-trash-can' ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( !$this::$firstCommentRequired and $this->canReportOrRevoke() === TRUE )
|
||||
{
|
||||
$report = new ContentMenuLink( url: $this->url('report'), languageString: 'report', identifier: 'report' );
|
||||
$report = new ContentMenuLink( url: $this->url('report'), languageString: 'report', identifier: 'report', icon: 'fa-solid fa-flag' );
|
||||
if( $member->member_id OR Captcha::supportsModal() )
|
||||
{
|
||||
$report->opensDialog(title: 'report', remoteSubmit: TRUE );
|
||||
@@ -7182,26 +7243,26 @@ abstract class Item extends Content
|
||||
|
||||
if ( IPS::classUsesTrait( $this, 'IPS\Content\FuturePublishing' ) AND $this->isFutureDate() AND static::canFuturePublish($member, $this->container()))
|
||||
{
|
||||
$publish = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'publish' ) ), languageString: 'publish', dataAttributes: [ 'title=\'{lang="publish_desc"}\'' ], identifier: 'publish' );
|
||||
$publish = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'publish' ) ), languageString: 'publish', dataAttributes: [ 'title=\'{lang="publish_desc"}\'' ], identifier: 'publish', icon: 'fa-solid fa-clock' );
|
||||
$publish->requiresConfirm();
|
||||
$menu->add( $publish );
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, 'IPS\Content\Pinnable' ) AND $this->canPin( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'pin' ) ), 'pin', identifier: 'pin' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'pin' ) ), 'pin', identifier: 'pin', icon: 'fa-solid fa-thumbtack' ) );
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, 'IPS\Content\Pinnable' ) AND $this->canUnpin( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unpin' ) ), 'unpin', identifier: 'unpin' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unpin' ) ), 'unpin', identifier: 'unpin', icon: 'fa-solid fa-thumbtack-slash' ) );
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, 'IPS\Content\Hideable' ) )
|
||||
{
|
||||
if( $this->canHide($member))
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'hide' ) ), languageString: 'hide' , dataAttributes: [
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'hide' ) ), languageString: 'hide', icon: 'fa-solid fa-eye-slash', dataAttributes: [
|
||||
'data-ipsDialog' => 'true',
|
||||
'data-ipsDialog-size' => 'medium',
|
||||
'data-ipsDialog-title' => Member::loggedIn()->language()->addToStack('hide'),
|
||||
@@ -7211,7 +7272,7 @@ abstract class Item extends Content
|
||||
|
||||
if( $this->canUnhide($member) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unhide' ) ), languageString: $this->hidden() === 1 ? 'approve' : 'unhide', dataAttributes: [
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unhide' ) ), languageString: $this->hidden() === 1 ? 'approve' : 'unhide', icon: 'fa-solid fa-eye', dataAttributes: [
|
||||
'data-ipsDialog-destructOnClose' => 'true'
|
||||
], identifier: 'unhide' ) );
|
||||
}
|
||||
@@ -7221,7 +7282,7 @@ abstract class Item extends Content
|
||||
{
|
||||
if( $this->canLock( $member ) )
|
||||
{
|
||||
$lock = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'lock' ) ), languageString: 'lock', identifier: 'lock' );
|
||||
$lock = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'lock' ) ), languageString: 'lock', identifier: 'lock', icon: 'fa-solid fa-lock' );
|
||||
if( $member->modPermission('can_manage_alerts') AND $this->author()->member_id )
|
||||
{
|
||||
$lock->addAttribute( 'data-ipsDialog')
|
||||
@@ -7234,32 +7295,32 @@ abstract class Item extends Content
|
||||
|
||||
if( $this->canUnlock( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unlock' ) ), languageString: 'unlock' ) );
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'unlock' ) ), languageString: 'unlock', icon: 'fa-solid fa-unlock' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->canMove( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'move' ) ), languageString: 'move' , opensDialog: true ) );
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'move' ) ), languageString: 'move' , opensDialog: true, icon: 'fa-solid fa-arrow-right' ) );
|
||||
}
|
||||
|
||||
if( $this->canMerge( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'merge' ) ), languageString: 'merge', opensDialog: true ) );
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'merge' ) ), languageString: 'merge', opensDialog: true, icon: 'fa-solid fa-down-left-and-up-right-to-center' ) );
|
||||
}
|
||||
|
||||
if( isset( static::$archiveClass ) )
|
||||
{
|
||||
if( $this->canUnarchive( $member ) )
|
||||
{
|
||||
$unarchive = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'unarchive' ) ), languageString: 'unarchive');
|
||||
$unarchive = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'unarchive' ) ), languageString: 'unarchive', icon: 'fa-solid fa-box-open');
|
||||
$unarchive->requiresConfirm( $this->unarchiveBlurb() );
|
||||
$menu->add( $unarchive );
|
||||
}
|
||||
|
||||
if( $this->canRemoveArchiveExcludeFlag( $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'removeArchiveExcludeFlag' ) ), languageString: 'remove_archive_exclude_flag') );
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'removeArchiveExcludeFlag' ) ), languageString: 'remove_archive_exclude_flag', icon: 'fa-solid fa-box') );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7274,6 +7335,7 @@ abstract class Item extends Content
|
||||
if ( !$this->isFeatured() )
|
||||
{
|
||||
$feature = new ContentMenuLink( url: $this->url()->setQueryString( array( 'do' => 'feature', 'fromItem' => 1 ) ), languageString: 'promote_social_button' );
|
||||
$feature->icon ="fa-solid fa-star";
|
||||
$feature->addAttribute( 'data-ipsDialog-flashMessage', Member::loggedIn()->language()->addToStack( 'promote_flash_msg' ) )
|
||||
->addAttribute( 'data-ipsDialog-flashMessageTimeout', 5 )
|
||||
->addAttribute( 'data-ipsDialog-flashMessageEscape', 'false' )
|
||||
@@ -7284,6 +7346,7 @@ abstract class Item extends Content
|
||||
else
|
||||
{
|
||||
$feature = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'unfeature', 'fromItem' => 1 ) ), languageString: 'demote_social_button' );
|
||||
$feature->icon ="fa-regular fa-star";
|
||||
$feature->addAttribute( 'data-ipsDialog-flashMessage', Member::loggedIn()->language()->addToStack( 'demote_flash_msg' ) )
|
||||
->addAttribute( 'data-ipsDialog-flashMessageTimeout', 5 )
|
||||
->addAttribute( 'data-ipsDialog-flashMessageEscape', 'false' )
|
||||
@@ -7297,6 +7360,7 @@ abstract class Item extends Content
|
||||
if( $this->canDelete( $member ) )
|
||||
{
|
||||
$delete = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' =>'delete' ) ), languageString: 'delete');
|
||||
$delete->icon ="fa-solid fa-trash";
|
||||
$delete->requiresConfirm();
|
||||
$menu->add( $delete );
|
||||
}
|
||||
@@ -7308,23 +7372,24 @@ abstract class Item extends Content
|
||||
if( IPS::classUsesTrait( $this, Statistics::class ) )
|
||||
{
|
||||
$analytics = new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'analytics' ) ), languageString: 'analytics_and_stats' );
|
||||
$analytics->icon = "fa-solid fa-chart-simple";
|
||||
$analytics->opensDialog( 'analytics_and_stats', 'large' );
|
||||
$menu->add( $analytics );
|
||||
}
|
||||
|
||||
$menu->add( new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'modlog' ) ), languageString: 'moderation_history' , opensDialog: true ) );
|
||||
$menu->add( new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'modlog' ) ), languageString: 'moderation_history' , opensDialog: true, icon: 'fa-solid fa-clock-rotate-left' ) );
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, MetaData::class ) )
|
||||
{
|
||||
if( $this->canOnMessage( 'add', $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->setQueryString( array( 'do' => 'messageForm' ) ), languageString: 'add_message' , opensDialog: true ) );
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->setQueryString( array( 'do' => 'messageForm' ) ), languageString: 'add_message' , opensDialog: true, icon: 'fa-solid fa-message' ) );
|
||||
}
|
||||
|
||||
if( $this->canToggleItemModeration( $member ) )
|
||||
{
|
||||
$toggle = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'toggleItemModeration' ) ), languageString: $this->itemModerationEnabled() ?'disable_topic_moderation' : 'enable_topic_moderation' );
|
||||
$toggle = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'toggleItemModeration' ) ), languageString: $this->itemModerationEnabled() ?'disable_topic_moderation' : 'enable_topic_moderation', icon: 'fa-solid fa-traffic-light' );
|
||||
$toggle->requiresConfirm( $this->itemModerationEnabled() ? 'disable_topic_moderation_confirm' : 'enable_topic_moderation_confirm' );
|
||||
$menu->add( $toggle );
|
||||
}
|
||||
@@ -7335,7 +7400,7 @@ abstract class Item extends Content
|
||||
$menu->addSeparator();
|
||||
foreach ( $actions as $action )
|
||||
{
|
||||
$actionLink = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'savedAction', 'action' => $action->_id ) ), languageString: $action->_title );
|
||||
$actionLink = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'savedAction', 'action' => $action->_id ) ), languageString: $action->_title, icon: 'fa-solid fa-wand-magic' );
|
||||
$actionLink->requiresConfirm();
|
||||
$menu->add( $actionLink );
|
||||
}
|
||||
@@ -7474,17 +7539,28 @@ abstract class Item extends Content
|
||||
* @apiresponse datetime date Date
|
||||
* @apiresponse string content Content
|
||||
* @apiresponse string url URL
|
||||
* @apiresponse \IPS\core\Assignments\Assignment|null assignment Assignment data
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
{
|
||||
return array(
|
||||
$data = [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title ? $this->title : NULL,
|
||||
'container' => $this->containerWrapper()?->apiOutput( $authorizedMember ),
|
||||
'title' => $this->mapped( 'title' ),
|
||||
'container' => $this->containerWrapper( true )?->apiOutput( $authorizedMember ),
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => ( isset( static::$databaseColumnMap['date'] ) ? DateTime::ts( $this->mapped( 'date' ) )->rfc3339() : null ),
|
||||
'content' => $this->content(),
|
||||
'url' => (string) $this->url()
|
||||
);
|
||||
'url' => (string) $this->url(),
|
||||
];
|
||||
if ( IPS::classUsesTrait( $this, Assignable::class ) )
|
||||
{
|
||||
$data['assignment'] = $this->assignment ? $this->assignment->apiOutput($authorizedMember, false ) : NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['assignment'] = NULL;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -103,10 +103,29 @@ trait ItemTopic
|
||||
try
|
||||
{
|
||||
$topic = Topic::load( $this->$column );
|
||||
|
||||
if ( $topic->isArchived() )
|
||||
{
|
||||
/* Do not update archived topics */
|
||||
return;
|
||||
}
|
||||
|
||||
/* If this is a future publish date, make sure we update that as well */
|
||||
if( IPS::classUsesTrait( $this, FuturePublishing::class ) and $topic->isFutureDate() )
|
||||
{
|
||||
$topic->start_date = $this->mapped( 'date' );
|
||||
$topic->is_future_entry = $this->isFutureDate();
|
||||
$topic->publish_date = $this->mapped( 'future_date' );
|
||||
}
|
||||
|
||||
$title = $this->getTopicTitle();
|
||||
Member::loggedIn()->language()->parseOutputForDisplay( $title );
|
||||
$topic->title = $title;
|
||||
$topic->save();
|
||||
|
||||
/* Synch the last comment data in case timestamps have changed */
|
||||
$topic->resyncLastComment();
|
||||
|
||||
$firstPost = $topic->comments( 1 );
|
||||
|
||||
/* If the first post of the topic is missing, NULL will be returned */
|
||||
@@ -118,6 +137,7 @@ trait ItemTopic
|
||||
$content = $this->getTopicContent();
|
||||
Member::loggedIn()->language()->parseOutputForDisplay( $content );
|
||||
$firstPost->post = $content;
|
||||
$firstPost->post_date = $topic->start_date;
|
||||
$firstPost->save();
|
||||
Index::i()->index( $firstPost );
|
||||
}
|
||||
@@ -181,6 +201,12 @@ trait ItemTopic
|
||||
$topic->sendNotifications();
|
||||
}
|
||||
|
||||
/* Check our auto-follow settings */
|
||||
if( $topic->author()->auto_follow['content'] )
|
||||
{
|
||||
$topic->follow( $topic->author()->auto_follow['method'], !$this->isAnonymous(), $topic->author() );
|
||||
}
|
||||
|
||||
/* Update file */
|
||||
$this->$column = $topic->tid;
|
||||
$this->save();
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace IPS\Content;
|
||||
use BadMethodCallException;
|
||||
use IPS\Member;
|
||||
|
||||
use IPS\Request;
|
||||
use function defined;
|
||||
use function get_class;
|
||||
use function header;
|
||||
@@ -61,6 +62,18 @@ trait Lockable
|
||||
*/
|
||||
public function canLock( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Is this the task? Let it through */
|
||||
if( Request::i()->isCliEnvironment() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'lock', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'lock', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -94,6 +107,12 @@ trait Lockable
|
||||
*/
|
||||
public function canUnlock( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'unlock', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'unlock', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -216,6 +216,12 @@ trait MetaData
|
||||
*/
|
||||
public function canOnMessage( string $action, ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'onMessage', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'onMessage', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -300,6 +306,12 @@ trait MetaData
|
||||
*/
|
||||
public function canFeatureComment( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'featureComment', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'featureComment', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -317,6 +329,12 @@ trait MetaData
|
||||
*/
|
||||
public function canUnfeatureComment( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'unfeatureComment', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'unfeatureComment', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -398,6 +416,12 @@ trait MetaData
|
||||
*/
|
||||
public function canToggleItemModeration( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'toggleItemModeration', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'toggleItemModeration', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Permissions
|
||||
* @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
|
||||
* @subpackage
|
||||
* @since 4/16/2025
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Application;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Db;
|
||||
use IPS\Dispatcher;
|
||||
use IPS\Extensions\PermissionsAbstract;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Settings;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class Permissions
|
||||
{
|
||||
/**
|
||||
* @brief Used for Permissions extensions; override default permissions
|
||||
*/
|
||||
const PERM_ALLOW = 1;
|
||||
|
||||
/**
|
||||
* @brief Used for Permissions extensions; override default permissions
|
||||
*/
|
||||
const PERM_DENY = 2;
|
||||
|
||||
/**
|
||||
* @brief Used for Permissions extensions; revert to default permission checks
|
||||
*/
|
||||
const PERM_DEFAULT = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static array $nodeIncludesMap = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static array $nodeExcludeMap = [];
|
||||
|
||||
/**
|
||||
* Load all the inclusion/exclusion mappings from extensions
|
||||
*
|
||||
* @param string $permission
|
||||
* @param string $nodeClass
|
||||
* @param Member $member
|
||||
* @return void
|
||||
*/
|
||||
protected static function loadMappings( string $permission, string $nodeClass, Member $member ) : void
|
||||
{
|
||||
if( !isset( static::$nodeIncludesMap[ $nodeClass ][ $permission ] ) )
|
||||
{
|
||||
$include = [];
|
||||
$exclude = [];
|
||||
foreach( Application::allExtensions( 'core', 'Permissions' ) as $ext )
|
||||
{
|
||||
/* @var PermissionsAbstract $ext */
|
||||
$include = array_merge( $include, $ext->nodeIdsToAllow( $permission, $nodeClass, $member ) );
|
||||
$exclude = array_merge( $exclude, $ext->nodeIdsToDeny( $permission, $nodeClass, $member ) );
|
||||
}
|
||||
|
||||
static::$nodeIncludesMap[ $nodeClass ][ $permission ] = $include;
|
||||
static::$nodeExcludeMap[ $nodeClass ][ $permission ] = $exclude;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Easily load included IDs, for my sanity
|
||||
*
|
||||
* @param string $permission
|
||||
* @param string $nodeClass
|
||||
* @param Member|null $member
|
||||
* @param int $permType
|
||||
* @return array|null
|
||||
*/
|
||||
protected static function getMapping( string $permission, string $nodeClass, ?Member $member, int $permType ) : ?array
|
||||
{
|
||||
/* Only allow these extensions to run on the front-end or from the API */
|
||||
if( !Dispatcher::hasInstance() or ( Dispatcher::i()->controllerLocation != 'front' and Dispatcher::i()->controllerLocation != 'api' ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
static::loadMappings( $permission, $nodeClass, $member );
|
||||
|
||||
switch( $permType )
|
||||
{
|
||||
case static::PERM_ALLOW:
|
||||
return count( static::$nodeIncludesMap[ $nodeClass ][ $permission ] ) ? static::$nodeIncludesMap[ $nodeClass ][ $permission ] : null;
|
||||
|
||||
case static::PERM_DENY:
|
||||
return count( static::$nodeExcludeMap[ $nodeClass ][ $permission ] ) ? static::$nodeExcludeMap[ $nodeClass ][ $permission ] : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper method to call extension
|
||||
*
|
||||
* @param string $permission
|
||||
* @param object $object
|
||||
* @param Member|null $member
|
||||
* @return int
|
||||
*/
|
||||
public static function can( string $permission, object $object, ?Member $member=null ) : int
|
||||
{
|
||||
/* Only allow these extensions to run on the front-end or from the API */
|
||||
if( !Dispatcher::hasInstance() or ( Dispatcher::i()->controllerLocation != 'front' and Dispatcher::i()->controllerLocation != 'api' ) )
|
||||
{
|
||||
return static::PERM_DEFAULT;
|
||||
}
|
||||
|
||||
foreach( Application::allExtensions( 'core', 'Permissions' ) as $ext )
|
||||
{
|
||||
/* @var PermissionsAbstract $ext */
|
||||
$result = $ext->checkPermission( $permission, $object, $member );
|
||||
if( $result !== static::PERM_DEFAULT )
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return static::PERM_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a query clause to pull node IDs from the permissions table.
|
||||
*
|
||||
* @param string $permission
|
||||
* @param string $nodeClass
|
||||
* @param Member|null $member
|
||||
* @param bool $includeClubs
|
||||
* @return array
|
||||
*/
|
||||
public static function nodePermissionClause( string $permission, string $nodeClass, ?Member $member=null, bool $includeClubs=true ) : array
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
|
||||
/* If clubs are disabled, or we don't have permission, force it to exclude them */
|
||||
if( !Settings::i()->clubs or !$member->canAccessModule( Module::get( 'core', 'clubs', 'front' ) ) or !IPS::classUsesTrait( $nodeClass, ClubContainer::class ) )
|
||||
{
|
||||
$includeClubs = false;
|
||||
}
|
||||
|
||||
/* @var Model $nodeClass */
|
||||
/* @var array $permissionMap */
|
||||
|
||||
$clause = [
|
||||
Db::i()->findInSet( 'core_permission_index.perm_' . $nodeClass::$permissionMap[ $permission ], ( $includeClubs ? $member->permissionArray() : $member->groups ) ),
|
||||
'core_permission_index.perm_' . $nodeClass::$permissionMap[ $permission ] . '=?'
|
||||
];
|
||||
$binds = [
|
||||
'*'
|
||||
];
|
||||
|
||||
/* If we have any specific inclusions, put them in */
|
||||
if( $includes = static::getMapping( $permission, $nodeClass, $member, static::PERM_ALLOW ) )
|
||||
{
|
||||
$clause[] = 'core_permission_index.perm_type_id in (' . implode( ",", $includes ) . ')';
|
||||
}
|
||||
|
||||
$return = [
|
||||
array_merge( array( '(' . implode( ' OR ', $clause ) . ')' ), $binds )
|
||||
];
|
||||
|
||||
/* If we have any specific exclusions, we need it separate so that we make it an AND */
|
||||
if( $excludes = static::getMapping( $permission, $nodeClass, $member, static::PERM_DENY ) )
|
||||
{
|
||||
$return[] = [ Db::i()->in( 'core_permission_index.perm_type_id', $excludes, true ) ];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,12 @@ trait Pinnable
|
||||
*/
|
||||
public function canPin( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'pin', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'pin', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -67,6 +73,12 @@ trait Pinnable
|
||||
*/
|
||||
public function canUnpin( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'unpin', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'unpin', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
namespace IPS\Content;
|
||||
|
||||
use BadMethodCallException;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Poll;
|
||||
use IPS\Node\Model;
|
||||
@@ -60,7 +61,8 @@ trait Polls
|
||||
if( $this->mapped('poll') )
|
||||
{
|
||||
/* If the poll is in a club, return the special extended class */
|
||||
if( $this->containerWrapper() AND $club = $this->containerWrapper()->club() )
|
||||
$container = $this->containerWrapper( true );
|
||||
if( $container and IPS::classUsesTrait( $container, ClubContainer::class ) and $club = $container->club() )
|
||||
{
|
||||
$poll = ClubPoll::load( $this->mapped('poll') );
|
||||
$poll->club = $club;
|
||||
|
||||
@@ -48,6 +48,12 @@ trait Ratings
|
||||
*/
|
||||
public function canRate( Member|null $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'rate', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
$member = $member ?: Member::loggedIn();
|
||||
if( !$member->member_id )
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use BadMethodCallException;
|
||||
use DateInterval;
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use IPS\Api\Webhook;
|
||||
use IPS\Application;
|
||||
use IPS\Content;
|
||||
use IPS\DateTime;
|
||||
@@ -164,6 +165,8 @@ trait Reactable
|
||||
/* Fire event */
|
||||
Event::fire( 'onReact', $member, [ $this, $reaction ] );
|
||||
|
||||
Webhook::fire( 'content_reaction_added', [ 'member' => $member, 'content' => $this, 'reaction' => $reaction ] );
|
||||
|
||||
/* Have we hit highlighted content? */
|
||||
if( $this->isHighlighted() )
|
||||
{
|
||||
@@ -262,6 +265,7 @@ trait Reactable
|
||||
|
||||
/* Fire event */
|
||||
Event::fire( 'onUnreact', $member, [ $this ] );
|
||||
Webhook::fire( 'content_reaction_removed', [ 'member' => $member, 'content' => $this, 'reaction' => $reaction ] );
|
||||
|
||||
/* Remove Notifications */
|
||||
if( $removeNotifications === TRUE )
|
||||
@@ -296,6 +300,12 @@ trait Reactable
|
||||
*/
|
||||
public function canReact( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'react', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'react', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace IPS\Content;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use Exception;
|
||||
use IPS\IPS;
|
||||
use IPS\Node\CustomBadge;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Data\Store;
|
||||
@@ -257,8 +258,16 @@ class Reaction extends Model
|
||||
{
|
||||
return $badge->file()?->url;
|
||||
}
|
||||
if( $this->_data['icon'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
return File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
}
|
||||
catch( Exception $e ){}
|
||||
|
||||
return File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,4 +498,25 @@ class Reaction extends Model
|
||||
|
||||
parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse string icon URL to the icon,
|
||||
* @apiresponse string value Value of the reaction
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
{
|
||||
$return = array(
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'icon' => (string) $this->icon,
|
||||
'value' => $this->value
|
||||
);
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ trait Recognizable
|
||||
*/
|
||||
public function canRemoveRecognize( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'unrecognize', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'unrecognize', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -82,6 +88,12 @@ trait Recognizable
|
||||
*/
|
||||
public function canRecognize( ?Member $member=NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'recognize', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'recognize', $member ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -320,6 +320,12 @@ abstract class Review extends Comment
|
||||
*/
|
||||
public function canView( Member|null $member= null ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'view', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( $member === NULL )
|
||||
{
|
||||
$member = Member::loggedIn();
|
||||
|
||||
@@ -103,6 +103,7 @@ class Index extends SearchIndex
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'properties' => array(
|
||||
'index_id' => array( 'type' => 'keyword' ),
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
@@ -205,6 +206,8 @@ class Index extends SearchIndex
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) )
|
||||
{
|
||||
$indexData['index_id'] = $this->getIndexId( $object );
|
||||
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
|
||||
@@ -44,6 +44,8 @@ class MassIndexer extends Index
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) and $indexData['index_permissions'] )
|
||||
{
|
||||
$indexData['index_id'] = $this->getIndexId( $object );
|
||||
|
||||
if ( $object instanceof Item )
|
||||
{
|
||||
$parent = $object;
|
||||
|
||||
@@ -25,8 +25,10 @@ use IPS\IPS;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Node\Grouping;
|
||||
use IPS\Settings;
|
||||
use StdClass;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
@@ -79,6 +81,11 @@ class Query extends SearchQuery
|
||||
*/
|
||||
protected array|null $itemClasses = NULL;
|
||||
|
||||
/**
|
||||
* @brief If true, just fetch the title record, or the first comment if a post, etc
|
||||
*/
|
||||
protected bool $getTitleRecordOnly = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -133,6 +140,19 @@ class Query extends SearchQuery
|
||||
{
|
||||
if ( $filter->containerIdFilter )
|
||||
{
|
||||
if ( $filter->containerIds )
|
||||
{
|
||||
if ( $filter->itemClass )
|
||||
{
|
||||
$itemClass = $filter->itemClass;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$filter->containerIds = $containerClass::normalizeIds( $filter->containerIds );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conditions[] = array(
|
||||
'terms' => array(
|
||||
'index_container_id' => $filter->containerIds
|
||||
@@ -217,9 +237,7 @@ class Query extends SearchQuery
|
||||
/* Only first comment? */
|
||||
if ( $filter->onlyFirstComment )
|
||||
{
|
||||
$conditions[] = array(
|
||||
'exists' => array( 'field' => 'index_title' )
|
||||
);
|
||||
$this->getTitleRecordOnly = true;
|
||||
}
|
||||
|
||||
/* Only last comment? */
|
||||
@@ -290,7 +308,7 @@ class Query extends SearchQuery
|
||||
/**
|
||||
* Filter by club
|
||||
*
|
||||
* @param Club|int|array|null $club The club, or array of club IDs
|
||||
* @param Club|int|array|null $club The club, or array of club IDs or NULL to exlude content from clubs
|
||||
* @return Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByClub( Club|int|array|null $club ): static
|
||||
@@ -1103,6 +1121,9 @@ class Query extends SearchQuery
|
||||
if ( $method & static::TERM_TITLES_ONLY )
|
||||
{
|
||||
$searchConditions[] = array( 'match_phrase' => array( 'index_title' => array( 'query' => $term ) ) );
|
||||
|
||||
/* Just get the title record only, or the first post if it's a topic as we don't want to show each comment as each comment will have the topic title in the index */
|
||||
$this->getTitleRecordOnly = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1115,6 +1136,9 @@ class Query extends SearchQuery
|
||||
if ( $method & static::TERM_TITLES_ONLY )
|
||||
{
|
||||
$searchConditions[] = array( 'wildcard' => array( 'index_title' => array( 'value' => $term ) ) );
|
||||
|
||||
/* Just get the title record only, or the first post if it's a topic as we don't want to show each comment as each comment will have the topic title in the index */
|
||||
$this->getTitleRecordOnly = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1138,6 +1162,9 @@ class Query extends SearchQuery
|
||||
if ( $method & static::TERM_TITLES_ONLY )
|
||||
{
|
||||
$searchConditions[] = array( 'match' => array( 'index_title' => array( 'query' => $term, 'operator' => $operator ) ) );
|
||||
|
||||
/* Just get the title record only, or the first post if it's a topic as we don't want to show each comment as each comment will have the topic title in the index */
|
||||
$this->getTitleRecordOnly = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1164,7 +1191,7 @@ class Query extends SearchQuery
|
||||
/* If we're not searching with a term, then just show the title record, not comments */
|
||||
if ( $term === NULL and ! $this->lastCommentMustBeTrue )
|
||||
{
|
||||
$must[] = ['exists' => ['field' => 'index_title']];
|
||||
$this->getTitleRecordOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,6 +1248,19 @@ class Query extends SearchQuery
|
||||
/* Peform the search */
|
||||
try
|
||||
{
|
||||
/* We only want the main record (of first comment) not the comments themselves, so we check to make sure _id (aka index_id) is the same as index_item_index_id */
|
||||
if ( $this->getTitleRecordOnly )
|
||||
{
|
||||
$filters[] = [
|
||||
'script' => [
|
||||
'script' => [
|
||||
'lang' => 'painless',
|
||||
'source' => "doc['index_item_index_id'] == doc['index_id']"
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/* Initial query */
|
||||
$query = array(
|
||||
'bool' => array(
|
||||
|
||||
@@ -22,6 +22,7 @@ use IPS\Db\Exception;
|
||||
use IPS\Db\Select;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Node\Grouping;
|
||||
use IPS\Settings;
|
||||
use IPS\IPS;
|
||||
use Throwable;
|
||||
@@ -33,6 +34,7 @@ use function in_array;
|
||||
use function intval;
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
use function is_string;
|
||||
use const IPS\CIC;
|
||||
use const IPS\USE_MYSQL_SEARCH_OPTIMIZED_MODE;
|
||||
|
||||
@@ -145,10 +147,23 @@ class Query extends \IPS\Content\Search\Query
|
||||
$clause[] = 'index_class=?';
|
||||
$params[] = array_pop( $filter->classes );
|
||||
}
|
||||
|
||||
|
||||
/* Set the containers */
|
||||
if ( $filter->containerIdFilter !== NULL )
|
||||
{
|
||||
if ( $filter->containerIds )
|
||||
{
|
||||
if ( $filter->itemClass )
|
||||
{
|
||||
$itemClass = $filter->itemClass;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$filter->containerIds = $containerClass::normalizeIds( $filter->containerIds );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$clause[] = Db::i()->in( 'index_container_id', $filter->containerIds, $filter->containerIdFilter === FALSE );
|
||||
}
|
||||
|
||||
|
||||
@@ -66,12 +66,19 @@ abstract class Query
|
||||
{
|
||||
if ( Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
return new Elastic\Query( $member ?: Member::loggedIn(), Url::external( rtrim( Settings::i()->search_elastic_server, '/' ) . '/' . Settings::i()->search_elastic_index ) );
|
||||
$search = new Elastic\Query( $member ?: Member::loggedIn(), Url::external( rtrim( Settings::i()->search_elastic_server, '/' ) . '/' . Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Mysql\Query( $member ?: Member::loggedIn() );
|
||||
$search = new Mysql\Query( $member ?: Member::loggedIn() );
|
||||
}
|
||||
|
||||
/* Don't include any club content if we have disabled clubs */
|
||||
if( !Settings::i()->clubs )
|
||||
{
|
||||
$search->filterByClub( NULL );
|
||||
}
|
||||
return $search;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,9 +106,10 @@ abstract class Query
|
||||
{
|
||||
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
|
||||
}
|
||||
|
||||
$filters = [];
|
||||
|
||||
/* Exclude disabled applications */
|
||||
$filters = array();
|
||||
foreach( SearchContent::searchableClasses() as $class )
|
||||
{
|
||||
/* We only want items here */
|
||||
|
||||
@@ -161,11 +161,14 @@ class Content extends Result
|
||||
$data = array_pop( $images );
|
||||
$key = key( $data );
|
||||
|
||||
try
|
||||
if( !empty( $data[ $key ] ) )
|
||||
{
|
||||
$enclosure = File::get( $key, $data[ $key ] );
|
||||
try
|
||||
{
|
||||
$enclosure = File::get( $key, $data[ $key ] );
|
||||
}
|
||||
catch( Exception ) { }
|
||||
}
|
||||
catch( Exception ) { }
|
||||
}
|
||||
|
||||
$document->addItem( $object instanceof Comment ? $object->item()->mapped( 'title' ) : $object->mapped( 'title' ), $object->url(), Result::preDisplay( $this->indexData['index_content'] ), DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
|
||||
|
||||
@@ -169,7 +169,22 @@ abstract class SearchContentAbstract
|
||||
}
|
||||
|
||||
/* Articles language */
|
||||
$articles = static::articlesFromIndexData( $objectClass, $containerData );
|
||||
if( is_subclass_of( $objectClass, Comment::class ) and
|
||||
(
|
||||
( $itemClass::$firstCommentRequired and $indexData['index_item_index_id'] == $indexData['index_id'] ) or
|
||||
( isset( $itemData['author'] ) )
|
||||
)
|
||||
)
|
||||
{
|
||||
/* If the first comment is required, and this is the first comment, treat it as an item, not a comment.
|
||||
OR.... if we are commenting on someone else's content. */
|
||||
$articles = static::articlesFromIndexData( $itemClass, $containerData );
|
||||
}
|
||||
else
|
||||
{
|
||||
$articles = static::articlesFromIndexData( $objectClass, $containerData );
|
||||
}
|
||||
|
||||
$summaryLanguage = static::searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData );
|
||||
|
||||
/* Container details */
|
||||
@@ -292,7 +307,7 @@ abstract class SearchContentAbstract
|
||||
{
|
||||
if( $itemClass::$firstCommentRequired )
|
||||
{
|
||||
if( $indexData['index_title'] )
|
||||
if( $indexData['index_item_index_id'] == $indexData['index_id'] )
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ trait Shareable
|
||||
*/
|
||||
public function canShare(): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'share', $this ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'share' ) )
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -226,6 +226,12 @@ trait Solvable
|
||||
*/
|
||||
public function canSolve( ?Member $member = NULL ): bool
|
||||
{
|
||||
/* Extensions go first */
|
||||
if( $permCheck = Permissions::can( 'solve', $this, $member ) )
|
||||
{
|
||||
return ( $permCheck === Permissions::PERM_ALLOW );
|
||||
}
|
||||
|
||||
if( !$this->actionEnabled( 'solve', $member ) )
|
||||
{
|
||||
return false;
|
||||
@@ -238,6 +244,12 @@ trait Solvable
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Guests cannot do this */
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If we have no replies, it's not solvable yet */
|
||||
if( $this->commentCount() <= 0 OR ( static::$firstCommentRequired AND $this->commentCount() == 1 ) )
|
||||
{
|
||||
|
||||
+147
-10
@@ -18,6 +18,7 @@ use BadMethodCallException;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application;
|
||||
use IPS\cms\Records;
|
||||
use IPS\Content;
|
||||
use IPS\Data\Store;
|
||||
use IPS\DateTime;
|
||||
@@ -27,6 +28,7 @@ use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\Translatable;
|
||||
use IPS\Helpers\Form\Upload;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Http\Url\Friendly;
|
||||
use IPS\IPS;
|
||||
@@ -127,6 +129,11 @@ class Tag extends Model
|
||||
*/
|
||||
public static string $application = 'core';
|
||||
|
||||
/**
|
||||
* @brief [Node] Sortable?
|
||||
*/
|
||||
public static bool $nodeSortable = false;
|
||||
|
||||
/**
|
||||
* [Node] Return the custom badge for each row
|
||||
*
|
||||
@@ -222,6 +229,21 @@ class Tag extends Model
|
||||
$this->_data['totals'] = ( is_array( $val ) and count( $val ) ) ? json_encode( $val ) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get content table meta description
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function metaDescription(): ?string
|
||||
{
|
||||
if( !Member::loggedIn()->language()->checkKeyExists( static::$titleLangPrefix . $this->id . static::$descriptionLangSuffix ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::metaDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this node currently queued for deleting or moving content OR is it the target of content queued to be moved from another node?
|
||||
*
|
||||
@@ -237,6 +259,16 @@ class Tag extends Model
|
||||
return parent::deleteOrMoveQueued();
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get the title to store in the log
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function titleForLog(): ?string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the tag has been flagged for merging.
|
||||
* We need to flag it because this can take a while if there
|
||||
@@ -257,7 +289,9 @@ class Tag extends Model
|
||||
*/
|
||||
public function form( Form &$form ): void
|
||||
{
|
||||
$form->add( new Text( 'tag_text', $this->text, true, [], function( $val ){
|
||||
$form->add( new Text( 'tag_text', $this->text, true, [
|
||||
'regex' => '/^[^,]+$/i',
|
||||
], function( $val ){
|
||||
try
|
||||
{
|
||||
$test = Db::i()->select( '*', static::$databaseTable, [ 'tag_text=? and tag_id <> ?', $val, (int) $this->id ] )->first();
|
||||
@@ -299,6 +333,8 @@ class Tag extends Model
|
||||
'multiple' => false,
|
||||
'allowStockPhotos' => true
|
||||
] ) );
|
||||
|
||||
$form->add( new YesNo( 'tag_recommended', $this->recommended, false ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,6 +370,56 @@ class Tag extends Model
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get buttons to display in tree
|
||||
* Example code explains return value
|
||||
*
|
||||
* @code
|
||||
* array(
|
||||
* array(
|
||||
* 'icon' => 'plus-circle', // Name of FontAwesome icon to use
|
||||
* 'title' => 'foo', // Language key to use for button's title parameter
|
||||
* 'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
|
||||
* 'class' => 'modalLink' // CSS Class to use on link (Optional)
|
||||
* ),
|
||||
* ... // Additional buttons
|
||||
* );
|
||||
* @endcode
|
||||
* @param Url $url Base URL
|
||||
* @param bool $subnode Is this a subnode?
|
||||
* @return array
|
||||
*/
|
||||
public function getButtons( Url $url, bool $subnode=FALSE ): array
|
||||
{
|
||||
$buttons = [];
|
||||
|
||||
if( $this->recommended )
|
||||
{
|
||||
$buttons['recommend'] = [
|
||||
'icon' => 'star',
|
||||
'title' => 'tag_mark_norecommend',
|
||||
'link' => $url->setQueryString( [ 'do' => 'recommend', 'id' => $this->id ] )->csrf()
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$buttons['recommend'] = [
|
||||
'icon' => 'regular fa-star',
|
||||
'title' => 'tag_mark_recommend',
|
||||
'link' => $url->setQueryString( [ 'do' => 'recommend', 'id' => $this->id ] )->csrf()
|
||||
];
|
||||
}
|
||||
|
||||
$buttons['view'] = [
|
||||
'icon' => 'search',
|
||||
'title' => 'tag_view_page',
|
||||
'link' => $this->url(),
|
||||
'target' => '_blank'
|
||||
];
|
||||
|
||||
return array_merge( $buttons, parent::getButtons( $url, $subnode ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recount the totals for this content type all affected tags
|
||||
*
|
||||
@@ -446,7 +532,7 @@ class Tag extends Model
|
||||
public static function allTags() : array
|
||||
{
|
||||
return iterator_to_array(
|
||||
Db::i()->select( 'tag_text', static::$databaseTable, null, 'tag_text' )
|
||||
Db::i()->select( 'tag_text', static::$databaseTable, null, 'tag_recommended desc, tag_text' )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -462,7 +548,10 @@ class Tag extends Model
|
||||
{
|
||||
if( IPS::classUsesTrait( $class, Taggable::class ) )
|
||||
{
|
||||
$return[ $class::$title ] = $class;
|
||||
$bits = explode( "\\", $class );
|
||||
$tab = $bits[1] . '__' . array_pop( $bits );
|
||||
$return[ $tab ] = $class;
|
||||
Member::loggedIn()->language()->words[ $tab . '_pl' ] = Member::loggedIn()->language()->get( $class::$title . '_pl' );
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
@@ -501,9 +590,11 @@ class Tag extends Model
|
||||
try
|
||||
{
|
||||
$item = $itemClass::load( $row['pinned_item_id'] );
|
||||
|
||||
$item->pinnedData = $row;
|
||||
$items[] = $item;
|
||||
if( $item->canView() )
|
||||
{
|
||||
$item->pinnedData = $row;
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
@@ -579,14 +670,14 @@ class Tag extends Model
|
||||
if( !isset( Store::i()->tagSlugs ) )
|
||||
{
|
||||
Store::i()->tagSlugs = iterator_to_array(
|
||||
Db::i()->select( 'tag_text,tag_text_seo', static::$databaseTable, [ 'tag_enabled=?', 1 ] )
|
||||
Db::i()->select( 'LOWER(tag_text) as tag_text,tag_text_seo', static::$databaseTable, [ 'tag_enabled=?', 1 ] )
|
||||
->setKeyField( 'tag_text' )
|
||||
->setValueField( 'tag_text_seo' )
|
||||
);
|
||||
}
|
||||
|
||||
$cache = Store::i()->tagSlugs;
|
||||
$slug = $cache[ $tagText ] ?? Friendly::seoTitle( $tagText );
|
||||
$slug = $cache[ mb_strtolower( $tagText ) ] ?? Friendly::seoTitle( $tagText );
|
||||
return Url::internal( "app=core&module=discover&controller=tag&tag=" . $slug, "front", "tags" );
|
||||
}
|
||||
|
||||
@@ -652,11 +743,21 @@ class Tag extends Model
|
||||
if ( !isset( Store::i()->tags ) )
|
||||
{
|
||||
/* Cache only the enabled tags */
|
||||
Store::i()->tags = iterator_to_array(
|
||||
Db::i()->select( 'tag_id,tag_text', static::$databaseTable, [ 'tag_enabled=?', 1 ], 'tag_text' )
|
||||
$recommended = iterator_to_array(
|
||||
Db::i()->select( 'tag_id,tag_text', static::$databaseTable, [ 'tag_enabled=? and tag_recommended=?', 1, 1 ], 'tag_text' )
|
||||
->setKeyField( 'tag_id' )
|
||||
->setValueField( 'tag_text' )
|
||||
);
|
||||
natcasesort( $recommended );
|
||||
|
||||
$other = iterator_to_array(
|
||||
Db::i()->select( 'tag_id,tag_text', static::$databaseTable, [ 'tag_enabled=? and tag_recommended=?', 1, 0 ], 'tag_text' )
|
||||
->setKeyField( 'tag_id' )
|
||||
->setValueField( 'tag_text' )
|
||||
);
|
||||
natcasesort( $other );
|
||||
|
||||
Store::i()->tags = $recommended + $other;
|
||||
}
|
||||
|
||||
return Store::i()->tags;
|
||||
@@ -740,4 +841,40 @@ class Tag extends Model
|
||||
/* Queue the background task to remove the tag from all currently tagged items */
|
||||
Task::queue( 'core', 'UpdateTaggedItems', [ 'tag' => $this->text, 'delete' => true ], 5, [ 'tag' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse string tag Name
|
||||
* @apiresponse string text_seo URL Slug
|
||||
* @apiresponse string headline Headline
|
||||
* @apiresponse string description description
|
||||
* @apiresponse string cover_photo Cover Photo Path
|
||||
* @apiresponse int recommended Recommended
|
||||
*/
|
||||
public function apiOutput( Member $authorizedMember = NULL ): array
|
||||
{
|
||||
$return = [
|
||||
'id' => $this->id,
|
||||
'tag' => $this->_title,
|
||||
'text-seo' => $this->text_seo,
|
||||
'headline' => $this->headline,
|
||||
'url' => (string) $this->url(),
|
||||
'description' => $this->description,
|
||||
];
|
||||
|
||||
if( $file = $this->coverPhotoFile() )
|
||||
{
|
||||
$return[ 'cover_photo' ] = (string) $file;
|
||||
}else
|
||||
{
|
||||
$return[ 'cover_photo' ] = null;
|
||||
}
|
||||
$return['recommended'] = $this->recommended;
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,11 @@
|
||||
namespace IPS\Content;
|
||||
|
||||
use DomainException;
|
||||
use IPS\Content\Search\SearchContent;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Db;
|
||||
use IPS\Content\Search\Index;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\IPS;
|
||||
|
||||
@@ -105,9 +103,7 @@ trait Taggable
|
||||
*/
|
||||
public static function definedTags(): array
|
||||
{
|
||||
$tags = array_values( Tag::getStore() );
|
||||
natcasesort( $tags );
|
||||
return $tags;
|
||||
return array_values( Tag::getStore() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,21 +140,17 @@ trait Taggable
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->tags = array( 'tags' => array(), 'prefix' => NULL );
|
||||
$tagList = Tag::getStore();
|
||||
array_walk( $tagList, function( &$value, $key ) use ( &$tagList ) {
|
||||
$tagList[ $key ] = strtolower( $value );
|
||||
} );
|
||||
|
||||
foreach ( Db::i()->select( '*', 'core_tags', array( 'tag_meta_app=? AND tag_meta_area=? AND tag_meta_id=?', static::$application, static::$module, $this->$idColumn ), 'tag_text' ) as $tag )
|
||||
{
|
||||
$tagText = Settings::i()->tags_force_lower ? mb_strtolower( $tag['tag_text'] ) : $tag['tag_text'];
|
||||
if ( $tag['tag_prefix'] )
|
||||
{
|
||||
$this->tags['prefix'] = $tag['tag_text'];
|
||||
$this->tags['prefix'] = $tagText;
|
||||
}
|
||||
/* Sanity check in case the tag is pending deletion */
|
||||
elseif( in_array( strtolower( $tag['tag_text'] ), $tagList ) )
|
||||
else
|
||||
{
|
||||
$this->tags['tags'][] = $tag['tag_text'];
|
||||
$this->tags['tags'][] = $tagText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,31 +296,15 @@ trait Taggable
|
||||
*/
|
||||
public static function tagsFormField( ?Item $item, Model|string|null $container = null, bool $minimized = FALSE ): Text|NULL
|
||||
{
|
||||
/* Include existing tags in case we're editing */
|
||||
if ( $item )
|
||||
{
|
||||
$source = $item->prefix() ? array_merge( array( 'prefix' => $item->prefix() ), $item->tags() ) : $item->tags();
|
||||
}
|
||||
else
|
||||
{
|
||||
$source = array();
|
||||
}
|
||||
|
||||
/* And get the defined tags */
|
||||
if( static::definedTags() )
|
||||
{
|
||||
$source = array_unique( array_merge( $source, static::definedTags() ) );
|
||||
}
|
||||
|
||||
$options = array(
|
||||
'autocomplete' => array(
|
||||
'unique' => TRUE,
|
||||
'source' => $source,
|
||||
'source' => static::definedTags(),
|
||||
'resultItemTemplate' => 'core.autocomplete.tagsResultItem',
|
||||
'freeChoice' => false,
|
||||
'minimized' => $minimized,
|
||||
'addTokenText' => Member::loggedIn()->language()->get( 'tags_optional' ),
|
||||
'alphabetical' => true
|
||||
'alphabetical' => false
|
||||
) );
|
||||
|
||||
if ( Settings::i()->tags_force_lower )
|
||||
|
||||
Reference in new issue
Block a user