Seditio Source
Root |
./othercms/ips_4.3.4/applications/forums/sources/Topic/Topic.php
<?php
/**
 * @brief        Topic 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
 * @subpackage    Forums
 * @since        8 Jan 2014
 */

namespace IPS\forums;

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

/**
 * Topic Model
 */
class _Topic extends \IPS\Content\Item implements
    \
IPS\Content\Permissions,
    \
IPS\Content\Pinnable, \IPS\Content\Lockable, \IPS\Content\Hideable, \IPS\Content\Featurable,
    \
IPS\Content\Tags,
    \
IPS\Content\Followable,
    \
IPS\Content\Shareable,
    \
IPS\Content\ReadMarkers,
    \
IPS\Content\Views,
    \
IPS\Content\Polls, \SplObserver,
    \
IPS\Content\Ratings,
    \
IPS\Content\Searchable,
    \
IPS\Content\Embeddable,
    \
IPS\Content\MetaData
{
    use \
IPS\Content\Reportable;
   
    const
ARCHIVE_NOT = 0;
    const
ARCHIVE_DONE = 1;
    const
ARCHIVE_WORKING = 2;
    const
ARCHIVE_EXCLUDE = 3;
    const
ARCHIVE_RESTORE = 4;
   
   
/**
     * @brief    Multiton Store
     */
   
protected static $multitons;

   
/**
     * @brief    [ActiveRecord] ID Database Column
     */
   
public static $databaseColumnId = 'tid';

   
/**
     * @brief    Application
     */
   
public static $application = 'forums';
   
   
/**
     * @brief    Module
     */
   
public static $module = 'forums';

   
/**
     * @brief    Database Table
     */
   
public static $databaseTable = 'forums_topics';
   
   
/**
     * @brief    Database Prefix
     */
   
public static $databasePrefix = '';
           
   
/**
     * @brief    Database Column Map
     */
   
public static $databaseColumnMap = array(
       
'author'                => 'starter_id',
       
'author_name'            => 'starter_name',
       
'container'                => 'forum_id',
       
'date'                    => 'start_date',
       
'title'                    => 'title',
       
'num_comments'            => 'posts',
       
'unapproved_comments'    => 'topic_queuedposts',
       
'hidden_comments'        => 'topic_hiddenposts',
       
'first_comment_id'        => 'topic_firstpost',
       
'last_comment'            => array( 'last_real_post', 'last_post' ),
       
'last_comment_by'        => 'last_poster_id',
       
'last_comment_name'        => 'last_poster_name',
       
'views'                    => 'views',
       
'approved'                => 'approved',
       
'pinned'                => 'pinned',
       
'poll'                    => 'poll_state',
       
'status'                => 'state',
       
'moved_to'                => 'moved_to',
       
'moved_on'                => 'moved_on',
       
'featured'                => 'featured',
       
'state'                    => 'state',
       
'updated'                => 'last_post',
       
'meta_data'                => 'topic_meta_data'
   
);

   
/**
     * @brief    Title
     */
   
public static $title = 'topic';
   
   
/**
     * @brief    Node Class
     */
   
public static $containerNodeClass = 'IPS\forums\Forum';
   
   
/**
     * @brief    [Content\Item]    Comment Class
     */
   
public static $commentClass = 'IPS\forums\Topic\Post';

   
/**
     * @brief    Archived comment class
     */
   
public static $archiveClass = 'IPS\forums\Topic\ArchivedPost';

   
/**
     * @brief    [Content\Item]    First "comment" is part of the item?
     */
   
public static $firstCommentRequired = TRUE;
   
   
/**
     * @brief    [Content\Comment]    Language prefix for forms
     */
   
public static $formLangPrefix = 'topic_';
   
   
/**
     * @brief    Icon
     */
   
public static $icon = 'comments';
   
   
/**
     * @brief    [Content]    Key for hide reasons
     */
   
public static $hideLogKey = 'topic';
   
   
/**
     * @brief    Hover preview
     */
   
public $tableHoverUrl = TRUE;
   
   
/**
     * Callback from \IPS\Http\Url\Inernal::correctUrlFromVerifyClass()
     *
     * This is called when verifying the *the URL currently being viewed* is correct, before calling self::loadFromUrl()
     * Can be used if there is a more effecient way to load and cache the objects that will be used later on that page
     *
     * @param    \IPS\Http\Url    $url    The URL of the page being viewed, which belongs to this class
     * @return    void
     */
   
public static function preCorrectUrlFromVerifyClass( \IPS\Http\Url $url )
    {
        \
IPS\forums\Forum::loadIntoMemory();
    }

   
/**
     * Check the request for legacy parameters we may need to redirect to
     *
     * @return    NULL|\IPS\Http\Url
     */
   
public function checkForLegacyParameters()
    {
       
/* Check for any changes in the parent, i.e. st=20 */
       
$url = parent::checkForLegacyParameters();

       
$paramsToSet    = array();
       
$paramsToUnset    = array();

       
/* view=findpost needs to go to do=findComment */
       
if( isset( \IPS\Request::i()->view ) AND \IPS\Request::i()->view == 'findpost' )
        {
           
$paramsToSet['do']        = 'findComment';
           
$paramsToUnset[]        = 'view';
        }

       
/* p=123 needs to go to comment=123 */
       
if( isset( \IPS\Request::i()->p ) )
        {
           
$paramsToSet['do']        = 'findComment';
           
$paramsToSet['comment']        = \IPS\Request::i()->p;
           
$paramsToUnset[]        = 'p';
        }

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

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

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

            return
$url;
        }

        return
$url;
    }

   
/**
     * Set custom posts per page setting
     *
     * @return int
     */
   
public static function getCommentsPerPage()
    {
        return \
IPS\Settings::i()->forums_posts_per_page;
    }
   
   
/**
     * Get comment count
     *
     * @return    int
     */
   
public function commentCount()
    {
       
$count = parent::commentCount();
       
        if (
$this->isQuestion() )
        {
           
$count--;
        }
       
        return
$count;
    }

   
/**
     * Should posting this increment the poster's post count?
     *
     * @param    \IPS\Node\Model|NULL    $container    Container
     * @return    void
     * @see        \IPS\Topic\Post::incrementPostCount()
     */
   
public static function incrementPostCount( \IPS\Node\Model $container = NULL )
    {
        return
FALSE;
    }

   
/**
     * Get items with permission check
     *
     * @param    array        $where                Where clause
     * @param    string        $order                MySQL ORDER BY clause (NULL to order by date)
     * @param    int|array    $limit                Limit clause
     * @param    string|NULL    $permissionKey        A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index or NULL to ignore permissions
     * @param    mixed        $includeHiddenItems    Include hidden items? NULL to detect if currently logged in member has permission, -1 to return public content only, TRUE to return unapproved content and FALSE to only return unapproved content the viewing member submitted
     * @param    int            $queryFlags            Select bitwise flags
     * @param    \IPS\Member    $member                The member (NULL to use currently logged in member)
     * @param    bool        $joinContainer        If true, will join container data (set to TRUE if your $where clause depends on this data)
     * @param    bool        $joinComments        If true, will join comment data (set to TRUE if your $where clause depends on this data)
     * @param    bool        $joinReviews        If true, will join review data (set to TRUE if your $where clause depends on this data)
     * @param    bool        $countOnly            If true will return the count
     * @param    array|null    $joins                Additional arbitrary joins for the query
     * @param    mixed        $skipPermission        If you are getting records from a specific container, pass the container to reduce the number of permission checks necessary or pass TRUE to skip conatiner-based permission. You must still specify this in the $where clause
     * @param    bool        $joinTags            If true, will join the tags table
     * @param    bool        $joinAuthor            If true, will join the members table for the author
     * @param    bool        $joinLastCommenter    If true, will join the members table for the last commenter
     * @param    bool        $showMovedLinks        If true, moved item links are included in the results
     * @return    \IPS\Patterns\ActiveRecordIterator|int
     */
   
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE )
    {
       
$where = static::getItemsWithPermissionWhere( $where, $permissionKey, $member, $joinContainer, $skipPermission );
        return
parent::getItemsWithPermission( $where, $order, $limit, $permissionKey, $includeHiddenItems, $queryFlags, $member, $joinContainer, $joinComments, $joinReviews, $countOnly, $joins, $skipPermission, $joinTags, $joinAuthor, $joinLastCommenter, $showMovedLinks );
    }
   
   
/**
     * Additional WHERE clauses for Follow view
     *
     * @param    bool        $joinContainer        If true, will join container data (set to TRUE if your $where clause depends on this data)
     * @param    array        $joins                Other joins
     * @return    array
     */
   
public static function followWhere( &$joinContainer, &$joins )
    {
       
$joinContainer = FALSE;
        return
array_merge( parent::followWhere( $joinContainer, $joins ), static::getItemsWithPermissionWhere( array(), 'read', NULL, $joinContainer ) );
    }
   
   
/**
     * WHERE clause for getItemsWithPermission
     *
     * @param    array        $where                Current WHERE clause
     * @param    string        $permissionKey        A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index
     * @param    \IPS\Member    $member                The member (NULL to use currently logged in member)
     * @param    bool        $joinContainer        If true, will join container data (set to TRUE if your $where clause depends on this data)
     * @param    mixed        $skipPermission        If you are getting records from a specific container, pass the container to reduce the number of permission checks necessary or pass TRUE to skip container-based permission. You must still specify this in the $where clause
     * @return    array
     */
   
public static function getItemsWithPermissionWhere( $where, $permissionKey, $member, &$joinContainer, $skipPermission=FALSE )
    {
       
/* Don't show topics in password protected forums */
       
if ( !$skipPermission and in_array( $permissionKey, array( 'view', 'read' ) ) )
        {
           
$joinContainer = TRUE;
           
$member = $member ?: \IPS\Member::loggedIn();
                       
           
$whereString = 'forums_forums.password=? OR ' . \IPS\Db::i()->findInSet( 'forums_forums.password_override', $member->groups );
           
$whereParams = array( NULL );
            if ( \
IPS\Dispatcher::hasInstance() AND $member === \IPS\Member::loggedIn() )
            {
                foreach ( \
IPS\Request::i()->cookie as $k => $v )
                {
                    if (
mb_substr( $k, 0, 13 ) === 'ipbforumpass_' )
                    {
                       
$whereString .= ' OR ( forums_forums.id=? AND MD5(forums_forums.password)=? )';
                       
$whereParams[] = (int) mb_substr( $k, 13 );
                       
$whereParams[] = $v;
                    }
                }
            }
           
           
$where['container'][] = array_merge( array( '( ' . $whereString . ' )' ), $whereParams );
        }

       
/* Don't show topics from forums in which topics only show to the poster */
       
if ( $skipPermission !== TRUE and in_array( $permissionKey, array( 'view', 'read' ) ) )
        {
           
$member = $member ?: \IPS\Member::loggedIn();

            if ( !
$member->modPermission( 'can_read_all_topics' ) or ( $member->modPermission( 'forums' ) !== TRUE and $member->modPermission( 'forums' ) !== -1 ) )
            {
                if (
$skipPermission instanceof \IPS\forums\Forum )
                {
                    if ( !
$skipPermission->can_view_others )
                    {
                       
$where['item'][] = array( 'forums_topics.starter_id=?', $member->member_id );
                    }
                }
                else
                {
                   
$joinContainer = TRUE;
                   
                   
$whereClause = array( '( forums_forums.can_view_others=1 OR forums_topics.starter_id=? )', (int) $member->member_id );
                   
                    if (
$member->modPermission( 'can_read_all_topics' ) )
                    {
                       
$forums = $member->modPermission( 'forums' );
                        if ( isset(
$forums ) and is_array ( $forums ) )
                        {
                           
$whereClause[0] = "( {$whereClause[0]} OR " . \IPS\Db::i()->in( 'forums_topics.forum_id', $forums ) . ' )';
                        }
                    }
                   
                   
$where[] = $whereClause;
                }
            }
        }

       
/* Don't show topics in forums we can't view because our post count is too low */
       
if ( !$skipPermission and in_array( $permissionKey, array( 'view', 'read' ) ) )
        {
           
$member = $member ?: \IPS\Member::loggedIn();
           
$joinContainer = TRUE;
           
$where['container'][] = array( 'forums_forums.min_posts_view<=?', $member->member_posts );
        }
       
       
/* Return */
       
return $where;
    }
   
   
/**
     * Total item count (including children)
     *
     * @param    \IPS\Node\Model    $container            The container
     * @param    bool            $includeItems        If TRUE, items will be included (this should usually be true)
     * @param    bool            $includeComments    If TRUE, comments will be included
     * @param    bool            $includeReviews        If TRUE, reviews will be included
     * @param    int                $depth                Used to keep track of current depth to avoid going too deep
     * @return    int|NULL|string    When depth exceeds 10, will return "NULL" and initial call will return something like "100+"
     * @note    This method may return something like "100+" if it has lots of children to avoid exahusting memory. It is intended only for display use
     * @note    This method includes counts of hidden and unapproved content items as well
     */
   
public static function contentCount( \IPS\Node\Model $container, $includeItems=TRUE, $includeComments=FALSE, $includeReviews=FALSE, $depth=0 )
    {
        return
parent::contentCount( $container, FALSE, TRUE, $includeReviews, $depth );
    }
   
   
/**
     * 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
     * @return    array
     */
   
public static function formElements( $item=NULL, \IPS\Node\Model $container=NULL )
    {
       
$formElements = parent::formElements( $item, $container );
       
       
/* Password protected */
       
if ( $container !== NULL AND !$container->loggedInMemberHasPasswordAccess() )
        {
           
$password = $container->password;
           
$formElements['password'] = new \IPS\Helpers\Form\Password( 'password', NULL, TRUE, array(), function( $val ) use ( $password )
            {
                if (
$val != $password )
                {
                    throw new \
DomainException( 'forum_password_bad' );
                }
            } );
        }

       
/* Build the topic state toggles */
       
$options = array();
       
$toggles = array();
       
$current = array();
        if ( static::
modPermission( 'lock', NULL, $container ) )
        {
           
$options['lock'] = 'create_topic_locked';
           
$toggles['lock'] = array( 'create_topic_locked' );
            if(
$item and $item->locked() )
            {
               
$current[] = 'lock';
            }
        }
       
        if ( static::
modPermission( 'pin', NULL, $container ) )
        {
           
$options['pin'] = 'create_topic_pinned';
           
$toggles['pin'] = array( 'create_topic_pinned' );
            if(
$item and $item->mapped('pinned') )
            {
               
$current[] = 'pin';
            }
        }
       
        if ( static::
modPermission( 'hide', NULL, $container ) )
        {
           
$options['hide'] = 'create_topic_hidden';
           
$toggles['hide'] = array( 'create_topic_hidden' );
            if(
$item and $item->hidden() === -1 )
            {
               
$current[] = 'hide';
            }
        }
       
        if ( static::
modPermission( 'feature', NULL, $container ) )
        {
           
$options['feature'] = 'create_topic_featured';
           
$toggles['feature'] = array( 'create_topic_featured' );
            if(
$item and $item->mapped('featured') )
            {
               
$current[] = 'feature';
            }
        }

        if (
count( $options ) or count ( $toggles ) )
        {
           
$formElements['topic_state'] = new \IPS\Helpers\Form\CheckboxSet( 'topic_create_state', $current, FALSE, array(
               
'options'     => $options,
               
'toggles'    => $toggles,
               
'multiple'    => TRUE
           
) );    
        }        
       
        if ( static::
modPermission( 'lock', NULL, $container ) )
        {
           
/* Poll always needs to go on the end */
           
$poll = NULL;
            if ( isset(
$formElements['poll'] ) )
            {
               
$poll = $formElements['poll'];
                unset(
$formElements['poll'] );
            }
           
           
/* Add lock/unlock options */
           
if ( static::modPermission( 'unlock', NULL, $container ) )
            {
               
$formElements['topic_open_time'] = new \IPS\Helpers\Form\Date( 'topic_open_time', ( $item and $item->topic_open_time ) ? \IPS\DateTime::ts( $item->topic_open_time ) : NULL, FALSE, array( 'time' => TRUE ) );
            }
           
$formElements['topic_close_time'] = new \IPS\Helpers\Form\Date( 'topic_close_time', ( $item and $item->topic_close_time ) ? \IPS\DateTime::ts( $item->topic_close_time ) : NULL, FALSE, array( 'time' => TRUE ) );
           
           
/* Poll always needs to go on the end */
           
if ( $poll )
            {
               
$formElements['poll'] = $poll;
            }
        }

        return
$formElements;
    }
   
   
/**
     * Process create/edit form
     *
     * @param    array                $values    Values from form
     * @return    void
     */
   
public function processForm( $values )
    {
       
parent::processForm( $values );
       
        if ( isset(
$values['password'] ) )
        {
           
/* Set Cookie */
           
$this->container()->setPasswordCookie( $values['password'] );
        }

       
/* Moderator actions */
       
if ( isset( $values['topic_create_state'] ) )
        {
            if ( static::
modPermission( 'lock', NULL, $this->container() ) )
            {
               
$this->state = ( in_array( 'lock', $values['topic_create_state'] ) ) ? 'closed' : 'open';
            }
           
            if ( static::
modPermission( 'pin', NULL, $this->container() ) )
            {
               
$this->pinned = ( in_array( 'pin', $values['topic_create_state'] ) ) ? 1 : 0;
            }

            if ( static::
modPermission( 'feature', NULL, $this->container() ) )
            {
               
$this->featured = ( in_array( 'feature', $values['topic_create_state'] ) ) ? 1 : 0;
            }
        }

        if ( static::
modPermission( 'lock', NULL, $this->container() ) )
        {
           
/* Set open/close time */
           
$this->topic_open_time = !empty( $values['topic_open_time'] ) ? $values['topic_open_time']->getTimestamp() : 0;
           
$this->topic_close_time = !empty( $values['topic_close_time'] ) ? $values['topic_close_time']->getTimestamp() : 0;
           
            if( isset(
$values['topic_create_state'] ) and !in_array( 'lock', $values['topic_create_state'] ) )
            {
               
$this->state = 'open';
            }

           
/* If open time is before close time, close now */
           
if ( $this->topic_open_time and $this->topic_close_time and $this->topic_open_time < $this->topic_close_time )
            {
               
$this->state = 'closed';
            }
        }
    }
   
   
/**
     * Process created object AFTER the object has been created
     *
     * @param    \IPS\Content\Comment|NULL    $comment    The first comment
     * @param    array                        $values        Values from form
     * @return    void
     */
   
protected function processAfterCreate( $comment, $values )
    {
       
$this->processAfterCreateOrEdit( $values );
       
       
parent::processAfterCreate( $comment, $values );
    }
   
   
/**
     * Process after the object has been edited on the front-end
     *
     * @param    array    $values        Values from form
     * @return    void
     */
   
public function processAfterEdit( $values )
    {
       
$this->processAfterCreateOrEdit( $values );
       
       
/* Initial Comment */
       
parent::processAfterEdit( $values );
       
       
/* Topic changed? */
       
if ( ! $this->hidden() and ( $this->tid === $this->container()->last_id ) )
        {
           
$this->container()->seo_last_title = $this->title_seo;
           
$this->container()->last_title     = $this->title;
           
$this->container()->save();
           
            foreach(
$this->container()->parents() AS $parent )
            {
                if ( (
$this->tid === $parent->last_id ) and ( $this->title_seo !== $parent->seo_last_title ) )
                {
                   
$parent->seo_last_title        = $this->title_seo;
                   
$parent->last_title            = $this->title;
                   
$parent->save();
                }
            }
        }
    }
   
   
/**
     * Process after the object has been edited or created on the front-end
     *
     * @param    array    $values        Values from form
     * @return    void
     */
   
protected function processAfterCreateOrEdit( $values )
    {
       
/* Moderator actions */
       
if ( isset( $values['topic_create_state'] ) )
        {
            if ( static::
modPermission( 'hide', NULL, $this->container() ) )
            {
                if(
in_array( 'hide', $values['topic_create_state'] ) )
                {
                   
$this->hide( NULL );
                }
               
/* Only unhide if the topic is not pending approval */
               
elseif( $this->hidden() !== 1 )
                {
                   
$this->unhide( NULL );
                }
            }
        }
    }

   
/**
     * @brief    Cached URLs
     */
   
protected $_url    = array();
   
   
/**
     * @brief    URL Base
     */
   
public static $urlBase = 'app=forums&module=forums&controller=topic&id=';
   
   
/**
     * @brief    URL Base
     */
   
public static $urlTemplate = 'forums_topic';
   
   
/**
     * @brief    SEO Title Column
     */
   
public static $seoTitleColumn = 'title_seo';

   
/**
     * Stats for table view
     *
     * @param    bool    $includeFirstCommentInCommentCount    Determines whether the first comment should be inlcluded in the comment count (e.g. For "posts", use TRUE. For "replies", use FALSE)
     * @return    array
     */
   
public function stats( $includeFirstCommentInCommentCount=TRUE )
    {
        if (
$this->popular_time !== NULL and $this->popular_time > time() )
        {
           
$this->hotStats[] = 'forums_comments';
           
$this->hotStats[] = 'answers_no_number';
        }
       
       
$stats = parent::stats( $includeFirstCommentInCommentCount );

        if( !
$includeFirstCommentInCommentCount )
        {
            if( isset(
$stats['comments'] ) )
            {
               
$stats = array_reverse( $stats );

                if(
$this->container()->forums_bitoptions['bw_enable_answers'] )
                {
                   
$stats['answers_no_number']    = $stats['comments'];
                }
                else
                {
                   
$stats['forums_comments']    = $stats['comments'];
                }

                unset(
$stats['comments'] );
               
$stats = array_reverse( $stats );
            }
        }

        return
$stats;
    }

   
/**
     * Set the new popular time if needed
     *
     */
   
public function rebuildPopularTime( )
    {
       
$popularNowSettings = json_decode( \IPS\Settings::i()->forums_popular_now, TRUE );

        if (
$popularNowSettings['posts'] and $popularNowSettings['minutes'] )
        {
           
$popularNowInterval = new \DateInterval( 'PT' . $popularNowSettings['minutes'] . 'M' );

           
$comments = iterator_to_array( new \IPS\Patterns\ActiveRecordIterator(
                \
IPS\Db::i()->select( '*', 'forums_posts', array( 'queued IN(0,2) AND post_date >? AND topic_id=?', \IPS\DateTime::create()->sub( $popularNowInterval )->getTimestamp(), $this->tid ), 'post_date DESC', NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER ),
               
'IPS\\forums\\Topic\\Post'
           
) );
            if (
count( $comments ) >= $popularNowSettings['posts'] )
            {
               
$commentToBasePopularNowTimeOff = array_slice( $comments, ( $popularNowSettings['posts'] - 1 ), 1 );
               
$commentToBasePopularNowTimeOff = array_pop( $commentToBasePopularNowTimeOff );

               
$this->popular_time = \IPS\DateTime::ts( $commentToBasePopularNowTimeOff->post_date )->add( $popularNowInterval )->getTimestamp();
               
$this->save();
            }
            elseif (
$this->popular_time !== NULL )
            {
               
$this->popular_time = NULL;
               
$this->save();
            }
        }
    }
   
   
/**
     * Set name
     *
     * @param    string    $title    Title
     * @return    void
     */
   
public function set_title( $title )
    {
       
$this->_data['title'] = $title;
       
$this->_data['title_seo'] = \IPS\Http\Url\Friendly::seoTitle( $title );
    }

   
/**
     * Get SEO name
     *
     * @return    string
     */
   
public function get_title_seo()
    {
        if( !
$this->_data['title_seo'] )
        {
           
$this->title_seo    = \IPS\Http\Url\Friendly::seoTitle( $this->title );
           
$this->save();
        }

        return
$this->_data['title_seo'] ?: \IPS\Http\Url\Friendly::seoTitle( $this->title );
    }

   
/**
     * Can view?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for or NULL for the currently logged in member
     * @return    bool
     */
   
public function canView( $member=NULL )
    {
        if ( !
parent::canView( $member ) )
        {
            return
FALSE;
        }
       
        if (
$minPostsToView = $this->container()->min_posts_view )
        {
           
$member = $member ?: \IPS\Member::loggedIn();
            if (
$minPostsToView > $member->member_posts )
            {
                return
FALSE;
            }
        }
       
        if ( !
$this->container()->can_view_others )
        {
           
$member = $member ?: \IPS\Member::loggedIn();
            if (
$member != $this->author() )
            {
                if
                (
                    (
                       
$member->modPermission( 'forums' ) !== -1
                       
and
                        (
                           
is_array( $member->modPermission( 'forums' ) )
                            and
                            !
in_array( $this->container()->_id, $member->modPermission( 'forums' ) )
                        )
                    )
                    or
                    !
$member->modPermission( 'can_read_all_topics' )
                )
                {
                    return
FALSE;
                }
            }
        }
               
        return
TRUE;
    }
   
   
/**
     * 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 = $this->container()->searchIndexPermissions();
       
        if (
$this->starter_id and !$this->container()->can_view_others )
        {
           
/* If the search index permissions are empty, just return now because no one can see content in this forum */
           
if( !$return )
            {
                return
$return;
            }

           
$return = $this->container()->permissionsThatCanAccessAllTopics();

            if (
$this->starter_id )
            {
               
$return[] = "m{$this->starter_id}";
            }

           
$return = implode( ',', $return );
        }
               
        return
$return;
    }
           
   
/**
     * Can Rate?
     *
     * @param    \IPS\Member|NULL        $member        The member to check for (NULL for currently logged in member)
     * @return    bool
     * @throws    \BadMethodCallException
     */
   
public function canRate( \IPS\Member $member = NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
$this->container()->forum_allow_rating and parent::canRate( $member );
    }
   
   
/**
     * Can create polls?
     *
     * @param    \IPS\Member|NULL        $member        The member to check for (NULL for currently logged in member)
     * @param    \IPS\Node\Model|NULL    $container    The container to check if tags can be used in, if applicable
     * @return    bool
     */
   
public static function canCreatePoll( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
    {
        return
parent::canCreatePoll( $member, $container ) and ( $container === NULL or $container->allow_poll );
    }
   
   
/**
     * SplObserver notification that poll has been voted on
     *
     * @param    \SplSubject    $subject    Subject
     * @return    void
     */
   
public function update( \SplSubject $subject )
    {
       
$this->last_vote = time();
       
       
$this->save();
    }
   
   
/**
     * Get template for content tables
     *
     * @return    callable
     */
   
public static function contentTableTemplate()
    {
        \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'forums.css', 'forums', 'front' ) );
        \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'forums_responsive.css', 'forums', 'front' ) );
        return array( \
IPS\Theme::i()->getTemplate( 'global', 'forums', 'front' ), 'rows' );
    }

   
/**
     * Table: Get rows
     *
     * @param    array    $rows    Rows to show
     * @return    void
     */
   
public static function tableGetRows( $rows )
    {
       
$openIds = array();
       
$closeIds = array();
       
$timeNow = time();
       
        foreach (
$rows as $topic )
        {
            if (
$topic->state != 'link' )
            {
               
$locked = $topic->locked();
                if (
$locked and $topic->topic_open_time and $topic->topic_open_time < $timeNow )
                {
                   
$openIds[] = $topic->tid;
                   
$topic->state = 'open';
                }
                if ( !
$locked and $topic->topic_close_time and $topic->topic_close_time < $timeNow )
                {
                   
$closeIds[] = $topic->tid;
                   
$topic->state = 'closed';
                }
            }
        }

        if ( !empty(
$openIds ) )
        {
            \
IPS\Db::i()->update( 'forums_topics', array( 'state' => 'open', 'topic_open_time' => 0 ), \IPS\Db::i()->in( 'tid', $openIds ) );
        }
        if ( !empty(
$closeIds ) )
        {
            \
IPS\Db::i()->update( 'forums_topics', array( 'state' => 'closed', 'topic_close_time' => 0 ), \IPS\Db::i()->in( 'tid', $closeIds ) );
        }
    }

   
/**
     * 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 (
$container and ( $container->preview_posts == 1 or $container->preview_posts == 2 ) and !$member->group['g_avoid_q'] )
        {
            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 )
    {
        if ( (
$this->container()->preview_posts == 1 or $this->container()->preview_posts == 3 ) and !$member->group['g_avoid_q'] )
        {
            return
TRUE;
        }
       
        return
parent::moderateNewComments( $member );
    }
   
   
/**
     * Move
     *
     * @param    \IPS\Node\Model    $container    Container to move to
     * @param    bool            $keepLink    If TRUE, will keep a link in the source
     * @return    void
     */
   
public function move( \IPS\Node\Model $container, $keepLink=FALSE )
    {
        if(    !
$container->sub_can_post or $container->redirect_url )
        {
            throw new \
InvalidArgumentException;
        }

       
parent::move( $container, $keepLink );
        \
IPS\Db::i()->update( 'forums_question_ratings', array( 'forum' => $container->_id ), array( 'topic=?', $this->tid ) );
    }
   
   
/**
     * Delete Record
     *
     * @return    void
     */
   
public function delete()
    {
       
parent::delete();

        try
        {
            \
IPS\forums\Topic\ArchivedPost::db()->delete( 'forums_archive_posts', array( 'archive_topic_id=?', $this->tid ) );
        }
       
/* catch db exceptions if e.g. if the connection credentials didn't work or if the database doesn't exist anymore */
       
catch ( \IPS\Db\Exception $e ){}

        \
IPS\Db::i()->delete( 'forums_question_ratings', array( 'topic=?', $this->tid ) );
        \
IPS\Db::i()->delete( 'forums_answer_ratings', array( 'topic=?', $this->tid ) );

       
/* Delete any moved topic links that point to this topic */
       
\IPS\Db::i()->delete( 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) );
    }

   
/**
     * Merge other items in (they will be deleted, this will be kept)
     *
     * @param    array    $items    Items to merge in
     * @param    bool    $keepLinks    Retain redirect links for the items that were merge in
     * @return    void
     */
   
public function mergeIn( array $items, $keepLinks=FALSE )
    {
       
/* If this is a QA forum we need to make sure we only have one best answer (at most) post-merge */
       
if( $this->isQuestion() )
        {
           
/* Does this topic already have a best answer? */
           
if( $this->topic_answered_pid )
            {
               
/* Then we need to make sure none of the items also has a best answer */
               
foreach( $items as $item )
                {
                   
/* Reset best answer for this topic */
                   
if( $item->topic_answered_pid )
                    {
                        try
                        {
                           
$post = \IPS\forums\Topic\Post::load( $item->topic_answered_pid );
                           
$post->post_bwoptions['best_answer'] = FALSE;
                           
$post->save();
                        }
                        catch( \
OutOfRangeException $e ){}
                    }
                }
            }
           
/* The topic doesn't have a best answer, but we still need to make sure we only have one best answer total post-merge */
           
else
            {
               
$bestAnswerSeen    = FALSE;

                foreach(
$items as $item )
                {
                    if(
$item->topic_answered_pid )
                    {
                       
/* Have we seen a best answer yet? If not, then we're ok. */
                       
if( $bestAnswerSeen === FALSE )
                        {
                           
/* This topic had no best answer flag set, so set it now */
                           
$this->topic_answered_pid = $item->topic_answered_pid;
                           
$this->save();

                           
$bestAnswerSeen = TRUE;
                            continue;
                        }

                       
/* If we have though, reset any others */
                       
try
                        {
                           
$post = \IPS\forums\Topic\Post::load( $item->topic_answered_pid );
                           
$post->post_bwoptions['best_answer'] = FALSE;
                           
$post->save();
                        }
                        catch( \
OutOfRangeException $e ){}
                    }
                }
            }
        }

        return
parent::mergeIn( $items, $keepLinks );
    }

   
/**
     * 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 )
    {
       
/* Hide any moved topic links that point to this topic */
       
foreach( \IPS\Db::i()->select( '*', 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) ) as $link )
        {
            \
IPS\forums\Topic::constructFromData( $link )->hide( $member, $reason );
        }

        return
parent::hide( $member, $reason );
    }

   
/**
     * 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 )
    {
       
/* Unhide any moved topic links that point to this topic */
       
foreach( \IPS\Db::i()->select( '*', 'forums_topics', array( "state=? AND moved_to LIKE CONCAT( ?, '%' )", 'link', $this->tid . '&' ) ) as $link )
        {
            \
IPS\forums\Topic::constructFromData( $link )->unhide( $member );
        }

        return
parent::unhide( $member );
    }
   
   
/**
     * 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
parent::canPromoteToSocialMedia( $member ) and $this->container()->can_view_others;
    }

   
/* !Saved Actions */
   
    /**
     * Get available saved actions for this topic
     *
     * @param    \IPS\Member|NULL    $member    The member (NULL for currently logged in)
     * @return    array
     */
   
public function availableSavedActions( \IPS\Member $member = NULL )
    {
        return \
IPS\forums\SavedAction::actions( $this->container(), $member );
    }
       
   
/**
     * 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                $immedaitely Delete Immediately
     * @return    void
     * @throws    \OutOfRangeException|\InvalidArgumentException|\RuntimeException
     */
   
public function modAction( $action, \IPS\Member $member = NULL, $reason = NULL, $immediately = FALSE )
    {
        if (
mb_substr( $action, 0, 12 ) === 'savedAction-' )
        {
           
$action = \IPS\forums\SavedAction::load( intval( mb_substr( $action, 12 ) ) );
           
$action->runOn( $this );
           
           
/* Log */
           
\IPS\Session::i()->modLog( 'modlog__saved_action', array( 'forums_mmod_' . $action->mm_id => TRUE, $this->url()->__toString() => FALSE, $this->mapped( 'title' ) => FALSE ), $this );
        }
       
       
call_user_func_array( 'parent::modAction', func_get_args() );
       
       
/* Prevent topics with an open time re-opening again after being locked */
       
if ( $action == 'lock' )
        {
           
$this->topic_open_time = 0;
           
$this->save();
        }

       
/* And prevent it from relocking if we are unlocking */
       
if( $action == 'unlock' )
        {
           
$this->topic_close_time = 0;
           
$this->save();
        }
    }
   
   
/* !Tags */
   
    /**
     * Can tag?
     *
     * @param    \IPS\Member|NULL        $member        The member to check for (NULL for currently logged in member)
     * @param    \IPS\Node\Model|NULL    $container    The container to check if tags can be used in, if applicable
     * @return    bool
     */
   
public static function canTag( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
    {
        return
parent::canTag( $member, $container ) and ( $container === NULL or !$container->forums_bitoptions['bw_disable_tagging'] );
    }
   
   
/**
     * Can use prefixes?
     *
     * @param    \IPS\Member|NULL        $member        The member to check for (NULL for currently logged in member)
     * @param    \IPS\Node\Model|NULL    $container    The container to check if tags can be used in, if applicable
     * @return    bool
     */
   
public static function canPrefix( \IPS\Member $member = NULL, \IPS\Node\Model $container = NULL )
    {
        return
parent::canPrefix( $member, $container ) and ( $container === NULL or !$container->forums_bitoptions['bw_disable_prefixes'] );
    }
   
   
/**
     * Defined Tags
     *
     * @param    \IPS\Node\Model|NULL    $container    The container to check if tags can be used in, if applicable
     * @return    array
     */
   
public static function definedTags( \IPS\Node\Model $container = NULL )
    {
        if (
$container and $container->tag_predefined )
        {
            return
explode( ',', $container->tag_predefined );
        }
       
        return
parent::definedTags( $container );
    }
   
   
/* !Questions & Answers */
   
    /**
     * Is this topic a question?
     *
     * @return    bool
     */
   
public function isQuestion()
    {
        return
$this->container()->forums_bitoptions['bw_enable_answers'];
    }
   
   
/**
     * Can user set the best answer?
     *
     * @param    \IPS\Member    $member    The member (null for currently logged in member)
     * @return    bool
     */
   
public function canSetBestAnswer( \IPS\Member $member = NULL )
    {
       
/* Archived topics cannot be modified */
       
if ( $this->isArchived() )
        {
            return
FALSE;
        }
       
       
/* If we asked this question, we can set the best answer */
       
$member = $member ?: \IPS\Member::loggedIn();
        if (
$member == $this->author() and $this->container()->forums_bitoptions['bw_enable_answers_member'] )
        {
            return
TRUE;
        }

       
/* Or if we're a moderator */
       
if
        (
           
$member->modPermission( 'can_set_best_answer' )
            and
            (
                (
$member->modPermission( \IPS\forums\Forum::$modPerm ) === TRUE or $member->modPermission( \IPS\forums\Forum::$modPerm ) === -1 )
                or
                (
                   
is_array( $member->modPermission( \IPS\forums\Forum::$modPerm ) )
                    and
                   
in_array( $this->container()->_id, $member->modPermission( \IPS\forums\Forum::$modPerm ) )
                )
            )
        ) {
            return
TRUE;
        }
       
       
/* Otherwise no */
       
return FALSE;
    }
   
   
/**
     * @brief    Answer Votes
     */
   
protected $answerVotes = array();
   
   
/**
     * Answer Votes
     *
     * @param    \IPS\Member    $member    The member
     * @return    array
     */
   
public function answerVotes( \IPS\Member $member )
    {
        if ( !isset(
$this->answerVotes[ $member->member_id ] ) )
        {
           
$this->answerVotes[ $member->member_id ] = iterator_to_array(
                \
IPS\Db::i()->select( 'post,rating', 'forums_answer_ratings', array( 'topic=? AND member=?', $this->tid, $member->member_id ) )
                ->
setKeyField( 'post' )
                ->
setValueField( 'rating' )
            );
        }
       
        return
$this->answerVotes[ $member->member_id ];
    }
   
   
/**
     * Get Best Answer
     *
     * @return    \IPS\forums\Topic\Post|NULL
     */
   
public function bestAnswer()
    {
        if (
$this->topic_answered_pid )
        {
            try
            {
                return \
IPS\forums\Topic\Post::load( $this->topic_answered_pid );
            }
            catch ( \
OutOfRangeException $e ){}
        }
        return
NULL;
    }
   
   
/**
     * Can the user rate answers?
     *
     * @param    int                    $rating        1 for positive, -1 for negative, 0 for either
     * @param    \IPS\Member|NULL    $member        The member (NULL for currently logged in member)
     * @return    bool
     * @throws    \InvalidArgumentException
     */
   
public function canVote( $rating=0, $member=NULL )
    {
       
/* Is $rating valid */
       
if ( !in_array( $rating, array( -1, 0, 1 ) ) )
        {
            throw new \
InvalidArgumentException;
        }
               
       
/* Guests can't vote */
       
$member = $member ?: \IPS\Member::loggedIn();
        if ( !
$member->member_id )
        {
            return
FALSE;
        }
       
       
/* Can't vote your own answers */
       
if ( $member == $this->author() )
        {
            return
FALSE;
        }
       
       
/* Check the forum settings */
       
if ( $this->container()->qa_rate_questions !== NULL and $this->container()->qa_rate_questions != '*' and !$member->inGroup( explode( ',', $this->container()->qa_rate_questions ) ) )
        {
            return
FALSE;
        }
       
       
/* Have we already voted? */
       
if ( $rating !== 0 or !\IPS\Settings::i()->forums_questions_downvote )
        {
           
$ratings = $this->votes();
            if ( isset(
$ratings[ $member->member_id ] ) and $ratings[ $member->member_id ] === $rating )
            {
                return
FALSE;
            }
        }
       
       
/* Downvoting disabled? */
       
if ( $rating === -1 and !\IPS\Settings::i()->forums_questions_downvote and ( !isset( $ratings[ $member->member_id ] ) or $ratings[ $member->member_id ] != 1 ) )
        {
            return
FALSE;
        }
       
        return
TRUE;
    }
   
   
/**
     * @brief    Votes
     */
   
protected $votes = NULL;
   
   
/**
     * Votes
     *
     * @return    array
     */
   
public function votes()
    {
        if (
$this->votes === NULL )
        {
           
$this->votes = iterator_to_array(
                \
IPS\Db::i()->select( 'member,rating', 'forums_question_ratings', array( 'topic=?', $this->tid ) )
                ->
setKeyField( 'member' )
                ->
setValueField( 'rating' )
            );
        }
       
        return
$this->votes;
    }
   
   
/**
     * Clear Votes Cache
     *
     * @return    void
     * @note    This is necessary so that when voting on a question or answer, the cached votes ($votes and $answerVotes) are reloaded properly.
     */
   
public function clearVotes()
    {
       
$this->votes        = NULL;
       
$this->answerVotes    = array();
    }
   
   
/**
     * [ActiveRecord]    Save
     *
     * @return    void
     */
   
public function save()
    {
       
parent::save();
       
$this->clearVotes();
    }
   
   
/**
     * 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 )
    {
       
$bitOptions = ( $containerData['forums_bitoptions'] instanceof \IPS\Patterns\Bitwise ) ? $containerData['forums_bitoptions'] : new \IPS\Patterns\Bitwise( array( 'forums_bitoptions' => $containerData['forums_bitoptions'] ), \IPS\forums\Forum::$bitOptions['forums_bitoptions'] );
       
        if (
$bitOptions['bw_enable_answers'] )
        {
           
$lang = $lang ?: \IPS\Member::loggedIn()->language();
            return
$lang->addToStack( '__indefart_question', FALSE );
        }
        else
        {
            return
parent::_indefiniteArticle( $containerData, $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() )
    {
       
$bitOptions = ( $containerData['forums_bitoptions'] instanceof \IPS\Patterns\Bitwise ) ? $containerData['forums_bitoptions'] : new \IPS\Patterns\Bitwise( array( 'forums_bitoptions' => $containerData['forums_bitoptions'] ), \IPS\forums\Forum::$bitOptions['forums_bitoptions'] );
       
        if (
$bitOptions['bw_enable_answers'] )
        {
           
$lang = $lang ?: \IPS\Member::loggedIn()->language();
            return
$lang->addToStack( '__defart_question', FALSE, $options );
        }
        else
        {
            return
parent::_definiteArticle( $containerData, $lang, $options );
        }
    }
   
   
/* !Sitemap */
   
    /**
     * WHERE clause for getting items for sitemap (permissions are already accounted for)
     *
     * @return    array
     */
   
public static function sitemapWhere()
    {
        return array( array(
'forums_forums.ipseo_priority<>?', 0 ) );
    }
   
   
/**
     * Sitemap Priority
     *
     * @return    int|NULL    NULL to use default
     */
   
public function sitemapPriority()
    {
       
$priority = $this->container()->ipseo_priority;
        if (
$priority === NULL or $priority == -1 )
        {
            return
NULL;
        }
        return
$priority;
    }
   
   
/* !Archiving */
   
    /**
     * Is archived?
     *
     * @return    bool
     */
   
public function isArchived()
    {
        return
in_array( $this->topic_archive_status, array( static::ARCHIVE_DONE, static::ARCHIVE_WORKING, static::ARCHIVE_RESTORE ) );
    }
   
   
/**
     * Can unarchive?
     *
     * @param    \IPS\Member\NULL    $member    The member (NULL for currently logged in member)
     * @return    bool
     */
   
public function canUnarchive( $member=NULL )
    {
        if (
$this->isArchived() and $this->topic_archive_status !== static::ARCHIVE_RESTORE )
        {
           
$member = $member ?: \IPS\Member::loggedIn();
            return
$member->hasAcpRestriction( 'forums', 'forums', 'archive_manage' );
        }
        return
FALSE;
    }

   
/**
     * Should this topic be archived again?
     *
     * @param \IPS\Member\NULL $member    The member (NULL for currently logged in member)
     * @return bool
     */
   
public function canRemoveArchiveExcludeFlag( $member=NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();

        if (
$member->hasAcpRestriction( 'forums', 'forums', 'archive_manage' ) AND $this->topic_archive_status == static::ARCHIVE_EXCLUDE )
        {
            return
TRUE;
        }
        return
FALSE;
    }
   
   
/**
     * Unarchive confirm message
     *
     * @return    string
     */
   
public function unarchiveBlurb()
    {
       
$taskData = \IPS\Db::i()->select( '*', 'core_tasks', array( '`key`=? AND app=?', 'archive', 'forums' ) )->first();
       
       
$time = \IPS\DateTime::ts( $taskData['next_run'] );
       
$postsToBeUnarchived = \IPS\Db::i()->select( 'SUM(posts) + COUNT(*)', 'forums_topics', array( 'topic_archive_status=?', static::ARCHIVE_RESTORE ) )->first();

        if (
$postsToBeUnarchived AND $postsToBeUnarchived > \IPS\forums\tasks\unarchive::PROCESS_PER_BATCH )
        {
           
$total = $postsToBeUnarchived / \IPS\forums\tasks\unarchive::PROCESS_PER_BATCH;
           
$interval = new \DateInterval( $taskData['frequency'] );
            foreach (
range( 1, $total ) as $i )
            {
               
$time->add( $interval );
            }
        }
       
        return \
IPS\Member::loggedIn()->language()->addToStack( 'unarchive_confirm', FALSE, array( 'pluralize' => array( ceil( ( $time->getTimestamp() - time() ) / 60 ) ) ) );
    }
   
   
/**
     * Can comment?
     *
     * @param    \IPS\Member\NULL    $member    The member (NULL for currently logged in member)
     * @return    bool
     */
   
public function canComment( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canComment( $member );
    }
   
   
/**
     * Can edit?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canEdit( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canEdit( $member );
    }
   
   
/**
     * Can feature?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canFeature( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canFeature( $member );
    }
   
   
/**
     * Can unfeature?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canUnfeature( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canUnfeature( $member );
    }

   
/**
     * Can lock?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canLock( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canLock( $member );
    }
   
   
/**
     * Can unlock?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canUnlock( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canUnlock( $member );
    }
   
   
/**
     * Can hide?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canHide( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canHide( $member );
    }
   
   
/**
     * Can unhide?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canUnhide( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canUnhide( $member );
    }
   
   
/**
     * Can move?
     *
     * @param    \IPS\Member|NULL    $member    The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canMove( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canMove( $member );
    }
   
   
/**
     * Can merge?
     *
     * @param    \IPS\Member|NULL    $member The member to check for (NULL for currently logged in member)
     * @return    bool
     */
   
public function canMerge( $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canMerge( $member );
    }
   
   
/**
     * Comment Multimod Actions
     *
     * @param    \IPS\Member|NULL    $member The member to check for (NULL for currently logged in member)
     * @return    array
     */
   
public function commentMultimodActions( \IPS\Member $member=NULL )
    {
        if (
$this->isArchived() )
        {
            return array();
        }
       
        return
parent::commentMultimodActions( $member );
    }
   
   
/**
     * Can Feature a Comment
     *
     * @param    \IPS\Member|NULL    The member, or NULL for currently logged in
     * @return    bool
     * @note This is a wrapper for the extension so content items can extend and apply their own logic
     */
   
public function canFeatureComment( \IPS\Member $member = NULL )
    {
        if (
$this->isArchived() OR $this->isQuestion() )
        {
            return
FALSE;
        }
       
        return
parent::canFeatureComment( $member );
    }
   
   
/**
     * Can perform an action on a message
     *
     * @param    string                The action
     * @param    \IPS\Member|NULL    The member, or NULL for currently logged in
     * @return    bool
     * @note This is a wrapper for the extension so content items can extend and apply their own logic
     */
   
public function canOnMessage( $action, \IPS\Member $member = NULL )
    {
        if (
$this->isArchived() )
        {
            return
FALSE;
        }
       
        return
parent::canOnMessage( $action, $member );
    }
   
   
/**
     * Get comments
     *
     * @param    int|NULL            $limit                    The number to get (NULL to use static::getCommentsPerPage())
     * @param    int|NULL            $offset                    The number to start at (NULL to examine \IPS\Request::i()->page)
     * @param    string                $order                    The column to order by
     * @param    string                $orderDirection            "asc" or "desc"
     * @param    \IPS\Member|NULL    $member                    If specified, will only get comments by that member
     * @param    bool|NULL            $includeHiddenComments    Include hidden comments or not? NULL to base of currently logged in member's permissions
     * @param    \IPS\DateTime|NULL    $cutoff                    If an \IPS\DateTime object is provided, only comments posted AFTER that date will be included
     * @param    mixed                $extraWhereClause    Additional where clause(s) (see \IPS\Db::build for details)
     * @param    bool|NULL            $bypassCache            Used in cases where comments may have already been loaded i.e. splitting comments on an item.
     * @param    bool                $includeDeleted            Include Deleted Content
     * @param    bool|NULL            $canViewWarn            TRUE to include Warning information, NULL to determine automatically based on moderator permissions.
     * @return    array|NULL|\IPS\Content\Comment    If $limit is 1, will return \IPS\Content\Comment or NULL for no results. For any other number, will return an array.
     */
   
public function comments( $limit=NULL, $offset=NULL, $order='date', $orderDirection='asc', $member=NULL, $includeHiddenComments=NULL, $cutoff=NULL, $extraWhereClause=NULL, $bypassCache=FALSE, $includeDeleted=FALSE, $canViewWarn=NULL )
    {
        static
$comments    = array();
       
$idField            = static::$databaseColumnId;
       
$_hash                = md5( $this->$idField . json_encode( func_get_args() ) );

        if( !
$bypassCache and isset( $comments[ $_hash ] ) )
        {
            return
$comments[ $_hash ];
        }
       
       
$includeWarnings    = $canViewWarn;
       
$commentClass        = NULL;

        if (
$this->isArchived() )
        {
           
/* We need to set $commentClass to the archive class, otherwise the includeHidden checks in _comments fail, as they verify $class == static::$commentClass */
           
$class            = static::$archiveClass;
           
$commentClass    = static::$commentClass;

            static::
$commentClass = $class;

           
$includeWarnings = FALSE;

            if(
$extraWhereClause !== NULL )
            {
                if(
is_array( $extraWhereClause ) )
                {
                    foreach(
$extraWhereClause as $k => $v )
                    {
                       
$extraWhereClause[ $k ]    = preg_replace( "/^author_id /", "archive_author_id ", $v );
                    }
                }
                else
                {
                   
$extraWhereClause    = preg_replace( "/^author_id /", "archive_author_id ", $extraWhereClause );
                }
            }
        }
        else
        {
           
$class = static::$commentClass;
        }
               
       
$comments[ $_hash ]    = $this->_comments( $class, $limit ?: static::getCommentsPerPage(), $offset, ( isset( $class::$databaseColumnMap[ $order ] ) ? ( $class::$databasePrefix . $class::$databaseColumnMap[ $order ] ) : $order ) . ' ' . $orderDirection, $member, $includeHiddenComments, $cutoff, $includeWarnings, $extraWhereClause, $includeDeleted );

       
/* Restore comment class now */
       
if( $commentClass )
        {
            static::
$commentClass    = $commentClass;
        }
        return
$comments[ $_hash ];
    }
   
   
/**
     * Resync the comments/unapproved comment counts
     *
     * @param    string    $commentClass    Override comment class to use
     * @return void
     */
   
public function resyncCommentCounts( $commentClass=NULL )
    {
        return
parent::resyncCommentCounts( $this->isArchived() ? static::$archiveClass : NULL );
    }
   
   
/**
     * 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 )
    {        
       
/* Load Member */
       
$member = $member ?: \IPS\Member::loggedIn();
       
       
/* Compatibility checks */
       
if ( in_array( $type, array( 'use_saved_actions', 'set_best_answer' ) ) )
        {
           
$containerClass = get_class( $container );
           
$title = static::$title;
            if
            (
               
$member->modPermission( $containerClass::$modPerm ) === -1
               
or
                (
                   
is_array( $member->modPermission( $containerClass::$modPerm ) )
                    and
                   
in_array( $container->_id, $member->modPermission( $containerClass::$modPerm ) )
                )
            )
            {
                return
TRUE;
            }
        }
       
        return
parent::modPermission( $type, $member, $container );
    }

   
/**
     * Mark as read
     *
     * @param    \IPS\Member|NULL    $member    The member (NULL for currently logged in member)
     * @param    int|NULL            $time    The timestamp to set (or NULL for current time)
     * @param    mixed                $extraContainerWhere    Additional where clause(s) (see \IPS\Db::build for details)
     * @return    void
     */
   
public function markRead( \IPS\Member $member = NULL, $time = NULL, $extraContainerWhere = NULL )
    {
       
$member = $member ?: \IPS\Member::loggedIn();
       
$time    = $time ?: time();

        if ( !
$this->container()->can_view_others and !$member->modPermission( 'can_read_all_topics' ) )
        {
           
$extraContainerWhere = array( 'starter_id = ?', $member->member_id );
        }

       
parent::markRead( $member, $time, $extraContainerWhere );
    }

   
/**
     * Returns the meta description
     *
     * @param    string|NULL    $return    Specific description to use (useful for paginated displays to prevent having to run extra queries)
     * @return    string
     * @throws    \BadMethodCallException
     */
   
public function metaDescription( $return = NULL )
    {
       
/* We have to store and restore this for the topic controller */
       
if( isset( $_SESSION['_findComment'] ) )
        {
           
$_findComment = $_SESSION['_findComment'];
        }

       
$metaDescription = parent::metaDescription( $return );

        if( isset(
$_findComment ) )
        {
           
$_SESSION['_findComment'] = $_findComment;
        }

        return
$metaDescription;
    }
   
   
/**
     * Get output for API
     *
     * @param    \IPS\Member|NULL    $authorizedMember    The member making the API request or NULL for API Key / client_credentials
     * @return    array
     * @apiresponse    int                        id                ID number
     * @apiresponse    string                    title            Title
     * @apiresponse    \IPS\forums\Forum        forum            Forum
     * @apiresponse    int                        posts            Number of posts
     * @apiresponse    int                        views            Number of views
     * @apiresponse    string                    prefix            The prefix tag, if there is one
     * @apiresponse    [string]                tags            The tags
     * @apiresponse    \IPS\forums\Topic\Post    firstPost        The first post in the topic
     * @apiresponse    \IPS\forums\Topic\Post    lastPost        The last post in the topic
     * @apiresponse    \IPS\forums\Topic\Post    bestAnswer        The best answer, if this is a question and there is one
     * @apiresponse    bool                    locked            Topic is locked
     * @apiresponse    bool                    hidden            Topic is hidden
     * @apiresponse    bool                    pinned            Topic is pinned
     * @apiresponse    bool                    featured        Topic is featured
     * @apiresponse    bool                    archived        Topic is archived
     * @apiresponse    \IPS\Poll                poll            Poll data, if there is one
     * @apiresponse    string                    url                URL
     * @apiresponse    float                    rating            Average Rating
     */
   
public function apiOutput( \IPS\Member $authorizedMember = NULL )
    {
       
$firstPost = $this->comments( 1, 0, 'date', 'asc' );
       
$lastPost = $this->comments( 1, 0, 'date', 'desc' );
       
$bestAnswer = $this->bestAnswer();
        return array(
           
'id'            => $this->tid,
           
'title'            => $this->title,
           
'forum'            => $this->container()->apiOutput( $authorizedMember ),
           
'posts'            => $this->posts,
           
'views'            => $this->views,
           
'prefix'        => $this->prefix(),
           
'tags'            => $this->tags(),
           
'firstPost'        => $firstPost ? $firstPost->apiOutput( $authorizedMember ) : null,
           
'lastPost'        => $lastPost ? $lastPost->apiOutput( $authorizedMember ) : null,
           
'bestAnswer'    => $bestAnswer ? $bestAnswer->apiOutput( $authorizedMember ) : null,
           
'locked'        => (bool) $this->locked(),
           
'hidden'        => (bool) $this->hidden(),
           
'pinned'        => (bool) $this->mapped('pinned'),
           
'featured'        => (bool) $this->mapped('featured'),
           
'archived'        => (bool) $this->isArchived(),
           
'poll'            => $this->poll_state ? \IPS\Poll::load( $this->poll_state )->apiOutput( $authorizedMember ) : null,
           
'url'            => (string) $this->url(),
           
'rating'        => $this->averageRating(),
        );
    }

   
/**
     * Returns the content
     *
     * @return    string
     * @throws    \BadMethodCallException
     * @note    This is overridden for performance reasons - selecting a post by a PID is more efficient than select * from posts order by date desc limit 1
     */
   
public function content()
    {
       
$firstComment = $this->firstComment();
        return
$firstComment ? $firstComment->content() : '';
    }
   
   
/**
     * Supported Meta Data Types
     *
     * @return    array|NULL
     */
   
public static function supportedMetaDataTypes()
    {
        return array(
'core_FeaturedComments', 'core_ContentMessages' );
    }

   
/**
     * Get content for embed
     *
     * @param    array    $params    Additional parameters to add to URL
     * @return    string
     */
   
public function embedContent( $params )
    {
        \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'embed.css', 'forums', 'front' ) );
        return \
IPS\Theme::i()->getTemplate( 'global', 'forums' )->embedTopic( $this, $this->url()->setQueryString( $params ) );
    }
}