Seditio Source
Root |
./othercms/ips_4.3.4/system/Content/Content.php
<?php
/**
 * @brief        Abstract Content 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        3 Oct 2013
 */

namespace IPS;

/* 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;
}

/**
 * Abstract Content Model
 */
abstract class _Content extends \IPS\Patterns\ActiveRecord
{
   
/**
     * @brief    [Content\Comment]    Database Column Map
     */
   
protected static $databaseColumnMap = array();
   
   
/**
     * @brief    [Content]    Key for hide reasons
     */
   
public static $hideLogKey = NULL;
   
   
/**
     * @brief    [Content\Comment]    Language prefix for forms
     */
   
public static $formLangPrefix = '';

   
/**
     * @brief    Include In Sitemap
     */
   
public static $includeInSitemap = TRUE;
   
   
/**
     * @brief    Reputation Store
     */
   
protected $reputation;
   
   
/**
     * @brief    Can this content be moderated normally from the front-end (will be FALSE for things like Pages and Commerce Products)
     */
   
public static $canBeModeratedFromFrontend = TRUE;
   
   
/**
     * 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
TRUE;
    }
   
   
/**
     * Post count for member
     *
     * @param    \IPS\Member    $member    The memner
     * @return    int
     */
   
public static function memberPostCount( \IPS\Member $member )
    {
        return ( isset( static::
$databaseColumnMap['author'] ) and static::incrementPostCount() ) ? \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ) )->first() : 0;
    }
       
   
/**
     * Load and check permissions
     *
     * @param    mixed                $id        ID
     * @param    \IPS\Member|NULL    $member    Member, or NULL for logged in member
     * @return    static
     * @throws    \OutOfRangeException
     */
   
public static function loadAndCheckPerms( $id, \IPS\Member $member = NULL )
    {
       
$obj = static::load( $id );
       
       
$member = $member ?: \IPS\Member::loggedIn();
        if ( !
$obj->canView( $member ) )
        {
            throw new \
OutOfRangeException;
        }

        return
$obj;
    }
   
   
/**
     * Construct ActiveRecord from database row
     *
     * @param    array    $data                            Row from database table
     * @param    bool    $updateMultitonStoreIfExists    Replace current object in multiton store if it already exists there?
     * @return    static
     */
   
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
    {
        if ( isset(
$data[ static::$databaseTable ] ) and is_array( $data[ static::$databaseTable ] ) )
        {
           
/* Add author data to multiton store to prevent ->author() running another query later */
           
if ( isset( $data['author'] ) and is_array( $data['author'] ) )
            {
                   
$author = \IPS\Member::constructFromData( $data['author'], FALSE );

                if ( isset(
$data['author_pfields'] ) )
                {
                    unset(
$data['author_pfields']['member_id'] );
                   
$author->contentProfileFields();
                }
            }

           
/* Load content */
           
$obj = parent::constructFromData( $data[ static::$databaseTable ], $updateMultitonStoreIfExists );

           
/* Add reputation if it was passed*/
           
if ( isset( $data['core_reputation_index'] ) and is_array( $data['core_reputation_index'] ) )
            {
               
$obj->_data = array_merge( $obj->_data, $data['core_reputation_index'] );
            }

           
/* Return */
           
return $obj;
        }
        else
        {
            return
parent::constructFromData( $data, $updateMultitonStoreIfExists );
        }
    }
   
   
/**
     * Get WHERE clause for Social Group considerations for getItemsWithPermission
     *
     * @param    string        $socialGroupColumn    The column which contains the social group ID
     * @param    \IPS\Member    $member                The member (NULL to use currently logged in member)
     * @return    string
     */
   
public static function socialGroupGetItemsWithPermissionWhere( $socialGroupColumn, $member )
    {            
       
$socialGroups = array();
       
       
$member = $member ?: \IPS\Member::loggedIn();
        if (
$member->member_id )
        {
           
$socialGroups = iterator_to_array( \IPS\Db::i()->select( 'group_id', 'core_sys_social_group_members', array( 'member_id=?', $member->member_id ) ) );
        }

        if (
count( $socialGroups ) )
        {
            return
$socialGroupColumn . '=0 OR ( ' . \IPS\Db::i()->in( $socialGroupColumn, $socialGroups ) . ' )';
        }
        else
        {
            return
$socialGroupColumn . '=0';
        }
    }

   
/**
     * Check the request for legacy parameters we may need to redirect to
     *
     * @return    NULL|\IPS\Http\Url
     */
   
public function checkForLegacyParameters()
    {
       
$paramsToSet    = array();
       
$paramsToUnset    = array();

       
/* st=20 needs to go to page=2 (or whatever the comments per page setting is set to) */
       
if( isset( \IPS\Request::i()->st ) )
        {
           
$commentsPerPage = static::getCommentsPerPage();

           
$paramsToSet['page']    = floor( intval( \IPS\Request::i()->st ) / $commentsPerPage ) + 1;
           
$paramsToUnset[]        = 'st';
        }

       
/* Did we have any? */
       
if( count( $paramsToSet ) )
        {
           
$url = $this->url();

            if(
count( $paramsToUnset ) )
            {
               
$url = $url->stripQueryString( $paramsToUnset );
            }

           
$url = $url->setQueryString( $paramsToSet );

            return
$url;
        }

        return
NULL;
    }

   
/**
     * Get mapped value
     *
     * @param    string    $key    date,content,ip_address,first
     * @return    mixed
     */
   
public function mapped( $key )
    {
        if ( isset( static::
$databaseColumnMap[ $key ] ) )
        {
           
$field = static::$databaseColumnMap[ $key ];
           
            if (
is_array( $field ) )
            {
               
$field = array_pop( $field );
            }
           
            return
$this->$field;
        }
        return
NULL;
    }
   
   
/**
     * Get author
     *
     * @return    \IPS\Member
     */
   
public function author()
    {
        if (
$this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') )
        {
            return \
IPS\Member::load( $this->mapped('author') );
        }
        else
        {
           
$guest = new \IPS\Member;
           
$guest->name = $this->mapped('author_name');
            return
$guest;
        }
    }
   
   
/**
     * Returns the content
     *
     * @return    string
     */
   
public function content()
    {
        return
$this->mapped('content');
    }
   
   
/**
     * Returns the content images
     *
     * @param    int|null    $limit        Number of attachments to fetch, or NULL for all
     *
     * @return    array|NULL
     * @throws    \BadMethodCallException
     */
   
public function contentImages( $limit = NULL )
    {
       
$idColumn = static::$databaseColumnId;
       
$internal = NULL;
       
$attachments = array();
       
        if ( isset( static::
$databaseColumnMap['content'] ) )
        {
           
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . mb_ucfirst( static::$module ), $this->$idColumn ) );
        }
       
        if (
$internal )
        {
            foreach( \
IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
            {
               
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
            }
        }
       
        return
count( $attachments ) ? $attachments : NULL;    
    }
   
   
/**
     * Text for use with data-ipsTruncate
     * Returns the post with paragraphs turned into line breaks
     *
     * @param    bool    $oneLine    If TRUE, will use spaces instead of line breaks. Useful if using a single line display.
     * @return    string
     * @note    For now we are removing all HTML. If we decide to change this to remove specific tags in future, we can use \IPS\Text\Parser::removeElements( $this->content() )
     */
   
public function truncated( $oneLine=FALSE )
    {    
       
/* Specifically remove quotes, any scripts (which someone with HTML posting allowed may have legitimately enabled, and spoilers (to prevent contents from being revealed) */
       
$text = \IPS\Text\Parser::removeElements( $this->content(), array( 'blockquote', 'script', 'div[class=ipsSpoiler]' ) );
       
       
/* Convert headers and paragraphs into line breaks or just spaces */
       
$text = str_replace( array( '</p>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>' ), ( $oneLine ? ' ' : '<br>' ), $text );

        if(
$oneLine === TRUE )
        {
           
$text = str_replace( '<br>', ' <br>', $text );
        }

       
/* Add a space at the end of list items to prevent two list items from running into each other */
       
$text = str_replace( '</li>', ' </li>', $text );
       
       
/* Remove all HTML apart from <br>s*/
       
$text = strip_tags( $text, '<br>' );
       
       
/* Remove any <br>s from the start so there isn't just blank space at the top, but maintaining <br>s elsewhere */
       
$text = preg_replace( '/^(\s|<br>|' . chr(0xC2) . chr(0xA0) . ')+/', '', $text );
       
       
/* Return */
       
return $text;
    }
   
   
/**
     * Delete Record
     *
     * @return    void
     */
   
public function delete()
    {
       
$idColumn = static::$databaseColumnId;
       
        if ( \
IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
        {
            \
IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) );
        }
       
        if ( \
IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
        {
           
$this->deleteReport();
        }

       
/* Remove any entries in the promotions table */
       
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', get_class( $this ), $this->$idColumn ) );
       
        \
IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );

        if ( static::
$hideLogKey )
        {
           
$idColumn = static::$databaseColumnId;
            \
IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
        }
       
       
parent::delete();

       
$this->expireWidgetCaches();
       
$this->adjustSessions();
    }

   
/**
     * Is this a future entry?
     *
     * @return bool
     */
   
public function isFutureDate()
    {
        if (
$this instanceof \IPS\Content\FuturePublishing )
        {
            if ( isset( static::
$databaseColumnMap['is_future_entry'] ) and isset( static::$databaseColumnMap['future_date'] ) )
            {
               
$column = static::$databaseColumnMap['future_date'];
                if (
$this->$column > time() )
                {
                    return
TRUE;
                }
            }
        }

        return
FALSE;
    }

   
/**
     * Return the tooltip blurb for future entries
     *
     * @return string
     */
   
public function futureDateBlurb()
    {
       
$column = static::$databaseColumnMap['future_date'];
       
$time   = \IPS\DateTime::ts( $this->$column );
        return  \
IPS\Member::loggedIn()->language()->addToStack("content_future_date_blurb", FALSE, array( 'sprintf' => array( $time->localeDate(), $time->localeTime() ) ) );
    }
   
   
/**
     * Content is hidden?
     *
     * @return    int
     *    @li -2 is pending deletion
     *     @li    -1 is hidden having been hidden by a moderator
     *     @li    0 is unhidden
     *    @li    1 is hidden needing approval
     * @note    The actual column may also contain 2 which means the item is hidden because the parent is hidden, but it is not hidden in itself. This method will return -1 in that case.
     *
     * @note    A piece of content (item and comment) can have an alias for hidden OR approved.
     *          With hidden: 0=not hidden, 1=hidden (needs moderator approval), -1=hidden by moderator, 2=parent item is hidden, -2=pending deletion
     *          With approved: 1=not hidden, 0=hidden (needs moderator approval), -1=hidden by moderator, -2=pending deletion
     *
     *          User posting has moderator approval set: When adding an unapproved ITEM (approved=0, hidden=1) you should *not* increment container()->_comments but you should update container()->_unapprovedItems
     *          User posting has moderator approval set: When adding an unapproved COMMENT (approved=0, hidden=1) you should *not* increment item()->num_comments in item or container()->_comments but you should update item()->unapproved_comments and container()->_unapprovedComments
     *
     *          User post is hidden by moderator (approved=-1, hidden=0) you should decrement item()->num_comments and decrement container()->_comments but *not* increment item()->unapproved_comments or container()->_unapprovedComments
     *          User item is hidden by a moderator (approved=-1, hidden=0) you should decrement container()->comments and subtract comment count from container()->_comments, but *not* increment container()->_unapprovedComments
     *
     *          Moderator hides item (approved=-1, hidden=-1) you should substract num_comments from container()->_comments. Comments inside item are flagged as approved=-1, hidden=2 but item()->num_comments should not be substracted from
     *
     *          Comments with a hidden value of 2 should increase item()->num_comments but not container()->_comments
     * @throws    \RuntimeException
     */
   
public function hidden()
    {
        if (
$this instanceof \IPS\Content\Hideable )
        {
            if ( isset( static::
$databaseColumnMap['hidden'] ) )
            {
               
$column = static::$databaseColumnMap['hidden'];
                return (
$this->$column == 2 ) ? -1 : intval( $this->$column );
            }
            elseif ( isset( static::
$databaseColumnMap['approved'] ) )
            {
               
$column = static::$databaseColumnMap['approved'];
                if (
$this->$column == -2 )
                {
                    return
intval( $this->$column );
                }
                return
$this->$column == -1 ? intval( $this->$column ) : intval( !$this->$column );
            }
            else
            {
                throw new \
RuntimeException;
            }
        }
       
        return
0;
    }
   
   
/**
     * Can see moderation tools
     *
     * @note    This is used generally to control if the user has permission to see multi-mod tools. Individual content items may have specific permissions
     * @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 canSeeMultiModTools( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
    {
        return static::
modPermission( 'pin', $member, $container ) or static::modPermission( 'unpin', $member, $container ) or static::modPermission( 'feature', $member, $container ) or static::modPermission( 'unfeature', $member, $container ) or static::modPermission( 'edit', $member, $container ) or static::modPermission( 'hide', $member, $container ) or static::modPermission( 'unhide', $member, $container ) or static::modPermission( 'delete', $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 )
    {
       
/* Compatibility checks */
       
if ( ( $type == 'hide' or $type == 'unhide' ) and !in_array( 'IPS\Content\Hideable', class_implements( get_called_class() ) ) )
        {
            return
FALSE;
        }
        if ( (
$type == 'pin' or $type == 'unpin' ) and !in_array( 'IPS\Content\Pinnable', class_implements( get_called_class() ) ) )
        {
            return
FALSE;
        }
        if ( (
$type == 'feature' or $type == 'unfeature' ) and !in_array( 'IPS\Content\Featurable', class_implements( get_called_class() ) ) )
        {
            return
FALSE;
        }
        if ( (
$type == 'future_publish' ) and !in_array( 'IPS\Content\FuturePublishing', class_implements( get_called_class() ) ) )
        {
            return
FALSE;
        }

       
/* If this is called from a gateway script, i.e. email piping, just return false as we are a "guest" */
       
if( $member === NULL AND !\IPS\Dispatcher::hasInstance() )
        {
            return
FALSE;
        }
       
       
/* Load Member */
       
$member = $member ?: \IPS\Member::loggedIn();

       
/* Global permission */
       
if ( $member->modPermission( "can_{$type}_content" ) )
        {
            return
TRUE;
        }
       
/* Per-container permission */
       
elseif ( $container )
        {
            return
$container->modPermission( $type, $member, static::getContainerModPermissionClass() );
        }
       
       
/* Still here? return false */
       
return FALSE;
    }

   
/**
     * Get the container item class to use for mod permission checks
     *
     * @return    string|NULL
     * @note    By default we will return NULL and the container check will execute against Node::$contentItemClass, however
     *    in some situations we may need to override this (i.e. for Gallery Albums)
     */
   
protected static function getContainerModPermissionClass()
    {
        return
NULL;
    }
       
   
/**
     * Do Moderator Action
     *
     * @param    string                $action    The action
     * @param    \IPS\Member|NULL    $member    The member doing the action (NULL for currently logged in member)
     * @param    string|NULL            $reason    Reason (for hides)
     * @param    bool                $immediately Delete Immediately
     * @return    void
     * @throws    \OutOfRangeException|\InvalidArgumentException|\RuntimeException
     */
   
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL, $immediately = FALSE )
    {
        if(
$action === 'approve' )
        {
           
$action    = 'unhide';
        }

       
/* Check it's a valid action */
       
if ( !in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'hide', 'unhide', 'move', 'lock', 'unlock', 'delete', 'publish', 'restore', 'restoreAsHidden' ) ) )
        {
            throw new \
InvalidArgumentException;
        }
       
       
/* And that we can do it */
       
$toCheck = $action;
        if (
$action == 'restoreAsHidden' )
        {
           
$toCheck = 'restore';
        }
       
        if ( !
call_user_func( array( $this, 'can' . mb_ucfirst( $toCheck ) ), $member ) )
        {
            throw new \
OutOfRangeException;
        }
       
       
/* Log */
       
\IPS\Session::i()->modLog( 'modlog__action_' . $action, array( static::$title => TRUE, $this->url()->__toString() => FALSE, $this->mapped('title') ?: ( method_exists( $this, 'item' ) ? $this->item()->mapped('title') : NULL ) => FALSE ), ( $this instanceof \IPS\Content\Item ) ? $this : $this->item() );
       
       
/* These ones just need a property setting */
       
if ( in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'lock', 'unlock' ) ) )
        {
           
$val = TRUE;
            switch (
$action )
            {
                case
'unpin':
                   
$val = FALSE;
                case
'pin':
                   
$column = static::$databaseColumnMap['pinned'];
                    break;
               
                case
'unfeature':
                   
$val = FALSE;
                case
'feature':
                   
$column = static::$databaseColumnMap['featured'];
                    break;
               
                case
'unlock':
                   
$val = FALSE;
                case
'lock':
                    if ( isset( static::
$databaseColumnMap['locked'] ) )
                    {
                       
$column = static::$databaseColumnMap['locked'];
                    }
                    else
                    {
                       
$val = $val ? 'closed' : 'open';
                       
$column = static::$databaseColumnMap['status'];
                    }
                    break;
            }
           
$this->$column = $val;
           
$this->save();

            return;
        }
       
       
/* Hide is a tiny bit more complicated */
       
elseif ( $action === 'hide' )
        {
           
$this->hide( $member, $reason );
            return;
        }
        elseif (
$action === 'unhide' )
        {
           
$this->unhide( $member );
            return;
        }
       
       
/* Delete is just a method */
       
elseif ( $action === 'delete' )
        {
           
/* If we are retaining content for a period of time, we need to just hide it instead for deleting later - this only works, though, with items that implement \IPS\Content\Hideable */
           
if ( \IPS\Settings::i()->dellog_retention_period AND ( $this instanceof \IPS\Content\Hideable ) AND $immediately === FALSE )
            {
               
$this->logDelete();
                return;
            }
           
           
$idColumn = static::$databaseColumnId;
           
$this->delete();
            return;
        }
       
       
/* Restore is just a method */
       
elseif ( $action === 'restore' )
        {
           
$this->restore();
            return;
        }
       
       
/* Restore As Hidden is just a method */
       
elseif ( $action === 'restoreAsHidden' )
        {
           
$this->restore( TRUE );
            return;
        }

       
/* Publish is just a method */
       
elseif ( $action === 'publish' )
        {
           
$this->publish();
            return;
        }

       
/* Move is just a method */
       
elseif ( $action === 'move' )
        {
           
$args    = func_get_args();
           
$this->move( $args[2][0], $args[2][1] );
            return;
        }
    }
   
   
/**
     * Log for deletion later
     *
     * \IPS\Member|NULL     $member    The member or NULL for currently logged in
     * @return    void
     */
   
public function logDelete( $member = NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();
       
       
/* Log it! */
       
$log = new \IPS\core\DeletionLog;
       
$log->setContentAndMember( $this, $member );
       
$log->save();
       
        if ( isset( static::
$databaseColumnMap['hidden'] ) )
        {
           
$column = static::$databaseColumnMap['hidden'];
        }
        else if ( isset( static::
$databaseColumnMap['approved'] ) )
        {
           
$column = static::$databaseColumnMap['approved'];
        }
       
       
$this->$column = -2;
       
$this->save();
       
        if (
$this instanceof \IPS\Content\Comment )
        {
           
$item = $this->item();
           
           
/* Update last comment stuff */
           
$item->resyncLastComment();

           
/* Update last review stuff */
           
$item->resyncLastReview();

           
/* Update number of comments */
           
$item->resyncCommentCounts();

           
/* Update number of reviews */
           
$item->resyncReviewCounts();

           
/* Save*/
           
$item->save();
        }
       
        try
        {
            if (
$this->container() )
            {
               
$this->container()->resetCommentCounts();
               
$this->container()->setLastComment();
               
$this->container()->setLastReview();
               
$this->container()->save();
            }
        }
        catch( \
BadMethodCallException $e ) {}
    }
   
   
/**
     * Restore Content
     *
     * @param    bool    Restore as hidden?
     * @return    void
     * @throws \BadMethodCallException
     */
   
public function restore( $hidden = FALSE )
    {
        try
        {
           
$idColumn = static::$databaseColumnId;
           
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id", get_class( $this ), $this->$idColumn ) )->first() );
        }
        catch( \
UnderflowException $e )
        {
            throw new \
BadMethodCallException;
        }
       
       
/* Restoring as hidden? */
       
if ( $hidden )
        {
            if ( isset( static::
$databaseColumnMap['hidden'] ) )
            {
               
$column = static::$databaseColumnMap['hidden'];
            }
            else if ( isset( static::
$databaseColumnMap['approved'] ) )
            {
               
$column = static::$databaseColumnMap['approved'];
            }
           
           
$this->$column = -1;
        }
        else
        {
            if ( isset( static::
$databaseColumnMap['hidden'] ) )
            {
               
$column = static::$databaseColumnMap['hidden'];
               
$this->$column = 0;
            }
            else if ( isset( static::
$databaseColumnMap['approved'] ) )
            {
               
$column = static::$databaseColumnMap['approved'];
               
$this->$column = 1;
            }
        }

       
/* Save the changes */
       
$this->save();

       
/* Reindex the now hidden content */
       
if ( $this instanceof \IPS\Content\Item and static::$firstCommentRequired )
        {
            \
IPS\Content\Search\Index::i()->index( $this->firstComment() );
        }
        else
        {
            \
IPS\Content\Search\Index::i()->index( $this );
        }
       
       
/* Delete the log */
       
$log->delete();

       
/* Recount the container counters */
       
if( $this->container() )
        {
           
$this->container()->resetCommentCounts();
           
$this->container()->setLastComment();
           
$this->container()->setLastReview();
           
$this->container()->save();
        }
    }
   
   
/**
     * Can restore?*
     *
     * @param    \IPS\Member|NULL    The member, or currently logged in member
     * @return    bool
     */
   
public function canRestore( $member=NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();
        return
$member->modPermission('can_manage_deleted_content');
    }
   
   
/**
     * Give class a chance to inspect and manipulate search engine filters for streams
     *
     * @param    array                         $filters    Filters to be used for activity stream
     * @param    \IPS\Content\Search\Query    $query        Search query object
     * @return    void
     */
   
public static function searchEngineFiltering( &$filters, &$query )
    {
       
/* Intentionally left blank but child classes can override */
   
}
   
   
/**
     * Hide
     *
     * @param    \IPS\Member|NULL|FALSE    $member    The member doing the action (NULL for currently logged in member, FALSE for no member)
     * @param    string                    $reason    Reason
     * @return    void
     */
   
public function hide( $member, $reason = NULL )
    {
        if ( isset( static::
$databaseColumnMap['hidden'] ) )
        {
           
$column = static::$databaseColumnMap['hidden'];
        }
        elseif ( isset( static::
$databaseColumnMap['approved'] ) )
        {
           
$column = static::$databaseColumnMap['approved'];
        }
        else
        {
            throw new \
RuntimeException;
        }

       
/* Already hidden? */
       
if( $this->$column == -1 )
        {
            return;
        }

       
$this->$column = -1;
       
$this->save();
       
$this->onHide( $member );
       
        if ( static::
$hideLogKey )
        {
           
$idColumn = static::$databaseColumnId;
            \
IPS\Db::i()->delete( 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) );
            \
IPS\Db::i()->insert( 'core_soft_delete_log', array(
               
'sdl_obj_id'        => $this->$idColumn,
               
'sdl_obj_key'        => static::$hideLogKey,
               
'sdl_obj_member_id'    => $member === FALSE ? 0 : intval( $member ? $member->member_id : \IPS\Member::loggedIn()->member_id ),
               
'sdl_obj_date'        => time(),
               
'sdl_obj_reason'    => $reason,
               
            ) );
        }
       
        if (
$this instanceof \IPS\Content\Tags )
        {
            \
IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 0 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
        }

       
/* Update search index */
       
if ( $this instanceof \IPS\Content\Searchable )
        {
            \
IPS\Content\Search\Index::i()->index( $this );
        }

       
$this->expireWidgetCaches();
       
$this->adjustSessions();
    }
   
   
/**
     * Unhide
     *
     * @param    \IPS\Member|NULL|FALSE    $member    The member doing the action (NULL for currently logged in member, FALSE for no member)
     * @return    void
     */
   
public function unhide( $member )
    {
       
/* If we're approving, we have to do extra stuff */
       
$approving = FALSE;
        if (
$this->hidden() === 1 )
        {
           
$approving = TRUE;
            if ( isset( static::
$databaseColumnMap['approved_by'] ) and $member !== FALSE )
            {
               
$column = static::$databaseColumnMap['approved_by'];
               
$this->$column = $member ? $member->member_id : \IPS\Member::loggedIn()->member_id;
            }
            if ( isset( static::
$databaseColumnMap['approved_date'] ) )
            {
               
$column = static::$databaseColumnMap['approved_date'];
               
$this->$column = time();
            }
        }

       
/* Now do the actual stuff */
       
if ( isset( static::$databaseColumnMap['hidden'] ) )
        {
           
$column = static::$databaseColumnMap['hidden'];

           
/* Already approved? */
           
if( $this->$column == 0 )
            {
                return;
            }

           
$this->$column = 0;
        }
        elseif ( isset( static::
$databaseColumnMap['approved'] ) )
        {
           
$column = static::$databaseColumnMap['approved'];

           
/* Already approved? */
           
if( $this->$column == 1 )
            {
                return;
            }

           
$this->$column = 1;
        }
        else
        {
            throw new \
RuntimeException;
        }
       
$this->save();
       
$this->onUnhide( $approving, $member );

        if ( static::
$hideLogKey )
        {
           
$idColumn = static::$databaseColumnId;
            \
IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
        }

       
/* And update the tags perm cache */
       
if ( $this instanceof \IPS\Content\Tags )
        {
            \
IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
        }
       
       
/* Update search index */
       
if ( $this instanceof \IPS\Content\Searchable )
        {
            \
IPS\Content\Search\Index::i()->index( $this );
        }
       
       
/* Update report center stuff */
       
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
        {
           
$this->moderated( 'unhide' );
        }
       
       
/* Send notifications if necessary */
       
if ( $approving )
        {
           
$this->sendApprovedNotification();
        }

       
$this->expireWidgetCaches();
       
$this->adjustSessions();
    }

   
/**
     * @brief    Hidden blurb cache
     */
   
protected $hiddenBlurb    = NULL;

   
/**
     * Blurb for when/why/by whom this content was hidden
     *
     * @return    string
     */
   
public function hiddenBlurb()
    {
        if ( !(
$this instanceof \IPS\Content\Hideable ) or !static::$hideLogKey )
        {
            throw new \
BadMethodCallException;
        }
       
        if(
$this->hiddenBlurb === NULL )
        {
            try
            {
               
$idColumn = static::$databaseColumnId;
               
$log = \IPS\Db::i()->select( '*', 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) )->first();
               
                if (
$log['sdl_obj_member_id'] )
                {
                   
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb', FALSE, array( 'sprintf' => array( \IPS\Member::load( $log['sdl_obj_member_id'] )->name, \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), \IPS\Member::loggedIn()->language()->addToStack( $log['sdl_obj_reason'] ) ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
                }
                else
                {
                   
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb_no_member', FALSE, array( 'sprintf' => array( \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), \IPS\Member::loggedIn()->language()->addToStack( $log['sdl_obj_reason'] ) ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
                }
           
            }
            catch ( \
UnderflowException $e )
            {
               
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
            }
        }

        return
$this->hiddenBlurb;
    }
   
   
/**
     * Blurb for when/why/by whom this content was deleted
     *
     * @return    string
     * @throws \BadMethodCallException
     */
   
public function deletedBlurb()
    {
        if ( !(
$this instanceof \IPS\Content\Hideable ) )
        {
            throw new \
BadMethodCallException;
        }
       
        try
        {
           
$idColumn = static::$databaseColumnId;
           
$log = \IPS\core\DeletionLog::constructFromData( \IPS\Db::i()->select( '*', 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) )->first() );
            return \
IPS\Member::loggedIn()->language()->addToStack( 'deletion_blurb', FALSE, array( 'sprintf' => array( $log->_deleted_by->name, $log->deleted_date->fullYearLocaleDate(), $log->deletion_date->fullYearLocaleDate() ) ) );
        }
        catch( \
UnderflowException $e )
        {
            return \
IPS\Member::loggedIn()->language()->addToStack('deleted');
        }
    }
   
   
/**
     * Can promote this comment/item?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    boolean
     */
   
public function canPromoteToSocialMedia( $member=NULL )
    {
        return \
IPS\core\Promote::canPromote( $member );
    }

   
/**
     * @brief    Have we already reported?
     */
   
protected $alreadyReported = NULL;
   
   
/**
     * Can report?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    TRUE|string            TRUE or a language string for why not
     * @note    This requires a few queries, so don't run a check in every template
     */
   
public function canReport( $member=NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();
       
       
/* Is this type of comment reportabe? */
       
if ( !( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) ) )
        {
            return
'generic_error';
        }
       
       
/* Can the member report content? */
       
if ( $member->group['g_can_report'] != '1' AND !in_array( get_class( $this ), explode( ',', $member->group['g_can_report'] ) ) )
        {
            return
'no_module_permission';
        }
       
       
/* Can they view this? */
       
if ( !$this->canView() )
        {
            return
'no_module_permission';
        }

       
/* Have they already subitted a report? */
       
if( $this->alreadyReported === TRUE )
        {
            return
'report_err_already_reported';
        }
        elseif(
$this->alreadyReported === NULL )
        {
           
$idColumn = static::$databaseColumnId;

            try
            {
               
$report = \IPS\Db::i()->select( 'id', 'core_rc_index', array( 'class=? AND content_id=?', get_called_class(), $this->$idColumn ) )->first();
               
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? AND report_by=?', $report, $member->member_id ) )->first();
               
                if ( \
IPS\Settings::i()->automoderation_report_again_mins )
                {
                    if ( ( (
time() - $report['date_reported'] ) / 60 ) > \IPS\Settings::i()->automoderation_report_again_mins )
                    {
                        return
TRUE;
                    }
                }
               
               
$this->alreadyReported = TRUE;
                return
'report_err_already_reported';
            }
            catch( \
UnderflowException $e ){}

           
$this->alreadyReported = FALSE;
        }
       
        return
TRUE;
    }

   
/**
     * Can report or revoke report?
     * This method will return TRUE if the link to report content should be shown (which can occur even if you have already reported if you have permission to revoke your report)
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     * @note    This requires a few queries, so don't run a check in every template
     */
   
public function canReportOrRevoke( $member=NULL )
    {
       
/* If we are allowed to report, then we can return TRUE. */
       
if( $this->canReport( $member ) === TRUE )
        {
            return
TRUE;
        }
       
/* If we have already reported but automatic moderation is enabled, show the link so the user can revoke their report. */
       
elseif( $this->alreadyReported === TRUE AND \IPS\Settings::i()->automoderation_enabled )
        {
            return
TRUE;
        }

        return
FALSE;
    }
   
   
/**
     * Report
     *
     * @param    string    $reportContent    Report content message from member
     * @param    int        $reportType        Report type (see constants in \IPS\core\Reports\Report
     * @return    \IPS\core\Reports\Report
     * @throws    \UnexpectedValueException    If there is a permission error - you should only call this method after checking canReport
     */
   
public function report( $reportContent, $reportType=1 )
    {
       
/* Permission check */
       
if ( $this->canReport() !== TRUE )
        {
            throw new \
UnexpectedValueException;
        }
       
       
/* Find or create an index */
       
$idColumn = static::$databaseColumnId;
        try
        {
           
$index = \IPS\core\Reports\Report::load( $this->$idColumn, 'content_id', array( 'class=?', get_called_class() ) );
           
$index->num_reports = $index->num_reports + 1;
        }
        catch ( \
OutOfRangeException $e )
        {
           
$index = new \IPS\core\Reports\Report;
           
$index->class = get_called_class();
           
$index->content_id = $this->$idColumn;
           
$index->perm_id = $this->permId();
           
$index->first_report_by = (int) \IPS\Member::loggedIn()->member_id;
           
$index->first_report_date = time();
           
$index->last_updated = time();
           
$index->author = (int) $this->author()->member_id;
           
$index->num_reports = 1;
           
$index->num_comments = 0;
           
$index->auto_moderation_exempt = 0;
        }

       
/* Only set this to a new report if it is not already under review */
       
if( $index->status != 2 )
        {
           
$index->status = 1;
        }

       
$index->save();

       
/* Create a report */
       
$reportInsert = array(
           
'rid'            => $index->id,
           
'report'        => $reportContent,
           
'report_by'        => (int) \IPS\Member::loggedIn()->member_id,
           
'date_reported'    => time(),
           
'ip_address'    => \IPS\Request::i()->ipAddress(),
           
'report_type'    => \IPS\Member::loggedIn()->member_id ? $reportType : 0
       
);
       
        \
IPS\Db::i()->insert( 'core_rc_reports', $reportInsert );
       
       
/* Run automatic moderation */
       
$index->runAutomaticModeration();
       
       
/* Send notification to mods */
       
$moderators = array( 'm' => array(), 'g' => array() );
        foreach ( \
IPS\Db::i()->select( '*', 'core_moderators' ) as $mod )
        {
           
$canView = FALSE;
            if (
$mod['perms'] == '*' )
            {
               
$canView = TRUE;
            }
            if (
$canView === FALSE )
            {
               
$perms = json_decode( $mod['perms'], TRUE );
               
                if ( isset(
$perms['can_view_reports'] ) AND $perms['can_view_reports'] === TRUE )
                {
                   
$canView = TRUE;
                }
            }
            if (
$canView === TRUE )
            {
               
$moderators[ $mod['type'] ][] = $mod['id'];
            }
        }
       
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) );
        foreach ( \
IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
        {
           
$notification->recipients->attach( \IPS\Member::constructFromData( $member ) );
        }
       
$notification->send();
       
       
/* Return */
       
return $index;
    }
   
   
/**
     * Change IP Address
     * @param    string        $ip        The new IP address
     *
     * @return void
     */
   
public function changeIpAddress( $ip )
    {
        if ( isset( static::
$databaseColumnMap['ip_address'] ) )
        {
           
$col = static::$databaseColumnMap['ip_address'];
           
$this->$col = (string) $ip;
           
$this->save();
        }
    }
   
   
/**
     * Change Author
     *
     * @param    \IPS\Member    $newAuthor    The new author
     * @return    void
     */
   
public function changeAuthor( \IPS\Member $newAuthor )
    {
       
$oldAuthor = $this->author();

       
/* If we delete a member, then change author, the old author returns 0 as does the new author as the
           member row is deleted before the task is run */
       
if( $newAuthor->member_id and ( $oldAuthor->member_id == $newAuthor->member_id ) )
        {
            return;
        }

        foreach ( array(
'author', 'author_name', 'edit_member_name' ) as $k )
        {
            if ( isset( static::
$databaseColumnMap[ $k ] ) )
            {
               
$col = static::$databaseColumnMap[ $k ];
                switch (
$k )
                {
                    case
'author':
                       
$this->$col = $newAuthor->member_id ? $newAuthor->member_id : 0;
                        break;
                   
                    case
'author_name':
                    case
'edit_member_name':
                       
/* Real name will contain the custom guest name if available or '' if not */
                       
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
                        break;
                }
            }
        }
       
$this->save();

        if ( \
IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
        {
            \
IPS\Session::i()->modLog( 'modlog__action_changeauthor', array( static::$title => TRUE, $this->url()->__toString() => FALSE, $this->mapped('title') ?: ( method_exists( $this, 'item' ) ? $this->item()->mapped('title') : NULL ) => FALSE ), ( $this instanceof \IPS\Content\Item ) ? $this : $this->item() );
        }
    }
   
   
/**
     * 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() )
    {
       
/* Item details */
       
$itemClass = $indexData['index_class'];
        if (
in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) )
        {
           
$itemClass = static::$itemClass;
           
$unread = $itemClass::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE );
        }
        else
        {
           
$unread = static::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE );
        }
       
$itemUrl = $itemClass::urlFromIndexData( $indexData, $itemData );
       
       
/* Object URL */
       
$indefiniteArticle = static::_indefiniteArticle( $containerData );
       
$definiteArticle = static::_definiteArticle( $containerData );
       
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
        if (
in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) )
        {
            if (
in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
            {
               
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findReview', 'review' => $indexData['index_object_id'] ) );
               
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsReview', 'review' => $indexData['index_object_id'] ) );
            }
            else
            {
               
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
               
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
            }
           
           
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
           
$definiteArticle = $itemClass::_definiteArticle( $containerData );
           
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
        }
        else
        {
           
$objectUrl = $itemUrl;
           
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
        }
       
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
       
       
/* Container details */
       
$containerUrl = NULL;
       
$containerTitle = NULL;
        if ( isset(
$itemClass::$containerNodeClass ) )
        {
           
$containerClass    = $itemClass::$containerNodeClass;
           
$containerTitle    = $containerClass::titleFromIndexData( $indexData, $itemData, $containerData );
           
$containerUrl    = $containerClass::urlFromIndexData( $indexData, $itemData, $containerData );
        }
               
       
/* Reputation - if we are showing the total value, then we need to load them up and total up all of the values */
       
if ( \IPS\Settings::i()->reaction_count_display == 'count' )
        {
           
$repCount = 0;
            foreach(
$reputationData AS $memberId => $reactionId )
            {
                try
                {
                   
$repCount += \IPS\Content\Reaction::load( $reactionId )->value;
                }
                catch( \
OutOfRangeException $e ) {}
            }
        }
        else
        {
           
$repCount = count( $reputationData );
        }
       
       
/* Snippet */
       
$snippet = static::searchResultSnippet( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $view );
       
        if (
$template === NULL )
        {
           
$template = array( \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' ), 'searchResult' );
        }
       
       
/* Return */
       
return call_user_func_array( $template, array( $indexData, $articles, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments, $reactions ) );
    }
   
   
/**
     * Get snippet 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    string        $view            'expanded' or 'condensed'
     * @return    callable
     */
   
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating, $view )
    {        
        return
$view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
    }

   
/**
     * Return the language string key to use in search results
     *
     * @note Normally we show "(user) posted a (thing) in (area)" but sometimes this may not be accurate, so this is abstracted to allow
     *    content classes the ability to override
     * @param    array         $authorData        Author data
     * @param    array         $articles        Articles language strings
     * @param    array         $indexData        Search index data
     * @param    array         $itemData        Data about the item
     * @return    string
     */
   
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData )
    {
        if(
in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
        {
            if(
in_array( 'IPS\Content\Review', class_parents( $indexData['index_class'] ) ) )
            {
                if( isset(
$itemData['author'] ) )
                {
                    return \
IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_review", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
                }
                else
                {
                    return \
IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_review", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
                }
            }
            else
            {
                if( static::
$firstCommentRequired )
                {
                    if(
$indexData['index_title'] )
                    {
                        return \
IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
                    }
                    else
                    {
                        if( isset(
$itemData['author'] ) )
                        {
                            return \
IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_reply", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
                        }
                        else
                        {
                            return \
IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_reply", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
                        }
                    }
                }
                else
                {
                    if( isset(
$itemData['author'] ) )
                    {
                        return \
IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_comment", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
                    }
                    else
                    {
                        return \
IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_comment", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
                    }
                }
            }
        }
        else
        {
            if ( isset( static::
$databaseColumnMap['author'] ) )
            {
                return \
IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
            }
            else
            {
                return \
IPS\Member::loggedIn()->language()->addToStack( "generic_activity_item", FALSE, array( 'sprintf' => array( $articles['definite_uc'] ) ) );
            }
        }
    }

   
/**
     * @brief    Return a classname applied to the search result block
     */
   
public static $searchResultClassName = '';

   
/**
     * Return the filters that are available for selecting table rows
     *
     * @return    array
     */
   
public static function getTableFilters()
    {
       
$return = array();
       
        if (
in_array( 'IPS\Content\Hideable', class_implements( get_called_class() ) ) )
        {
           
$return[] = 'hidden';
           
$return[] = 'unhidden';
           
$return[] = 'unapproved';
        }
               
        return
$return;
    }
   
   
/**
     * Get content table states
     *
     * @return string
     */
   
public function tableStates()
    {
       
$return    = array();

        if (
$this instanceof \IPS\Content\Hideable )
        {
            switch (
$this->hidden() )
            {
                case -
1:
                   
$return[] = 'hidden';
                    break;
                case
0:
                   
$return[] = 'unhidden';
                    break;
                case
1:
                   
$return[] = 'unapproved';
                    break;
            }
        }
       
        return
implode( ' ', $return );
       
    }
   
   
/**
     * Prune IP addresses from content
     *
     * @param    int        $days         Remove from content posted older than DAYS ago
     * @return    void
     */
   
public static function pruneIpAddresses( $days=0 )
    {
        if (
$days and isset( static::$databaseColumnMap['ip_address'] ) and isset( static::$databaseColumnMap['date'] ) )
        {
           
$time = time() - ( 86400 * $days );
            \
IPS\Db::i()->update( static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['ip_address'] => '' ), array( static::$databasePrefix . static::$databaseColumnMap['date'] . ' <= ' . $time ) );
        }
    }
   
   
/* !Follow */
   
   
const FOLLOW_PUBLIC = 1;
    const
FOLLOW_ANONYMOUS = 2;
       
    const
NOTIFICATIONS_PER_BATCH = \IPS\NOTIFICATIONS_PER_BATCH;
   
   
/**
     * Send notifications
     *
     * @return    void
     */
   
public function sendNotifications()
    {        
       
/* Send quote and mention notifications */
       
$sentTo = $this->sendQuoteAndMentionNotifications();
       
       
/* How many followers? */
       
$idColumn = $this::$databaseColumnId;
        try
        {
           
$count = $this->notificationRecipients( NULL, NULL, TRUE );
        }
        catch ( \
BadMethodCallException $e )
        {
            return;
        }
       
       
/* Queue if there's lots, or just send them */
       
if ( $count > static::NOTIFICATIONS_PER_BATCH )
        {
            \
IPS\Task::queue( 'core', 'Follow', array( 'class' => get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo ), 2 );
        }
        else
        {
           
$this->sendNotificationsBatch( 0, $sentTo );
        }
    }
   
   
/**
     * Send notifications batch
     *
     * @param    int                $offset        Current offset
     * @param    array            $sentTo        Members who have already received a notification and how - e.g. array( 1 => array( 'inline', 'email' )
     * @param    string|NULL        $extra        Additional data
     * @return    int|null        New offset or NULL if complete
     */
   
public function sendNotificationsBatch( $offset=0, &$sentTo=array(), $extra=NULL )
    {
       
$followIds = array();
       
$followers = $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra );
       
       
/* Send notification */
       
$notification = $this->createNotification( $extra );
       
$notification->unsubscribeType = 'follow';
        foreach (
$followers as $follower )
        {
           
$member = \IPS\Member::load( $follower['follow_member_id'] );
            if (
$member != $this->author() and $this->canView( $member ) )
            {
               
$followIds[] = $follower['follow_id'];
               
$notification->recipients->attach( $member, $follower );
            }
        }

       
/* Log that we sent it */
       
if( count( $followIds ) )
        {
            \
IPS\Db::i()->update( 'core_follow', array( 'follow_notify_sent' => time() ), \IPS\Db::i()->in( 'follow_id', $followIds ) );
        }

       
$sentTo = $notification->send( $sentTo );
       
       
/* Update the queue */
       
$newOffset = $offset + static::NOTIFICATIONS_PER_BATCH;
        if (
$newOffset > $followers->count( TRUE ) )
        {
            return
NULL;
        }
        return
$newOffset;
    }
   
   
/**
     * Send Approved Notification
     *
     * @return    void
     */
   
public function sendApprovedNotification()
    {
       
$this->sendNotifications();
    }
   
   
/**
     * Send Unapproved Notification
     *
     * @return    void
     */
   
public function sendUnapprovedNotification()
    {
       
$moderators = array( 'g' => array(), 'm' => array() );
        foreach( \
IPS\Db::i()->select( '*', 'core_moderators' ) AS $mod )
        {
           
$canView = FALSE;
           
$canApprove = FALSE;
            if (
$mod['perms'] == '*' )
            {
               
$canView = TRUE;
               
$canApprove = TRUE;
            }
            else
            {
               
$perms = json_decode( $mod['perms'], TRUE );
                               
                foreach ( array(
'canView' => 'can_view_hidden_', 'canApprove' => 'can_unhide_' ) as $varKey => $modPermKey )
                {
                    if ( isset(
$perms[ $modPermKey . 'content' ] ) AND $perms[ $modPermKey . 'content' ] )
                    {
                        $
$varKey = TRUE;
                    }
                    else
                    {                        
                        try
                        {
                           
$container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->container() : $this->container();
                           
$containerClass = get_class( $container );
                           
$title = static::$title;
                            if
                            (
                                isset(
$containerClass::$modPerm )
                                and
                                (
                                   
$perms[ $containerClass::$modPerm ] === -1
                                   
or
                                    (
                                       
is_array( $perms[ $containerClass::$modPerm ] )
                                        and
                                       
in_array( $container->_id, $perms[ $containerClass::$modPerm ] )
                                    )
                                )
                                and
                               
$perms["{$modPermKey}{$title}"]
                            )
                            {
                                $
$varKey = TRUE;
                            }
                        }
                        catch ( \
BadMethodCallException $e ) { }
                    }
                }
            }
            if (
$canView === TRUE and $canApprove === TRUE )
            {
               
$moderators[ $mod['type'] ][] = $mod['id'];
            }
        }
                       
       
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'unapproved_content', $this, array( $this, $this->author() ) );
        foreach ( \
IPS\Db::i()->select( '*', 'core_members', ( count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
        {
           
/* We don't need to notify the author of the content */
           
if( $this->author()->member_id != $member['member_id'] )
            {
               
$notification->recipients->attach(\IPS\Member::constructFromData($member));
            }
        }
       
$notification->send();
    }
   
   
/**
     * Send the notifications after the content has been edited (for any new quotes or mentiones)
     *
     * @param    string    $oldContent    The content before the edit
     * @return    void
     */
   
public function sendAfterEditNotifications( $oldContent )
    {                
       
$existingData = static::_getQuoteAndMentionIdsFromContent( $oldContent );
       
$this->sendQuoteAndMentionNotifications( array_unique( array_merge( $existingData['quotes'], $existingData['mentions'] ) ) );
    }
       
   
/**
     * Send quote and mention notifications
     *
     * @param    array    $exclude        An array of member IDs *not* to send notifications to
     * @return    array    The members that were notified and how they were notified
     */
   
protected function sendQuoteAndMentionNotifications( $exclude=array() )
    {
        return
$this->_sendQuoteAndMentionNotifications( static::_getQuoteAndMentionIdsFromContent( $this->content() ), $exclude );
    }
   
   
/**
     * Send quote and mention notifications from data
     *
     * @param    array    array( 'quotes' => array( ... member IDs ... ), 'mentions' => array( ... member IDs ... ) )
     * @param    array    $exclude        An array of member IDs *not* to send notifications to
     * @return    array    The members that were notified and how they were notified
     */
   
protected function _sendQuoteAndMentionNotifications( $data, $exclude=array() )
    {
       
/* Init */
       
$sentTo = array();
       
       
/* Quotes */
       
$data['quotes'] = array_filter( $data['quotes'], function( $v ) use ( $exclude )
        {
            return !
in_array( $v, $exclude );
        } );
        if ( !empty(
$data['quotes'] ) )
        {
           
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'quote', ( $this instanceof \IPS\Content\Item ) ? $this : $this->item(), array( $this ), array( $this->author()->member_id ), FALSE );
            foreach (
$data['quotes'] as $quote )
            {
               
$member = \IPS\Member::load( $quote );
                if (
$member->member_id and $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'posts' ) )
                {
                   
$notification->recipients->attach( $member );
                }
            }
           
$sentTo = $notification->send( $sentTo );
        }
       
       
/* Mentions */
       
$data['mentions'] = array_filter( $data['mentions'], function( $v ) use ( $exclude )
        {
            return !
in_array( $v, $exclude );
        } );
        if ( !empty(
$data['mentions'] ) )
        {
           
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'mention', ( $this instanceof \IPS\Content\Item ) ? $this : $this->item(), array( $this ), array( $this->author()->member_id ), FALSE );
            foreach (
$data['mentions'] as $mention )
            {
               
$member = \IPS\Member::load( $mention );
                if (
$member->member_id AND $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'mentions' ) )
                {
                   
$notification->recipients->attach( $member );
                }
            }
           
$sentTo = $notification->send( $sentTo );
        }
   
       
/* Return */
       
return $sentTo;
    }
   
   
/**
     * Get quote and mention notifications
     *
     * @param    string    $content    The content
     * @return    array    array( 'quotes' => array( ... member IDs ... ), 'mentions' => array( ... member IDs ... ) )
     */
   
protected static function _getQuoteAndMentionIdsFromContent( $content )
    {
       
$return = array( 'quotes' => array(), 'mentions' => array() );
       
       
$document = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
        if ( @
$document->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( '<div>' . $content . '</div>' ) ) !== FALSE )
        {
           
/* Quotes */
           
foreach( $document->getElementsByTagName('blockquote') as $quote )
            {
                if (
$quote->getAttribute('data-ipsquote-userid') and (int) $quote->getAttribute('data-ipsquote-userid') > 0 )
                {
                   
$return['quotes'][] = $quote->getAttribute('data-ipsquote-userid');
                }
            }
           
           
/* Mentions */
           
foreach( $document->getElementsByTagName('a') as $link )
            {
                if (
$link->getAttribute('data-mentionid') )
                {                    
                    if ( !
preg_match( '/\/blockquote(\[\d*\])?\//', $link->getNodePath() ) )
                    {
                       
$return['mentions'][] = $link->getAttribute('data-mentionid');
                    }
                }
            }
        }
       
        return
$return;
    }
   
   
/**
     * Expire appropriate widget caches automatically
     *
     * @return void
     */
   
public function expireWidgetCaches()
    {
        \
IPS\Widget::deleteCaches( NULL, static::$application );
    }

   
/**
     * Update "currently viewing" session data after moderator actions that invalidate that data for other users
     *
     * @return void
     */
   
public function adjustSessions()
    {
        if(
$this instanceof \IPS\Content\Comment )
        {
           
$item = $this->item();
        }
        else
        {
           
$item = $this;
        }

       
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
       
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE ) as $session )
        {
            if(
mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
            {
               
$sessionData = $session;
               
$sessionData['location_url']            = NULL;
               
$sessionData['location_lang']            = NULL;
               
$sessionData['location_data']            = json_encode( array() );
               
$sessionData['current_id']                = 0;
               
$sessionData['location_permissions']    = 0;

                \
IPS\Session\Store::i()->updateSession( $sessionData );
            }
        }
    }

   
/**
     * Fetch classes from content router
     *
     * @param    bool|\IPS\Member    $member        Check member access
     * @param    bool                $archived    Include any supported archive classes
     * @param    bool                $onlyItems    Only include item classes
     * @return    array
     */
   
public static function routedClasses( $member=FALSE, $archived=FALSE, $onlyItems=FALSE )
    {
       
$classes    = array();

        foreach ( \
IPS\Application::allExtensions( 'core', 'ContentRouter', $member, NULL, NULL, TRUE ) as $router )
        {
            foreach (
$router->classes as $class )
            {
               
$classes[]    = $class;

                if(
$onlyItems )
                {
                    continue;
                }
               
                if ( !(
$member instanceof \IPS\Member ) )
                {
                   
$member = $member ? \IPS\Member::loggedIn() : NULL;
                }
               
                if ( isset(
$class::$commentClass ) and $class::supportsComments( $member ) )
                {
                   
$classes[]    = $class::$commentClass;
                }

                if ( isset(
$class::$reviewClass ) and $class::supportsReviews( $member ) )
                {
                   
$classes[]    = $class::$reviewClass;
                }

                if(
$archived === TRUE AND isset( $class::$archiveClass ) )
                {
                   
$classes[]    = $class::$archiveClass;
                }
            }
        }

        return
$classes;
    }

   
/**
     * Override the HTML parsing enabled flag for rebuilds?
     *
     * @note    By default this will return FALSE, but classes can override
     * @see        \IPS\forums\Topic\Post
     * @return    bool
     */
   
public function htmlParsingEnforced()
    {
        return
FALSE;
    }

   
/**
     * Return any custom multimod actions this content item supports
     *
     * @return    array
     */
   
public function customMultimodActions()
    {
        return array();
    }

   
/**
     * Return any available custom multimod actions this content item class supports
     *
     * @note    Return in format of EITHER
     *    @li    array( array( 'action' => ..., 'icon' => ..., 'label' => ... ), ... )
     *    @li    array( array( 'grouplabel' => ..., 'icon' => ..., 'groupaction' => ..., 'action' => array( array( 'action' => ..., 'label' => ... ), ... ) ) )
     * @note    For an example, look at \IPS\core\Announcements\Announcement
     * @return    array
     */
   
public static function availableCustomMultimodActions()
    {
        return array();
    }

   
/**
     * Get HTML for search result display
     *
     * @return    callable
     */
   
public function approvalQueueHtml( $ref=NULL, $container, $title )
    {
        return \
IPS\Theme::i()->getTemplate( 'modcp', 'core', 'front' )->approvalQueueItem( $this, $ref, $container, $title );
    }

   
/**
     * Indefinite Article
     *
     * @param    \IPS\Lang|NULL    $language    The language to use, or NULL for the language of the currently logged in member
     * @return    string
     */
   
public function indefiniteArticle( \IPS\Lang $lang = NULL )
    {
       
$container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper();
        return static::
_indefiniteArticle( $container ? $container->_data : array(), $lang );
    }
   
   
/**
     * Indefinite Article
     *
     * @param    \IPS\Lang|NULL    $language    The language to use, or NULL for the language of the currently logged in member
     * @return    string
     */
   
public static function _indefiniteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL )
    {
       
$lang = $lang ?: \IPS\Member::loggedIn()->language();
        return
$lang->addToStack( '__indefart_' . static::$title, FALSE );
    }
   
   
/**
     * Definite Article
     *
     * @param    \IPS\Lang|NULL    $language    The language to use, or NULL for the language of the currently logged in member
     * @return    string
     */
   
public function definiteArticle( \IPS\Lang $lang = NULL )
    {
       
$container = ( $this instanceof \IPS\Content\Comment ) ? $this->item()->containerWrapper() : $this->containerWrapper();
        return static::
_definiteArticle( $container ? $container->_data : array(), $lang );
    }
   
   
/**
     * Definite Article
     *
     * @param    array            $containerData    Basic data about the container. Only includes columns returned by container::basicDataColumns()
     * @param    \IPS\Lang|NULL    $language        The language to use, or NULL for the language of the currently logged in member
     * @param    array            $options        Options to pass to \IPS\Lang::addToStack
     * @return    string
     */
   
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL, $options = array() )
    {
       
$lang = $lang ?: \IPS\Member::loggedIn()->language();
        return
$lang->addToStack( '__defart_' . static::$title, FALSE, $options );
    }

   
/**
     * Get preview image for share services
     *
     * @return    string
     */
   
public function shareImage()
    {
        return (string) \
IPS\Theme::i()->logo_sharer;
    }

   
/**
     * Log keyword usage, if any
     *
     * @param    string        $content    Content/text of submission
     * @param    string|NULL    $title        Title of submission
     * @return    void
     */
   
public function checkKeywords( $content, $title=NULL )
    {
       
/* Do we have any keywords to track? */
       
if( !\IPS\Settings::i()->stats_keywords )
        {
            return;
        }

       
/* We need to know the ID */
       
$idColumn    = static::$databaseColumnId;

       
/* If this is a content item and first comment is required, skip checking the comment */
       
if ( $this instanceof \IPS\Content\Comment )
        {
           
$itemClass = static::$itemClass;

            if(
$itemClass::$firstCommentRequired === TRUE )
            {
               
/* During initial post, at this point the firstCommentIdColumn value won't be set, so we check for that or explicitly if this is the first post */
               
if( !$this->item()->mapped('first_comment_id') OR $this->$idColumn == $this->item()->mapped('first_comment_id') )
                {
                    return;
                }
            }
        }

       
$words = preg_split("/[\s]+/", trim( strip_tags( preg_replace( "/<br( \/)?>/", "\n", $content ) ) ), NULL, PREG_SPLIT_NO_EMPTY );

        if(
$title !== NULL )
        {
           
$titleWords = explode( ' ', $title );
           
$words        = array_merge( $words, $titleWords );
        }

       
$words = array_unique( $words );

       
$keywords = json_decode( \IPS\Settings::i()->stats_keywords, true );

       
$extraData    = json_encode( array( 'class' => get_class( $this ), 'id' => $this->$idColumn ) );

        foreach(
$keywords as $keyword )
        {
            if(
in_array( $keyword, $words ) )
            {
                \
IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
            }
        }
    }
   
   
/* !Search */
   
    /**
     * Title for search index
     *
     * @return    string
     */
   
public function searchIndexTitle()
    {
        return
$this->mapped('title');
    }
   
   
/**
     * Content for search index
     *
     * @return    string
     */
   
public function searchIndexContent()
    {
        return
$this->mapped('content');
    }
}