<?php
/**
* @brief Status Update Model
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 10 Feb 2014
*/
namespace IPS\core\Statuses;
/* 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;
}
/**
* Status Update Model
*/
class _Status extends \IPS\Content\Item implements \IPS\Content\Lockable, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Shareable
{
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/* !\IPS\Patterns\ActiveRecord */
/**
* @brief Database Table
*/
public static $databaseTable = 'core_member_status_updates';
/**
* @brief Database Prefix
*/
public static $databasePrefix = 'status_';
/**
* @brief [Content\Comment] Icon
*/
public static $icon = 'comment-o';
/**
* @brief Number of comments per page
*/
public static $commentsPerPage = 3;
/**
* @brief Multiton Store
*/
protected static $multitons;
/**
* Columns needed to query for search result / stream view
*
* @return array|string
*/
public static function basicDataColumns()
{
return '*';
}
/**
* Save Changed Columns
*
* @return void
*/
public function save()
{
parent::save();
\IPS\Widget::deleteCaches( 'recentStatusUpdates', 'core' );
}
/**
* Delete Record
*
* @return void
*/
public function delete()
{
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_member_status_replies', array( 'reply_status_id=?', $this->id ) ), 'IPS\core\Statuses\Reply' ) AS $reply )
{
$reply->delete();
}
parent::delete();
\IPS\Widget::deleteCaches( 'recentStatusUpdates', 'core' );
}
/* !\IPS\Content\Item */
/**
* @brief Title
*/
public static $title = 'member_status';
/**
* @brief [ActiveRecord] ID Database Column
*/
public static $databaseColumnId = 'id';
/**
* @brief Database Column Map
*/
public static $databaseColumnMap = array(
'date' => 'date',
'author' => 'author_id',
'num_comments' => 'replies',
'locked' => 'is_locked',
'approved' => 'approved',
'ip_address' => 'author_ip',
'content' => 'content',
'title' => 'content',
);
/**
* @brief Application
*/
public static $application = 'core';
/**
* @brief Module
*/
public static $module = 'members';
/**
* @brief Language prefix for forms
*/
public static $formLangPrefix = 'status_';
/**
* @brief Comment Class
*/
public static $commentClass = 'IPS\core\Statuses\Reply';
/**
* @brief [Content\Item] First "comment" is part of the item?
*/
public static $firstCommentRequired = FALSE;
/**
* @brief [Content] Key for hide reasons
*/
public static $hideLogKey = 'status_status';
/**
* Should posting this increment the poster's post count?
*
* @param \IPS\Node\Model|NULL $container Container
* @return void
*/
public static function incrementPostCount( \IPS\Node\Model $container = NULL )
{
return FALSE;
}
/**
* Get Title
*
* @return string
*/
public function get_title()
{
return trim( preg_replace( "/\s+/u", ' ', strip_tags( str_replace( '<', ' <', \IPS\Text\Parser::removeElements( $this->mapped('content'), 'div[class="ipsSpoiler"]' ) ) ) ) );
}
/**
* Get container ID for search index
*
* @return int
*/
public function searchIndexContainer()
{
return $this->member_id;
}
/**
* Search Index Permissions
*
* @return string Comma-delimited values or '*'
* @li Number indicates a group
* @li Number prepended by "m" indicates a member
* @li Number prepended by "s" indicates a social group
*/
public function searchIndexPermissions()
{
return \IPS\Application\Module::get( 'core', 'status', 'front' )->permissions()['perm_view'];
}
/**
* Get mapped value
*
* @param string $key date,content,ip_address,first
* @return mixed
*/
public function mapped( $key )
{
if ( $key === 'title' )
{
/* We do not want the content (which is mapped to title in $databaseColumnMap) to be added into core_search_index.index_title as the field is only varchar(255) */
return mb_substr( $this->title, 0, 85 );
}
return parent::mapped( $key );
}
/**
* Can a given member create a status update?
*
* @param \IPS\Member $member
* @return bool
*/
public static function canCreateFromCreateMenu( \IPS\Member $member = null)
{
if ( !$member )
{
$member = \IPS\Member::loggedIn();
}
/* Can we access the module? */
if ( !parent::canCreate( $member, NULL, FALSE ) )
{
return FALSE;
}
/* We have to be logged in */
if ( !$member->member_id )
{
return FALSE;
}
/* Member has statuses disabled or statuses are completely disabled */
if ( !$member->pp_setting_count_comments or !\IPS\Settings::i()->profile_comments )
{
return FALSE;
}
/* Are we restricted? */
if ( $member->members_bitoptions['bw_no_status_update'] OR $member->group['gbw_no_status_update'] )
{
return FALSE;
}
return TRUE;
}
/**
* Can a given member reply to a status update?
*
* @param \IPS\Member $member The member
* @param \IPS\Node\Model|NULL $container Container
* @param bool $showError If TRUE, rather than returning a boolean value, will display an error
* @return bool
*/
public static function canCreateReply( \IPS\Member $member, \IPS\Node\Model $container=NULL, $showError=FALSE )
{
/* Can we access the module? */
if ( !parent::canCreate( $member, $container, $showError ) )
{
return FALSE;
}
/* We have to be logged in */
if ( !$member->member_id )
{
return FALSE;
}
/* And not restricted */
if ( $member->members_bitoptions['bw_no_status_update'] or $member->group['gbw_no_status_update'] )
{
return FALSE;
}
return TRUE;
}
/**
* Can a given member create this type of content?
*
* @param \IPS\Member $member The member
* @param \IPS\Node\Model|NULL $container Container
* @param bool $showError If TRUE, rather than returning a boolean value, will display an error
* @return bool
*/
public static function canCreate( \IPS\Member $member, \IPS\Node\Model $container=NULL, $showError=FALSE )
{
$profileOwner = isset( \IPS\Request::i()->id ) ? \IPS\Member::load( \IPS\Request::i()->id ) : \IPS\Member::loggedIn();
$error = 'no_module_permission';
$return = TRUE;
/* Can we access the module? */
if ( !parent::canCreate( $member, $container, $showError ) )
{
$return = FALSE;
}
/* We have to be logged in */
if ( !$member->member_id )
{
$return = FALSE;
}
/* And not restricted */
if ( $member->members_bitoptions['bw_no_status_update'] or $member->group['gbw_no_status_update'] )
{
$return = FALSE;
}
/* Is the user being ignored */
if ( $profileOwner->isIgnoring( $member, 'messages' ) )
{
$return = FALSE;
}
if ( !$profileOwner->pp_setting_count_comments )
{
$return = FALSE;
}
if ( !\IPS\Settings::i()->profile_comments )
{
$return = FALSE;
}
/* Return */
if ( $showError and !$return )
{
\IPS\Output::i()->error( $error, '2C322/1', 403 );
}
return $return;
}
/**
* Get elements for add/edit form
*
* @param \IPS\Content\Item|NULL $item The current item if editing or NULL if creating
* @param \IPS\Node\Model|NULL $container Container (e.g. forum), if appropriate
* @param bool $fromCreateMenu false to deactivate the minimize feature
* @return array
*/
public static function formElements( $item=NULL, \IPS\Node\Model $container=NULL , $fromCreateMenu=FALSE)
{
$formElements = parent::formElements( $item, $container );
unset( $formElements['title'] );
if ( $fromCreateMenu )
{
$minimize = NULL;
}
else
{
$member = isset( \IPS\Request::i()->id ) ? \IPS\Member::load( \IPS\Request::i()->id ) : \IPS\Member::loggedIn();
$minimize = ( $member->member_id != \IPS\Member::loggedIn()->member_id ) ?
\IPS\Member::loggedIn()->language()->addToStack( static::$formLangPrefix . '_update_placeholder_other', FALSE, array( 'sprintf' => array( $member->name ) ) ) :
static::$formLangPrefix . '_update_placeholder';
}
$formElements['status_content'] = new \IPS\Helpers\Form\Editor( static::$formLangPrefix . 'content' . ( $fromCreateMenu ? '_ajax' : '' ), ( $item ) ? $item->content : NULL, TRUE, array(
'app' => static::$application,
'key' => 'Members',
'autoSaveKey' => 'status',
'minimize' => $minimize,
), '\IPS\Helpers\Form::floodCheck' );
return $formElements;
}
/**
* Create from form
*
* @param array $values Values from form
* @param \IPS\Node\Model|NULL $container Container (e.g. forum), if appropriate
* @param bool $sendNotification TRUE to automatically send new content notifications (useful for items that may be uploaded in bulk)
* @return \IPS\Content\Item
*/
public static function createFromForm( $values, \IPS\Node\Model $container = NULL, $sendNotification = TRUE )
{
/* Create */
$status = parent::createFromForm( $values, $container, $sendNotification );
\IPS\File::claimAttachments( 'status', $status->id );
/* Return */
return $status;
}
/**
* Process create/edit form
*
* @param array $values Values from form
* @return void
*/
public function processForm( $values )
{
parent::processForm( $values );
/* Work out which profile we are posting too, but only if we are NOT coming from the Create menu or the status updates widget (neither of which allow posting to another profile */
/* @todo The dependency on \IPS\Request here needs to be moved to the controller */
$this->member_id = ( isset( \IPS\Request::i()->id ) AND ( isset( \IPS\Request::i()->controller ) AND \IPS\Request::i()->controller != 'ajaxcreate' ) ) ? \IPS\Request::i()->id : \IPS\Member::loggedIn()->member_id;
if ( !$this->_new )
{
$oldContent = $this->content;
}
$this->content = $values['status_content'];
if ( !$this->_new )
{
$this->sendAfterEditNotifications( $oldContent );
}
}
/**
* @brief Cached URL
*/
protected $_url = NULL;
/**
* Get URL
*
* @param string|NULL $action Action
* @return \IPS\Http\Url
*/
public function url( $action=NULL )
{
if( $this->_url === NULL )
{
$member = \IPS\Member::load( $this->member_id );
$this->_url = \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$member->member_id}&status={$this->id}&type=status", 'front', 'profile', array( $member->members_seo_name ) );
}
$return = $this->_url;
if ( $action )
{
if ( $action == 'edit' )
{
$return = $return->setQueryString( 'do', 'editStatus' );
}
else
{
$return = $return->setQueryString( array( 'do' => $action, 'type' => 'status' ) );
}
if ( $action == 'moderate' AND \IPS\Request::i()->controller == 'feed' )
{
$return = $return->setQueryString( '_fromFeed', 1 );
}
}
return $return;
}
/**
* Get URL from index data
*
* @param array $indexData Data from the search index
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @return \IPS\Http\Url
*/
public static function urlFromIndexData( $indexData, $itemData )
{
return \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$indexData['index_author']}&status={$indexData['index_item_id']}&type=status", 'front', 'profile', array( $itemData['author']['members_seo_name'] ) );
}
/**
* Send notifications
*
* @return void
*/
public function sendNotifications()
{
$sentTo = parent::sendNotifications();
/* Notify when somebody comments on my profile */
if( $this->author()->member_id != $this->member_id )
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'profile_comment', $this, array( $this ) );
$member = \IPS\Member::load( $this->member_id );
$notification->recipients->attach( $member );
$notification->send();
}
/* Notify when a follower posts a status update */
if ( $this->author()->member_id == $this->member_id )
{
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'new_status', $this, array( $this ) );
$followers = \IPS\Member::load( $this->member_id )->followers( 3, array( 'immediate' ), $this->mapped('date'), NULL );
if( count( $followers ) )
{
foreach( $followers AS $follower )
{
$notification->recipients->attach( \IPS\Member::load( $follower['follow_member_id'] ) );
}
}
$notification->send( $sentTo );
}
}
/**
* Should new items be moderated?
*
* @param \IPS\Member $member The member posting
* @param \IPS\Node\Model $container The container
* @return bool
*/
public static function moderateNewItems( \IPS\Member $member, \IPS\Node\Model $container = NULL )
{
if ( $member->moderateNewContent() or \IPS\Settings::i()->profile_comment_approval )
{
return !static::modPermission( 'approve', $member, $container );
}
return parent::moderateNewItems( $member, $container );
}
/**
* Should new comments be moderated?
*
* @param \IPS\Member $member The member posting
* @return bool
*/
public function moderateNewComments( \IPS\Member $member )
{
return ( $member->moderateNewContent() or \IPS\Settings::i()->profile_comment_approval );
}
/**
* Can delete?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @return bool
*/
public function canDelete( $member=NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
/* Profile owner should always be able to delete */
if ( $member->member_id == $this->member_id )
{
return TRUE;
}
return parent::canDelete( $member );
}
/**
* @brief Cached comment count
*/
protected $commentCount = NULL;
/**
* Get comment count
*
* @return int
*/
public function commentCount()
{
if( $this->commentCount === NULL )
{
$count = parent::commentCount();
if( $this->canViewHiddenComments() )
{
$idColumn = static::$databaseColumnId;
$class = static::$commentClass;
$authorCol = $class::$databasePrefix . $class::$databaseColumnMap['author'];
$where = array( array( $class::$databasePrefix . $class::$databaseColumnMap['item'] . '=?', $this->$idColumn ) );
if ( isset( static::$databaseColumnMap['hidden_comments'] ) )
{
$count += $this->mapped('hidden_comments');
}
else
{
if ( isset( $class::$databaseColumnMap['approved'] ) )
{
$col = $class::$databasePrefix . $class::$databaseColumnMap['approved'];
$where[] = array( "{$col}=-1" );
}
elseif( isset( $class::$databaseColumnMap['hidden'] ) )
{
$col = $class::$databasePrefix . $class::$databaseColumnMap['hidden'];
$where[] = array( "{$col}=-1" );
}
$count += \IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first();
}
if ( isset( static::$databaseColumnMap['unapproved_comments'] ) )
{
$count += $this->mapped('unapproved_comments');
}
else
{
if ( isset( $class::$databaseColumnMap['approved'] ) )
{
$col = $class::$databasePrefix . $class::$databaseColumnMap['approved'];
$where[] = array( "{$col}=0" );
}
elseif( isset( $class::$databaseColumnMap['hidden'] ) )
{
$col = $class::$databasePrefix . $class::$databaseColumnMap['hidden'];
$where[] = array( "{$col}=1" );
}
$count += \IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first();
}
}
$this->commentCount = $count;
}
return $this->commentCount;
}
/**
* Get comments to display
*
* @return array
*/
public function commentsForDisplay()
{
/* Init */
$limit = static::getCommentsPerPage();
$numberOfComments = $this->commentCount();
/* If there is more than 3 comments, we want to display the LAST 3 on page 1, the 3 before that on page 2, etc */
if ( $numberOfComments >= $limit )
{
/* Work out what page we're looking at, but only if we are actually paginating through comments */
/* @future we should probably remove the dependancy on \IPS\Request::i()->page here, as the status may be display on a page that isn't necessarily the profile (ex: My Activity) */
$page = ( isset( \IPS\Request::i()->page ) AND isset( \IPS\Request::i()->status ) ) ? intval( \IPS\Request::i()->page ) : 1;
if( $page < 1 )
{
$page = 1;
}
/* Start by making the offset to be the $numberOfComments - ( 3 * $page )
For example, if there's 5 comments, and we're on page 1, the offset will be 2 */
$offset = $numberOfComments - ( $limit * $page );
/* However, if we've got to the start, set teh offset to 0 and adjust the limit to get whatever is left */
if ( $offset < 0 )
{
$limit += $offset;
$offset = 0;
}
/* Is limit still in the negatives? Just reset it. */
if ( $limit < 0 )
{
$limit = static::getCommentsPerPage();
}
}
/* If there's less than 3 comments, just display those */
else
{
$offset = 0;
}
/* Return */
$return = parent::comments( $limit, $offset );
/* If the limit is 1, comments() returns an object, but we want an array */
return ( $limit == 1 ) ? array( $return ) : $return;
}
/**
* Get template for content tables
*
* @return callable
*/
public static function contentTableTemplate()
{
return array( \IPS\Theme::i()->getTemplate( 'statuses', 'core', 'front' ), 'statusContentRows' );
}
/**
* Get HTML for search result display
*
* @param array $indexData Data from the search index
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
* @param array $reputationData Array of people who have given reputation and the reputation they gave
* @param int|NULL $reviewRating If this is a review, the rating
* @param bool $iPostedIn If the user has posted in the item
* @param string $view 'expanded' or 'condensed'
* @param bool $asItem Displaying results as items?
* @param bool $canIgnoreComments Can ignore comments in the result stream? Activity stream can, but search results cannot.
* @param array $template Optional custom template
* @param array $reactions Reaction Data
* @return string
*/
public static function searchResult( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments=FALSE, $template=NULL, $reactions=array() )
{
if ( $template !== NULL )
{
return parent::searchResult( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $iPostedIn, $view, $asItem, $canIgnoreComments, $template, $reactions );
}
$status = static::constructFromData( $itemData );
$status->reputation = $reputationData;
$profileOwner = isset( $itemData['profile'] ) ? $itemData['profile'] : NULL;
$profileOwnerData = $profileOwner ?: $authorData;
$status->_url = \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$profileOwnerData['member_id']}&status={$itemData['status_id']}&type=status", 'front', 'profile', array( $profileOwnerData['members_seo_name'] ) );
return \IPS\Theme::i()->getTemplate( 'statuses', 'core', 'front' )->statusContainer( $status, $authorData, $profileOwner, $view == 'condensed' );
}
/**
* Get number of comments to show per page
*
* @return int
*/
public static function getCommentsPerPage()
{
return static::$commentsPerPage;
}
/**
* Share this content using a share service
*
* @param string $className The share service classname
* @return void
* @throws \InvalidArgumentException
*/
protected function autoShare( $className )
{
$className::publish( html_entity_decode( trim( strip_tags( $this->content ) ), ENT_QUOTES | ENT_DISALLOWED, 'UTF-8' ) );
}
/**
* Reaction Type
*
* @return string
*/
public static function reactionType()
{
return 'status_id';
}
/**
* Can view hidden items?
*
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
* @param \IPS\Node\Model|null $container Container
* @return bool
* @note If called without passing $container, this method falls back to global "can view hidden content" moderator permission which isn't always what you want - pass $container if in doubt or use canViewHiddenItemsContainers()
*/
public static function canViewHiddenItems( $member=NULL, \IPS\Node\Model $container = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if( $member->modPermission( "can_view_hidden_member_status" ) )
{
return TRUE;
}
return parent::canViewHiddenItems( $member, $container );
}
/**
* Check Moderator Permission
*
* @param string $type 'edit', 'hide', 'unhide', 'delete', etc.
* @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member
* @param \IPS\Node\Model|NULL $container The container
* @return bool
*/
public static function modPermission( $type, \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
{
$return = parent::modPermission( $type, $member, $container );
if( $member !== NULL AND $return === FALSE )
{
$title = static::$title;
if ( $member->modPermission( "can_{$type}_{$title}" ) )
{
return TRUE;
}
}
return $return;
}
}