Files
IPB/system/Content/Reactable.php
T
2025-12-19 05:44:59 -08:00

575 lines
15 KiB
PHP

<?php
/**
* @brief Reaction Trait
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 10 Nov 2016
*/
namespace IPS\Content;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Reaction Trait
*/
trait Reactable
{
/**
* Reaction type
*
* @return string
*/
public static function reactionType()
{
throw new \BadMethodCallException;
}
/**
* React
*
* @param \IPS\core\Reaction THe reaction
* @param \IPS\Member The member reacting, or NULL
* @return void
* @throws \DomainException
*/
public function react( \IPS\Content\Reaction $reaction, \IPS\Member $member = NULL )
{
/* Did we pass a member? */
$member = $member ?: \IPS\Member::loggedIn();
/* Figure out the owner of this - if it is content, it will be the author. If it is a node, then it will be the person who created it */
if ( $this instanceof \IPS\Content )
{
$owner = $this->author();
}
else if ( $this instanceof \IPS\Node\Model )
{
$owner = $this->owner();
}
/* Can we react? */
if ( $this->canReact( $member ) === FALSE )
{
throw new \DomainException( 'cannot_react' );
}
/* Have we hit our limit? Also, why 999 for unlimited? */
if ( $member->group['g_rep_max_positive'] !== -1 )
{
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_index', array( 'member_id=? AND rep_date>?', $member->member_id, \IPS\DateTime::create()->sub( new \DateInterval( 'P1D' ) )->getTimestamp() ) )->first();
if ( $count >= $member->group['g_rep_max_positive'] )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'react_daily_exceeded', FALSE, array( 'sprintf' => array( $member->group['g_rep_max_positive'] ) ) ) );
}
}
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
/* If this is a comment, we need the parent items ID */
$itemId = 0;
if ( $this instanceof \IPS\Content\Comment )
{
$item = $this->item();
$itemIdColumn = $item::$databaseColumnId;
$itemId = $item->$itemIdColumn;
}
/* Have we already reacted? */
if ( $this->reacted( $member ) )
{
$this->removeReaction( $member );
}
/* Actually insert it */
$idColumn = static::$databaseColumnId;
\IPS\Db::i()->insert( 'core_reputation_index', array(
'member_id' => $member->member_id,
'app' => $app,
'type' => static::reactionType(),
'type_id' => $this->$idColumn,
'rep_date' => \IPS\DateTime::create()->getTimestamp(),
'rep_rating' => $reaction->value,
'member_received' => $owner->member_id,
'rep_class' => get_class( $this ),
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
'item_id' => $itemId,
'reaction' => $reaction->id
) );
/* Send the notification */
if ( $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $owner ) )
{
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, $member ) );
$notification->recipients->attach( $owner );
$notification->send();
}
if ( $owner->member_id )
{
$owner->pp_reputation_points += $reaction->value;
$owner->save();
}
}
/**
* Remove Reaction
*
* @param \IPS\Member|NULL The member, or NULL
* @return void
*/
public function removeReaction( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
try
{
try
{
$idColumn = static::$databaseColumnId;
$rep = \IPS\Db::i()->select( '*', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first();
}
catch( \UnderflowException $e )
{
throw new \OutOfRangeException;
}
$memberReceived = \IPS\Member::load( $rep['member_received'] );
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
}
catch( \OutOfRangeException $e )
{
throw new \DomainException;
}
if ( $memberReceived->member_id )
{
$memberReceived->pp_reputation_points = $memberReceived->pp_reputation_points - $reaction->value;
$memberReceived->save();
}
\IPS\Db::i()->delete( 'core_reputation_index', array( "id=?", $rep['id'] ) );
}
/**
* Can React
*
* @param \IPS\Member|NULL The member, or NULL for currently logged in
* @return bool
*/
public function canReact( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $this instanceof \IPS\Content )
{
$owner = $this->author();
}
else if ( $this instanceof \IPS\Node\Model )
{
$owner = $this->owner();
}
/* Only members can react */
if ( !$member->member_id )
{
return FALSE;
}
if ( !$owner->member_id )
{
return FALSE;
}
/* Protected Groups */
if ( $owner->inGroup( explode( ',', \IPS\Settings::i()->reputation_protected_groups ) ) )
{
return FALSE;
}
/* Reactions per day */
if ( $member->group['g_rep_max_positive'] == 0 )
{
return FALSE;
}
/* React to own content */
if ( !\IPS\Settings::i()->reputation_can_self_vote AND $this->author()->member_id == $member->member_id )
{
return FALSE;
}
/* Still here? All good. */
return TRUE;
}
/**
* @brief Reactions Cache
*/
protected $_reactions = NULL;
/**
* Reactions
*
* @param array|NULL $mixInData If the data is already know, it can be passed here to be manually set
* @return array
*/
public function reactions()
{
if ( $this->_reactionCount === NULL )
{
$this->_reactionCount = 0;
}
if ( $this->_reactions === NULL )
{
$idColumn = static::$databaseColumnId;
$this->_reactions = array();
if ( is_array( $this->reputation ) )
{
foreach( $this->reputation AS $memberId => $reactionId )
{
try
{
$this->_reactionCount += \IPS\Content\Reaction::load( $reactionId )->value;
$this->_reactions[ $memberId ][] = $reactionId;
}
catch( \OutOfRangeException $e ) { }
}
}
else
{
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
{
$this->_reactions[ $reaction['member_id'] ][] = $reaction['reaction'];
$this->_reactionCount += $reaction['rep_rating'];
}
}
}
return $this->_reactions;
}
/**
* @brief Reaction Count
*/
protected $_reactionCount = NULL;
/**
* Reaction Count
*
* @return int
*/
public function reactionCount()
{
$this->reactions();
return $this->_reactionCount;
}
/**
* Reaction Where Clause
*
* @param \IPS\Content\Reaction|array|int|NULL This can be any one of the following: An \IPS\Content\Reaction object, an array of \IPS\Content\Reaction objects, and integer, or an array of integers, or NULL
* @return array
*/
public function getReactionWhereClause( $reactions = NULL )
{
$idColumn = static::$databaseColumnId;
$where = array( array( 'rep_class=? AND type=? AND type_id=? AND reaction_enabled=?', get_class( $this ), static::reactionType(), $this->$idColumn, 1 ) );
if ( $reactions !== NULL )
{
if ( !is_array( $reactions ) )
{
$reactions = array( $reactions );
}
$in = array();
foreach( $reactions AS $reaction )
{
if ( $reaction instanceof \IPS\Content\Reaction )
{
$in[] = $reaction->id;
}
else
{
$in[] = $reaction;
}
}
if ( count( $in ) )
{
$where[] = array( \IPS\Db::i()->in( 'reaction', $in ) );
}
}
return $where;
}
/**
* Reaction Table
*
* @return \IPS\Helpers\Table\Db
*/
public function reactionTable( $reaction=NULL )
{
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
{
throw new \DomainException;
}
$idColumn = static::$databaseColumnId;
$table = new \IPS\Helpers\Table\Db( 'core_reputation_index', $this->url('showReactions'), $this->getReactionWhereClause( $reaction ) );
$table->sortBy = 'rep_date';
$table->sortDirection = 'desc';
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLogTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'reactionLog' );
$table->joins = array( array( 'from' => 'core_reactions', 'where' => 'reaction=reaction_id' ) );
$table->rowButtons = function( $row )
{
return array(
'delete' => array(
'icon' => 'times-circle',
'title' => 'delete',
'link' => $this->url( 'unreact' )->csrf()->setQueryString( array( 'member' => $row['member_id'] ) ),
'data' => array( 'confirm' => TRUE )
)
);
};
return $table;
}
/**
* Has reacted?
*
* @param \IPS\Member|NULL The member, or NULL for currently logged in
* @return \IPS\Content\Reaction|FALSE
*/
public function reacted( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
$idColumn = static::$databaseColumnId;
try
{
if ( is_array( $this->reputation ) )
{
if ( isset( $this->reputation[ $member->member_id ] ) )
{
return \IPS\Content\Reaction::load( $this->reputation[ $member->member_id ] );
}
else
{
return FALSE;
}
}
else
{
return \IPS\Content\Reaction::load( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first() );
}
}
catch( \UnderflowException $e )
{
return FALSE;
}
}
/**
* @brief Cached React Blurb
*/
public $reactBlurb = NULL;
/**
* React Blurb
*
* @return string
*/
public function reactBlurb()
{
if ( $this->reactBlurb === NULL )
{
$this->reactBlurb = array();
if ( count( $this->reactions() ) )
{
$idColumn = static::$databaseColumnId;
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "app=? AND rep_class=? AND type=? AND type_id=?", $app, get_class( $this ), static::reactionType(), $this->$idColumn ) ) AS $rep )
{
if ( !isset( $this->reactBlurb[ $rep ] ) )
{
$this->reactBlurb[ $rep ] = 0;
}
$this->reactBlurb[ $rep ]++;
}
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
@uksort( $this->reactBlurb, function( $a, $b ) {
try
{
$a = \IPS\Content\Reaction::load( $a );
$b = \IPS\Content\Reaction::load( $b );
}
/* One of the reactions does not exist */
catch( \OutOfRangeException $e )
{
return 1;
}
if ( $a->position < $b->position )
{
return -1;
}
elseif ( $a->position == $b->position )
{
return 0;
}
else
{
return 1;
}
} );
}
else
{
$this->reactBlurb = array();
}
}
return $this->reactBlurb;
}
/**
* @brief Cached like blurb
*/
public $likeBlurb = NULL;
/**
* Who Reacted
*
* @param bool|NULL Use like text instead? NULL to automatically determine
* @return string
*/
public function whoReacted( $isLike = NULL )
{
if ( $isLike === NULL )
{
$isLike = \IPS\Content\Reaction::isLikeMode();
}
if( $this->likeBlurb === NULL )
{
$langPrefix = 'react_';
if ( $isLike )
{
$langPrefix = 'like_';
}
/* Did anyone like it? */
$numberOfLikes = $this->reactionCount(); # int
if ( $numberOfLikes )
{
/* Is it just us? */
$userLiked = ( $this->reacted() );
if ( $userLiked and $numberOfLikes < 2 )
{
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_just_you");
}
/* Nope, we need to display a number... */
else
{
$peopleToDisplayInMainView = array();
$andXOthers = $numberOfLikes;
/* If the user liked, we always show "You" first */
if ( $userLiked )
{
$peopleToDisplayInMainView[] = \IPS\Member::loggedIn()->language()->addToStack("{$langPrefix}blurb_you_and_others");
$andXOthers--;
}
$peopleToDisplayInSecondaryView = array();
/* Some random names */
$idColumn = static::$databaseColumnId;
$i = 0;
$peopleToDisplayInSecondaryView = array();
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
$app = explode( '\\', get_class( $this ) );
if ( \strtolower( $app[1] ) === $app[1] )
{
$app = $app[1];
}
else
{
$app = 'core';
}
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=?', $app, static::reactionType(), $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0 ), 'RAND()', $userLiked ? 17 : 18 ) as $rep )
{
if ( $i < ( $userLiked ? 2 : 3 ) )
{
$peopleToDisplayInMainView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$andXOthers--;
}
else
{
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
}
$i++;
}
/* If there's people to display in the secondary view, add that */
if ( $peopleToDisplayInSecondaryView )
{
if ( count( $peopleToDisplayInSecondaryView ) < $andXOthers )
{
$peopleToDisplayInSecondaryView[] = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others_secondary", FALSE, array( 'pluralize' => array( $andXOthers - count( $peopleToDisplayInSecondaryView ) ) ) );
}
$peopleToDisplayInMainView[] = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reputationOthers( $this->url( 'showReactions' ), \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb_others", FALSE, array( 'pluralize' => array( $andXOthers ) ) ), json_encode( $peopleToDisplayInSecondaryView ) );
}
/* Put it all together */
$this->likeBlurb = \IPS\Member::loggedIn()->language()->addToStack( "{$langPrefix}blurb", FALSE, array( 'pluralize' => array( $numberOfLikes ), 'htmlsprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $peopleToDisplayInMainView ) ) ) );
}
}
/* Nobody liked it - show nothing */
else
{
$this->likeBlurb = '';
}
}
return $this->likeBlurb;
}
}