Files
IPB/applications/forums/modules/front/forums/topic.php
T
2025-12-19 16:00:35 -08:00

1289 lines
38 KiB
PHP

<?php
/**
* @brief Topic View
* @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 08 Jan 2014
*/
namespace IPS\forums\modules\front\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 View
*/
class _topic extends \IPS\Content\Controller
{
/**
* [Content\Controller] Class
*/
protected static $contentModel = 'IPS\forums\Topic';
/**
* Execute
*
* @return void
*/
public function execute()
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js('front_topic.js', 'forums' ) );
parent::execute();
}
/**
* View Topic
*
* @return \IPS\Content\Item
* @throws \Exception
*/
protected function manage()
{
/* Load topic */
$topic = parent::manage();
\IPS\Member::loggedIn()->language()->words['submit_comment'] = \IPS\Member::loggedIn()->language()->addToStack( 'submit_reply', FALSE );
/* If it failed, it might be because we want a password */
if ( $topic === NULL )
{
$forum = NULL;
try
{
$topic = \IPS\forums\Topic::load( \IPS\Request::i()->id );
$forum = $topic->container();
if ( $forum->can('view') and !$forum->loggedInMemberHasPasswordAccess() )
{
\IPS\Output::i()->redirect( $forum->url()->setQueryString( 'topic', \IPS\Request::i()->id ) );
}
if ( !$topic->canView() )
{
if ( $topic instanceof \IPS\Content\Hideable and $topic->hidden() )
{
/* If the item is hidden we don't want to show the custom no permission error as the conditions may not apply */
\IPS\Output::i()->error( 'node_error', '2F173/O', 404, '' );
}
else
{
\IPS\Output::i()->error( $forum ? $forum->errorMessage() : 'node_error_no_perm', '2F173/H', 403, '' );
}
}
}
catch ( \OutOfRangeException $e )
{
/* Nope, just a generic no access error */
\IPS\Output::i()->error( 'node_error', '2F173/1', 404, '' );
}
}
$topic->container()->clubCheckRules();
/* If there's only one forum and we're not in a club, we actually don't want the nav */
if ( !$topic->container()->club() AND ( \IPS\forums\Forum::theOnlyForum() or \IPS\forums\Forum::isSimpleView() ) )
{
$topicBreadcrumb = array_pop( \IPS\Output::i()->breadcrumb );
\IPS\Output::i()->breadcrumb = isset( \IPS\Output::i()->breadcrumb['module'] ) ? array( 'module' => \IPS\Output::i()->breadcrumb['module'] ) : array();
\IPS\Output::i()->breadcrumb[] = $topicBreadcrumb;
}
/* Legacy findpost redirect */
if ( \IPS\Request::i()->findpost )
{
\IPS\Output::i()->redirect( $topic->url()->setQueryString( array( 'do' => 'findComment', 'comment' => \IPS\Request::i()->findpost ) ), NULL, 301 );
}
elseif ( \IPS\Request::i()->p )
{
\IPS\Output::i()->redirect( $topic->url()->setQueryString( array( 'do' => 'findComment', 'comment' => \IPS\Request::i()->p ) ), NULL, 301 );
}
elseif ( \IPS\Request::i()->pid )
{
\IPS\Output::i()->redirect( $topic->url()->setQueryString( array( 'do' => 'findComment', 'comment' => \IPS\Request::i()->pid ) ), NULL, 301 );
}
if ( \IPS\Request::i()->view )
{
$this->_doViewCheck();
}
/* If the topic is locked and scheduled to unlock already, or vice versa, do that */
if( $topic->locked() && $topic->topic_open_time && $topic->topic_open_time < time() )
{
$topic->state = 'open';
$topic->save();
}
elseif( !$topic->locked() && $topic->topic_close_time && $topic->topic_close_time < time() )
{
$topic->state = 'closed';
$topic->save();
}
/* If this is an AJAX request fetch the comment form now. The HTML will be cached so calling here and then again in the template has no overhead
and this is necessary if you entered into a topic with &queued_posts=1, approve the posts, then try to reply. Otherwise, clicking into the
editor produces an error when the getUploader=1 call occurs, and submitting a reply results in an error. */
if ( \IPS\Request::i()->isAjax() and ( !isset( \IPS\Request::i()->preview ) OR !\IPS\Request::i()->preview ) )
{
$topic->commentForm();
}
/* AJAX hover preview? */
if ( \IPS\Request::i()->isAjax() and \IPS\Request::i()->preview )
{
$postClass = '\IPS\forums\Topic\Post';
if( $topic->isArchived() )
{
$postClass = '\IPS\forums\Topic\ArchivedPost';
}
/* If this topic was moved or merged, load that up in case someone loads the preview after that happens but before they reload the page */
$previewTopic = $topic;
if ( \in_array( $topic->state, array( 'merged', 'link' ) ) )
{
$movedTo = explode( '&', $topic->moved_to );
try
{
$previewTopic = \IPS\forums\Topic::loadAndCheckPerms( $movedTo[0] );
}
catch( \OutOfRangeException $e )
{
/* I can't help you */
\IPS\Output::i()->error( 'node_error', '2F173/Q', 404, '' );
return;
}
}
$firstPost = $postClass::load( $previewTopic->topic_firstpost );
$topicOverview = array( 'firstPost' => array( $previewTopic->isQuestion() ? 'question_mainTab' : 'first_post', $firstPost ) );
if ( $previewTopic->posts > 1 )
{
$latestPost = $previewTopic->comments( 1, 0, 'date', 'DESC' );
$topicOverview['latestPost'] = array( $previewTopic->isQuestion() ? 'latest_answer' : 'latest_post', $latestPost );
$timeLastRead = $previewTopic->timeLastRead();
if ( $timeLastRead instanceof \IPS\DateTime AND $previewTopic->unread() !== 0 )
{
$firstUnread = $previewTopic->comments( 1, NULL, 'date', 'asc', NULL, NULL, $timeLastRead );
if( $firstUnread instanceof \IPS\forums\Topic\Post AND $firstUnread->date !== $latestPost->date AND $firstUnread->date !== $firstPost->date )
{
$topicOverview['firstUnread'] = array( 'first_unread_post_hover', $previewTopic->comments( 1, NULL, 'date', 'asc', NULL, NULL, $timeLastRead ) );
}
}
}
if ( $previewTopic->isQuestion() and $previewTopic->topic_answered_pid )
{
$topicOverview['bestAnswer'] = array( 'best_answer_post', \IPS\forums\Topic\Post::load( $previewTopic->topic_answered_pid ) );
}
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'forums' )->topicHover( $previewTopic, $topicOverview ) );
return;
}
$topic->container()->setTheme();
/* Watch for votes */
if ( $poll = $topic->getPoll() )
{
$poll->attach( $topic );
}
/* How are we sorting posts? */
$question = NULL;
$offset = NULL;
$order = 'date';
$orderDirection = 'asc';
$where = NULL;
if( \IPS\forums\Topic::modPermission( 'unhide', NULL, $topic->container() ) AND \IPS\Request::i()->queued_posts )
{
if ( $topic->isArchived() )
{
$where = 'archive_queued=1';
}
else
{
$where = 'queued=1';
}
$queuedPagesCount = ceil( \IPS\Db::i()->select( 'COUNT(*)', 'forums_posts', array( 'topic_id=? AND queued=1', $topic->id ) )->first() / $topic->getCommentsPerPage() );
$pagination = ( $queuedPagesCount > 1 ) ? $topic->commentPagination( array( 'queued_posts', 'sortby' ), 'pagination', $queuedPagesCount ) : NULL;
if ( $topic->isQuestion() )
{
$question = $topic->comments( 1, 0 );
}
}
else
{
if ( $topic->isQuestion() )
{
$question = $topic->comments( 1, 0 );
try
{
$question->warning = \IPS\core\Warnings\Warning::constructFromData( \IPS\Db::i()->select( '*', 'core_members_warn_logs', array( array( 'wl_content_app=? AND wl_content_module=? AND wl_content_id1=? AND wl_content_id2=?', 'forums', 'forums-comment', $question->topic_id, $question->pid ) ) )->first() );
}
catch ( \UnderflowException $e ) { }
$page = ( isset( \IPS\Request::i()->page ) ) ? \intval( \IPS\Request::i()->page ) : 1;
if( $page < 1 )
{
$page = 1;
}
$offset = ( ( $page - 1 ) * $topic::getCommentsPerPage() ) + 1;
if ( ( !isset( \IPS\Request::i()->sortby ) or \IPS\Request::i()->sortby != 'date' ) )
{
if ( $topic->isArchived() )
{
$order = "archive_is_first desc, archive_bwoptions";
$orderDirection = 'desc';
}
else
{
$order = "new_topic DESC, post_bwoptions DESC, post_field_int DESC, post_date";
$orderDirection = 'ASC';
}
}
}
$pagination = ( $topic->commentPageCount() > 1 ) ? $topic->commentPagination( array( 'sortby' ) ) : NULL;
}
$comments = $topic->comments( NULL, $offset, $order, $orderDirection, NULL, NULL, NULL, $where, FALSE, ( isset( \IPS\Request::i()->showDeleted ) ) );
$current = current( $comments );
reset( $comments );
if( ( !\count( $comments ) AND !$topic->isQuestion() ) OR ( $topic->isQuestion() AND empty( $question ) ) )
{
\IPS\Output::i()->error( 'no_posts_returned', '2F173/L', 404, '' );
}
/* Mark read */
if( !$topic->isLastPage() AND $topic->unread() !== 0 )
{
$maxTime = 0;
foreach( $comments as $comment )
{
$maxTime = ( $comment->mapped('date') > $maxTime ) ? $comment->mapped('date') : $maxTime;
}
if( $topic->timeLastRead() === NULL OR $maxTime > $topic->timeLastRead()->getTimestamp() )
{
$topic->markRead( NULL, $maxTime );
}
}
elseif( $topic->isLastPage() )
{
/* See if the last comment is hidden or pending approval. If so, force the topic read because it won't be done so automatically. */
$lastComment = end( $comments );
if( $lastComment and $lastComment->hidden() !== 0 )
{
$topic->markRead( NULL, NULL, NULL, TRUE );
}
reset( $comments );
}
/* A convenient hook point to do any further set up */
$this->finishManage( $topic );
$votes = array();
$topicVotes = array();
/* Get post ratings for this user */
if ( $topic->isQuestion() && \IPS\Member::loggedIn()->member_id )
{
$votes = $topic->answerVotes( \IPS\Member::loggedIn() );
$topicVotes = $topic->votes();
}
if ( $topic->isQuestion() )
{
\IPS\Member::loggedIn()->language()->words[ 'topic__comment_placeholder' ] = \IPS\Member::loggedIn()->language()->addToStack( 'question__comment_placeholder', FALSE );
}
/* Online User Location */
\IPS\Session::i()->setLocation( $topic->url(), ( $topic->container()->password or !$topic->container()->can_view_others or $topic->container()->min_posts_view ) ? 0 : $topic->onlineListPermissions(), 'loc_forums_viewing_topic', array( $topic->title => FALSE ) );
/* Next unread */
try
{
$nextUnread = $topic->containerHasUnread();
}
catch( \Exception $e )
{
$nextUnread = NULL;
}
/* Sidebar? */
if ( $topic->showSummaryOnDesktop() == 'sidebar' )
{
\IPS\Output::i()->sidebar['contextual'] = \IPS\Theme::i()->getTemplate( 'topics' )->activity( $topic, 'sidebar' );
}
/* Add Json-LD */
$isQuestion = ( $topic->isQuestion() or $topic->isSolved() );
if ( $isQuestion )
{
\IPS\Output::i()->jsonLd['topic'] = array(
'@context' => "http://schema.org",
'@type' => 'QAPage',
'@id' => (string) $topic->url(),
'url' => (string) $topic->url(),
'mainEntity' => [
'@type' => "Question",
'name' => $topic->title,
'text' => $topic->comments( 1, 0 )->truncated( TRUE, NULL ),
'answerCount' => $topic->posts ? $topic->posts - 1 : 0,
'dateCreated' => \IPS\DateTime::ts( $topic->start_date )->format( \IPS\DateTime::ISO8601 ),
'author' => [
'@type' => 'Person',
'name' => $topic->author()->name
]
]
);
if( $topic->topic_answered_pid )
{
try
{
$postClass = '\IPS\forums\Topic\Post';
if( $topic->isArchived() )
{
$postClass = '\IPS\forums\Topic\ArchivedPost';
}
$answer = $postClass::load( $topic->topic_answered_pid );
/* Set up our column names */
$authorIdColumn = $answer::$databaseColumnMap['author'];
$dateColumn = $answer::$databaseColumnMap['date'];
if ( $truncatedAnswer = $answer->truncated( TRUE, NULL ) )
{
\IPS\Output::i()->jsonLd['topic']['mainEntity']['acceptedAnswer'] = array(
'@type' => 'Answer',
'text' => $truncatedAnswer,
'url' => (string) $answer->url(),
'dateCreated' => \IPS\DateTime::ts( $answer->$dateColumn )->format( \IPS\DateTime::ISO8601 ),
'upvoteCount' => ( $topic->isArchived() ) ? $answer->field_int : $answer->post_field_int,
'author' => array(
'@type' => 'Person',
'name' => \IPS\Member::load( $answer->$authorIdColumn )->name,
'image' => \IPS\Member::load( $answer->$authorIdColumn )->get_photo( TRUE, TRUE )
),
);
if( $topic->isQuestion() )
{
\IPS\Output::i()->jsonLd['topic']['mainEntity']['acceptedAnswer']['upvoteCount'] = ( $topic->isArchived() ) ? $answer->field_int : $answer->post_field_int;
}
if( $answer->author_id )
{
\IPS\Output::i()->jsonLd['topic']['mainEntity']['acceptedAnswer']['author']['url'] = (string) \IPS\Member::load( $answer->$authorIdColumn )->url();
}
}
}
catch( \OutOfRangeException $e ){}
}
}
else
{
\IPS\Output::i()->jsonLd['topic'] = array(
'@context' => "http://schema.org",
'@type' => 'DiscussionForumPosting',
'@id' => (string) $topic->url(),
'isPartOf' => array(
'@id' => \IPS\Settings::i()->base_url . '#website'
),
'publisher' => array(
'@id' => \IPS\Settings::i()->base_url . '#organization'
),
'url' => (string) $topic->url(),
'discussionUrl' => (string) $topic->url(),
'mainEntityOfPage' => array(
'@type' => 'WebPage',
'@id' => (string) $topic->url()
),
'pageStart' => 1,
'pageEnd' => $topic->commentPageCount(),
);
}
/* Add in comments */
if( $topic->posts > 1 )
{
if ( $isQuestion )
{
\IPS\Output::i()->jsonLd['topic']['mainEntity']['suggestedAnswer'] = [];
}
else
{
\IPS\Output::i()->jsonLd['topic']['comment'] = [];
}
$i = 0;
$commentJson = [];
foreach( $comments as $comment )
{
/* Set up our column names */
$idColumn = $comment::$databaseColumnId;
$authorIdColumn = $comment::$databaseColumnMap['author'];
$dateColumn = $comment::$databaseColumnMap['date'];
// Don't include the first post as a "comment"
if( $comment->$idColumn == $topic->topic_firstpost )
{
continue;
}
// Don't include the answer as the suggested answer
if( $isQuestion and ( $comment->$idColumn === $topic->mapped('solved_comment_id') ) )
{
continue;
}
if ( $truncatedComment = $comment->truncated( TRUE, NULL ) )
{
$url = $topic->url()->setPage( 'page', \IPS\Request::i()->page );
$commentJson[$i] = array(
'@type' => $isQuestion ? 'Answer' : 'Comment',
'@id' => (string)$url->setFragment( 'comment-' . $comment->$idColumn ),
'url' => (string)$url->setFragment( 'comment-' . $comment->$idColumn ),
'author' => array(
'@type' => 'Person',
'name' => \IPS\Member::load( $comment->$authorIdColumn )->name,
'image' => \IPS\Member::load( $comment->$authorIdColumn )->get_photo( TRUE, TRUE )
),
'dateCreated' => \IPS\DateTime::ts( $comment->$dateColumn )->format( \IPS\DateTime::ISO8601 ),
'text' => $truncatedComment,
);
if ( $comment->$authorIdColumn )
{
$commentJson[$i]['author']['url'] = (string)\IPS\Member::load( $comment->$authorIdColumn )->url();
}
if( $isQuestion )
{
$commentJson[$i]['upvoteCount'] = ( $topic->isArchived() ) ? $comment->field_int : $comment->post_field_int;
}
else
{
$commentJson[$i]['upvoteCount'] = $comment->reactionCount();
}
$i++;
}
}
if ( $isQuestion )
{
\IPS\Output::i()->jsonLd['topic']['mainEntity']['suggestedAnswer'] = $commentJson;
}
else
{
\IPS\Output::i()->jsonLd['topic']['comment'] = $commentJson;
}
}
/* Do we have a real author */
if( $topic->starter_id )
{
\IPS\Output::i()->jsonLd['topic']['author']['url'] = (string) \IPS\Member::load( $topic->starter_id )->url();
\IPS\Output::i()->jsonLd['topic']['publisher']['member'] = array(
'@type' => "Person",
'name' => \IPS\Member::load( $topic->starter_id )->name,
'image' => (string) \IPS\Member::load( $topic->starter_id )->get_photo( TRUE, TRUE ),
'url' => (string) \IPS\Member::load( $topic->starter_id )->url(),
);
}
/* Enable caching for archived topics */
if( $topic->isArchived() AND !\IPS\Member::loggedIn()->member_id )
{
/* We do not want to use the \IPS\CACHE_PRIVATE_TIMEOUT constant here, as we explicitly want to cache archived topics for longer times */
$httpHeaders = array( 'Expires' => \IPS\DateTime::create()->add( new \DateInterval( 'PT12H' ) )->rfc1123() ,
'Cache-Control' => 'no-cache="Set-Cookie", max-age=' . ( 60 * 60 * 12 ) . ", s-maxage=" . ( 60 * 60 * 12 ) . ", public, stale-if-error, stale-while-revalidate" );
\IPS\Output::i()->httpHeaders += $httpHeaders;
}
\IPS\Output::i()->jsonLd['topic'] = array_merge_recursive( array(
'name' => $topic->mapped('title'),
'headline' => $topic->mapped('title'),
'text' => $topic->isQuestion() ? $question->truncated( TRUE, NULL ) : $current->truncated( TRUE, NULL ),
'dateCreated' => \IPS\DateTime::ts( $topic->start_date )->format( \IPS\DateTime::ISO8601 ),
'datePublished' => \IPS\DateTime::ts( $topic->start_date )->format( \IPS\DateTime::ISO8601 ),
'dateModified' => \IPS\DateTime::ts( $topic->last_post )->format( \IPS\DateTime::ISO8601 ),
/* Image is required, but we don't have "topic images", so we'll use topic starter's profile photo for now */
'image' => (string) \IPS\Member::load( $topic->starter_id )->get_photo( TRUE, TRUE ),
'author' => array(
'@type' => 'Person',
'name' => \IPS\Member::load( $topic->starter_id )->name,
'image' => \IPS\Member::load( $topic->starter_id )->get_photo( TRUE, TRUE )
),
'interactionStatistic' => array(
array(
'@type' => 'InteractionCounter',
'interactionType' => "http://schema.org/ViewAction",
'userInteractionCount' => $topic->views
),
array(
'@type' => 'InteractionCounter',
'interactionType' => "http://schema.org/CommentAction",
'userInteractionCount' => $topic->posts - 1 // We subtract one to account for the "first post"
),
array(
'@type' => 'InteractionCounter',
'interactionType' => "http://schema.org/FollowAction",
'userInteractionCount' => $topic->followersCount()
),
)
), \IPS\Output::i()->jsonLd['topic'] );
/* Show topic */
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'topics.css', 'forums' ) );
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'topics' )->topic( $topic, $comments, $question, $votes, $nextUnread, $pagination, $topicVotes );
return $topic;
}
/**
* Check our view method and act accordingly (redirect if appropriate)
*
* @return void
*/
protected function _doViewCheck()
{
try
{
$class = static::$contentModel;
$topic = $class::loadAndCheckPerms( \IPS\Request::i()->id );
switch( \IPS\Request::i()->view )
{
case 'getnewpost':
\IPS\Output::i()->redirect( $topic->url( 'getNewComment' ) );
break;
case 'getlastpost':
\IPS\Output::i()->redirect( $topic->url( 'getLastComment' ) );
break;
}
}
catch( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/F', 403, '' );
}
}
/**
* Edit topic
*
* @return void
*/
public function edit()
{
try
{
$class = static::$contentModel;
$topic = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$forum = $topic->container();
$forum->setTheme();
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'no_module_permission', '2F173/D', 403, 'no_module_permission_guest' );
}
if ( $forum->forums_bitoptions['bw_enable_answers'] )
{
\IPS\Member::loggedIn()->language()->words['topic_mainTab'] = \IPS\Member::loggedIn()->language()->addToStack( 'question_mainTab', FALSE );
}
// We check if the form has been submitted to prevent the user loosing their content
if ( isset( \IPS\Request::i()->form_submitted ) )
{
if ( ! $topic->couldEdit() )
{
\IPS\Output::i()->error( 'edit_no_perm_err', '2F173/E', 403, '' );
}
}
else
{
if ( ! $topic->canEdit() )
{
\IPS\Output::i()->error( 'edit_no_perm_err', '2F173/E', 403, '' );
}
}
$formElements = $class::formElements( $topic, $forum );
$hasModOptions = FALSE;
/* We used to just check against the ability to lock, however this may not be enough - a moderator could pin, for example, but not lock */
foreach( array( 'lock', 'pin', 'feature' ) AS $perm )
{
if ( $class::modPermission( $perm, NULL, $forum ) )
{
$hasModOptions = TRUE;
break;
}
}
if( $topic->canHide() )
{
$hasModOptions = TRUE;
}
$form = $topic->buildEditForm();
if ( $values = $form->values() )
{
if ( $topic->canEdit() )
{
$titleField = $topic::$databaseColumnMap['title'];
$oldTitle = $topic->$titleField;
$topic->processForm( $values );
$topic->save();
$topic->processAfterEdit( $values );
/* Moderator log */
$toLog = array( $topic::$title => FALSE, $topic->url()->__toString() => FALSE, $topic::$title => TRUE, $topic->mapped( 'title' ) => FALSE );
if ( $oldTitle != $topic->$titleField )
{
$toLog[ $oldTitle ] = false;
}
\IPS\Session::i()->modLog( 'modlog__item_edit', $toLog, $topic );
\IPS\Output::i()->redirect( $topic->url() );
}
else
{
$form->error = \IPS\Member::loggedIn()->language()->addToStack('edit_no_perm_err');
}
}
$formTemplate = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'submit', 'forums' ), 'createTopicForm' ), $forum, $hasModOptions, $topic );
$title = $forum->forums_bitoptions['bw_enable_answers'] ? 'edit_question' : 'edit_topic';
\IPS\Output::i()->sidebar['enabled'] = FALSE;
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'submit' )->createTopic( $formTemplate, $forum, $title );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( $title );
if ( !\IPS\forums\Forum::theOnlyForum() and ! \IPS\forums\Forum::isSimpleView() )
{
try
{
foreach( $forum->parents() AS $parent )
{
\IPS\Output::i()->breadcrumb[] = array( $parent->url(), $parent->_title );
}
\IPS\Output::i()->breadcrumb[] = array( $forum->url(), $forum->_title );
}
catch( \Exception $e ) {}
}
\IPS\Output::i()->breadcrumb[] = array( $topic->url(), $topic->mapped('title') );
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( $title ) );
}
/**
* Unarchive
*
* @return void
*/
public function unarchive()
{
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
if ( !$topic->canUnarchive() )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/B', 404, '' );
}
$topic->topic_archive_status = \IPS\forums\Topic::ARCHIVE_RESTORE;
$topic->save();
/* Make sure the task is enabled */
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 'unarchive' ) );
/* Log */
\IPS\Session::i()->modLog( 'modlog__unarchived_topic', array( $topic->url()->__toString() => FALSE, $topic->mapped( 'title' ) => FALSE ), $topic );
\IPS\Output::i()->redirect( $topic->url() );
}
/**
* Remove the archive exclude flag
*
* @return void
*/
public function removeArchiveExclude()
{
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
if ( !$topic->canRemoveArchiveExcludeFlag() )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/P', 404, '' );
}
$topic->topic_archive_status = \IPS\forums\Topic::ARCHIVE_NOT;
$topic->save();
/* Log */
\IPS\Session::i()->modLog( 'modlog__removed_archive_exclude_topic', array( $topic->url()->__toString() => FALSE, $topic->mapped( 'title' ) => FALSE ), $topic );
\IPS\Output::i()->redirect( $topic->url() );
}
/**
* Rate Question
*
* @return void
*/
public function rateQuestion()
{
/* CSRF Check */
\IPS\Session::i()->csrfCheck();
/* Get the question */
try
{
$question = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/8', 404, '' );
}
/* Voting up or down? */
$rating = \intval( \IPS\Request::i()->rating );
if ( $rating !== 1 and $rating !== -1 )
{
\IPS\Output::i()->error( 'form_bad_value', '2F173/A', 403, '' );
}
/* Check we can cast this vote */
if ( !$question->canVote( $rating ) )
{
\IPS\Output::i()->error( 'no_module_permission', '2F173/9', 403, '' );
}
/* If we have an existing vote, remove it first */
$ratings = $question->votes();
if ( isset( $ratings[ \IPS\Member::loggedIn()->member_id ] ) )
{
\IPS\Db::i()->delete( 'forums_question_ratings', array( 'topic=? AND `member`=?', $question->tid, \IPS\Member::loggedIn()->member_id ) );
}
/* Revoting for the same thing you already voted for should remove your vote - so don't insert if we voted for the same thing we did before */
if ( !isset( $ratings[ \IPS\Member::loggedIn()->member_id ] ) OR $ratings[ \IPS\Member::loggedIn()->member_id ] != $rating )
{
\IPS\Db::i()->insert( 'forums_question_ratings', array(
'topic' => $question->tid,
'forum' => $question->forum_id,
'member' => \IPS\Member::loggedIn()->member_id,
'rating' => $rating,
'date' => time()
), TRUE );
}
/* Rebuild count */
$question->question_rating = \IPS\Db::i()->select( 'SUM(rating)', 'forums_question_ratings', array( 'topic=?', $question->tid ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$question->save();
/* Redirect back */
\IPS\Output::i()->redirect( $question->url() );
}
/**
* Rate Answer
*
* @return void
*/
public function rateAnswer()
{
\IPS\Session::i()->csrfCheck();
try
{
$question = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
$answer = \IPS\forums\Topic\Post::loadAndCheckPerms( \IPS\Request::i()->answer );
}
catch ( \Exception $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/4', 404, '' );
}
if ( !$answer->item()->can('read') or !$answer->canVote() )
{
\IPS\Output::i()->error( 'no_module_permission', '2F173/5', 403, '' );
}
$rating = \intval( \IPS\Request::i()->rating );
if ( $rating !== 1 and $rating !== -1 )
{
\IPS\Output::i()->error( 'form_bad_value', '2F173/6', 403, '' );
}
$ratings = $question->answerVotes( \IPS\Member::loggedIn() );
/* If we've already rated the answer, remove that first */
\IPS\Db::i()->delete( 'forums_answer_ratings', array( 'topic=? AND post=? AND `member`=?', $question->tid, $answer->pid, \IPS\Member::loggedIn()->member_id ) );
/* Revoting for the same thing you already voted for should remove your vote - so don't insert if we voted for the same thing we did before */
if ( !isset( $ratings[ $answer->pid ] ) OR $ratings[ $answer->pid ] != $rating )
{
\IPS\Db::i()->insert( 'forums_answer_ratings', array(
'post' => $answer->pid,
'topic' => $question->tid,
'member' => \IPS\Member::loggedIn()->member_id,
'rating' => $rating,
'date' => time()
), TRUE );
}
$answer->post_field_int = (int) \IPS\Db::i()->select( 'SUM(rating)', 'forums_answer_ratings', array( 'post=?', $answer->pid ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$answer->save();
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->json( array( 'votes' => $answer->post_field_int, 'canVoteUp' => $answer->canVote(1), 'canVoteDown' => $answer->canVote(-1) ) );
}
else
{
\IPS\Output::i()->redirect( $answer->url() );
}
}
/**
* Set Best Answer
*
* @return void
*/
public function bestAnswer()
{
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
$post = \IPS\forums\Topic\Post::loadAndCheckPerms( \IPS\Request::i()->answer );
if ( !$topic->canSetBestAnswer() )
{
throw new \OutOfRangeException;
}
if ( $post->item() != $topic )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/7', 404, '' );
}
$topic->toggleSolveComment( $post->pid, TRUE );
/* Log */
if ( \IPS\Member::loggedIn()->modPermission('can_set_best_answer') )
{
\IPS\Session::i()->modLog( 'modlog__best_answer_set', array( $post->pid => FALSE ), $topic );
}
\IPS\Output::i()->redirect( $post->url() );
}
/**
* Unset Best Answer
*
* @return void
*/
public function unsetBestAnswer()
{
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
$post = \IPS\forums\Topic\Post::loadAndCheckPerms( \IPS\Request::i()->answer );
if ( !$topic->canSetBestAnswer() )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/G', 404, '' );
}
if ( $post->item() != $topic )
{
throw new \OutOfRangeException;
}
try
{
$topic->toggleSolveComment( $post->pid, FALSE );
if ( \IPS\Member::loggedIn()->modPermission('can_set_best_answer') )
{
\IPS\Session::i()->modLog( 'modlog__best_answer_unset', array( $post->pid => FALSE ), $topic );
}
}
catch ( \Exception $e ) {}
\IPS\Output::i()->redirect( $post->url() );
}
/**
* Saved Action
*
* @return void
*/
public function savedAction()
{
try
{
\IPS\Session::i()->csrfCheck();
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
$action = \IPS\forums\SavedAction::load( \IPS\Request::i()->action );
$action->runOn( $topic );
/* Log */
\IPS\Session::i()->modLog( 'modlog__saved_action', array( 'forums_mmod_' . $action->mm_id => TRUE, $topic->url()->__toString() => FALSE, $topic->mapped( 'title' ) => FALSE ), $topic );
\IPS\Output::i()->redirect( $topic->url() );
}
catch ( \LogicException $e )
{
}
}
/**
* Mark Topic Read
*
* @return void
*/
public function markRead()
{
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::load( \IPS\Request::i()->id );
$topic->markRead();
if ( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->json( "OK" );
}
else
{
\IPS\Output::i()->redirect( $topic->url() );
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'no_module_permission', '2F173/C', 403, 'no_module_permission_guest' );
}
}
/**
* We need to use the custom widget poll template for ajax methods
*
* @return void
*/
public function widgetPoll()
{
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
}
catch( \OutOfRangeException $ex )
{
\IPS\Output::i()->error( 'node_error', '2F173/N', 403, '' );
}
$poll = $topic->getPoll();
$poll->displayTemplate = array( \IPS\Theme::i()->getTemplate( 'widgets', 'forums', 'front' ), 'pollWidget' );
$poll->url = $topic->url();
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'widgets', 'forums', 'front' )->poll( $topic, $poll );
}
/**
* Show a single comment requested by ajax
*
* @return void
*/
public function ajaxShowComment()
{
try
{
if ( ! \IPS\Request::i()->isAjax() )
{
throw new \BadMethodCallException();
}
\IPS\Session::i()->csrfCheck();
try
{
$topic = \IPS\forums\Topic::loadAndCheckPerms( \IPS\Request::i()->id );
}
catch( \OutOfRangeException $ex )
{
\IPS\Output::i()->error( 'node_error', '2F173/N', 403, '' );
}
$comment = \IPS\forums\Topic\Post::load( \IPS\Request::i()->showComment );
if ( ! $comment->canView() )
{
throw new \BadMethodCallException();
}
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'global', 'core' )->commentContainer( $topic, $comment ), 200, 'text/html' ) );
}
catch( \Exception $e )
{
return '';
}
}
/**
* Find a Comment / Review (do=findComment/findReview)
*
* @param string $commentClass The comment/review class
* @param \IPS\Content\Comment $comment The comment/review
* @param \IPS\Content\Item $item The item
* @return void
*/
public function _find( $commentClass, $comment, $item )
{
/* For normal topics (i.e. not questions), we can handle this normally */
if ( !$item->isQuestion() )
{
return parent::_find( $commentClass, $comment, $item );
}
/* Otherwise we need to get the position ordered by votes... */
if ( $item->isArchived() )
{
$where = array( array( 'archive_topic_id=?', $item->tid ) );
if ( !$item->canViewHiddenComments() )
{
$hiddenWhereClause = "(archive_queued != -2 AND archive_queued != -1 AND archive_queued != 1)";
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array( "( {$hiddenWhereClause} OR ( archive_queued=1 AND archive_author_id=" . \IPS\Member::loggedIn()->member_id . '))' );
}
else
{
$where[] = array( $hiddenWhereClause );
}
}
/* Connect to the remote DB if needed */
if ( \IPS\CIC2 )
{
$storage = \IPS\Cicloud\getForumArchiveDb();
}
else
{
$storage = !\IPS\Settings::i()->archive_remote_sql_host ? \IPS\Db::i() : \IPS\Db::i( 'archive', array(
'sql_host' => \IPS\Settings::i()->archive_remote_sql_host,
'sql_user' => \IPS\Settings::i()->archive_remote_sql_user,
'sql_pass' => \IPS\Settings::i()->archive_remote_sql_pass,
'sql_database' => \IPS\Settings::i()->archive_remote_sql_database,
'sql_port' => \IPS\Settings::i()->archive_sql_port,
'sql_socket' => \IPS\Settings::i()->archive_sql_socket,
'sql_tbl_prefix'=> \IPS\Settings::i()->archive_sql_tbl_prefix,
'sql_utf8mb4' => isset( \IPS\Settings::i()->sql_utf8mb4 ) ? \IPS\Settings::i()->sql_utf8mb4 : FALSE
) );
}
$answers = $storage->select( 'archive_id, @rownum := @rownum + 1 AS position', 'forums_archive_posts', $where, 'archive_is_first DESC, archive_bwoptions DESC, archive_field_int DESC, archive_content_date' )->join( array( '(SELECT @rownum := 0)', 'r' ), NULL, 'JOIN' );
$commentPosition = $storage->select( 'position', $answers, array( 'archive_id=?', $comment->id ) )->first() - 1;
}
else
{
$where = array( array( 'topic_id=?', $item->tid ) );
if ( !$item->canViewHiddenComments() )
{
$hiddenWhereClause = "(queued != -2 AND queued != -1 AND queued != 1)";
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array( "( {$hiddenWhereClause} OR ( queued=1 AND author_id=" . \IPS\Member::loggedIn()->member_id . '))' );
}
else
{
$where[] = array( $hiddenWhereClause );
}
}
$answers = \IPS\Db::i()->select( 'pid, @rownum := @rownum + 1 AS position', 'forums_posts', $where, 'new_topic DESC, post_bwoptions DESC, post_field_int DESC, post_date' )->join( array( '(SELECT @rownum := 0)', 'r' ), NULL, 'JOIN' );
$commentPosition = \IPS\Db::i()->select( 'position', $answers, array( 'pid=?', $comment->pid ) )->first() - 1;
}
/* Now work out what page that makes it */
$url = $item->url();
$perPage = $item::getCommentsPerPage();
$page = ceil( $commentPosition / $perPage );
if ( $page != 1 )
{
$url = $url->setPage( 'page', $page );
}
/* And redirect */
$idField = $commentClass::$databaseColumnId;
\IPS\Output::i()->redirect( $url->setFragment( 'comment-' . $comment->$idField ) );
}
/**
* Edit Comment/Review
*
* @param string $commentClass The comment/review class
* @param \IPS\Content\Comment $comment The comment/review
* @param \IPS\Content\Item $item The item
* @return void
* @throws \LogicException
*/
protected function _edit( $commentClass, $comment, $item )
{
\IPS\Member::loggedIn()->language()->words['edit_comment'] = \IPS\Member::loggedIn()->language()->addToStack( 'edit_reply', FALSE );
return parent::_edit( $commentClass, $comment, $item );
}
/**
* Stuff that applies to both comments and reviews
*
* @param string $method Desired method
* @param array $args Arguments
* @return void
*/
public function __call( $method, $args )
{
$class = static::$contentModel;
try
{
$item = $class::load( \IPS\Request::i()->id );
if ( !$item->canView() )
{
$forum = $item->container();
\IPS\Output::i()->error( $forum ? $forum->errorMessage() : 'node_error_no_perm', '2F173/K', 403, '' );
}
if ( $item->isArchived() )
{
$class::$commentClass = $class::$archiveClass;
}
return parent::__call( $method, $args );
}
catch( \OutOfRangeException $e )
{
if ( isset( \IPS\Request::i()->do ) AND \IPS\Request::i()->do === 'findComment' AND isset( \IPS\Request::i()->comment ) )
{
try
{
$commentClass = $class::$commentClass;
$comment = $commentClass::load( \IPS\Request::i()->comment );
$topic = \IPS\forums\Topic::load( $comment->topic_id );
\IPS\Output::i()->redirect( $topic->url()->setQueryString( array( 'do' => 'findComment', 'comment' => \IPS\Request::i()->comment ) ), NULL, 301 );
}
catch( \Exception $e )
{
\IPS\Output::i()->error( 'node_error', '2F173/M', 404, '' );
}
}
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'topic_call' );
\IPS\Output::i()->error( 'node_error', '2F173/I', 404, '' );
}
}
/**
* Form for splitting
*
* @param \IPS\Content\Item $item The item
* @return \IPS\Helpers\Form
*/
protected function _splitForm( \IPS\Content\Item $item )
{
$form = parent::_splitForm( $item );
if ( isset( $form->elements['']['topic_create_state'] ) )
{
unset( $form->elements['']['topic_create_state'] );
}
return $form;
}
/**
* Split Comment
*
* @param string $commentClass The comment/review class
* @param \IPS\Content\Comment $comment The comment/review
* @param \IPS\Content\Item $item The item
* @return void
* @throws \LogicException
*/
protected function _split( $commentClass, $comment, $item )
{
parent::_split( $commentClass, $comment, $item );
$item->rebuildPopularTime();
}
}