Source of file Forum.php
Size: 50,595 Bytes - Last Modified: 2021-12-23T10:30:31+00:00
/var/www/docs.ssmods.com/process/src/code/pagetypes/Forum.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497 | <?php /** * Forum represents a collection of forum threads. Each thread is a different topic on * the site. You can customize permissions on a per forum basis in the CMS. * * @todo Implement PermissionProvider for editing, creating forums. * * @package forum */ class Forum extends Page { private static $allowed_children = 'none'; private static $icon = "forum/images/treeicons/user"; /** * Enable this to automatically notify moderators when a message is posted * or edited on his forums. */ static $notify_moderators = false; private static $db = array( "Abstract" => "Text", "CanPostType" => "Enum('Inherit, Anyone, LoggedInUsers, OnlyTheseUsers, NoOne', 'Inherit')", "CanAttachFiles" => "Boolean", ); private static $has_one = array( "Moderator" => "Member", "Category" => "ForumCategory" ); private static $many_many = array( 'Moderators' => 'Member', 'PosterGroups' => 'Group' ); private static $defaults = array( "ForumPosters" => "LoggedInUsers" ); /** * Number of posts to include in the thread view before pagination takes effect. * * @var int */ static $posts_per_page = 8; /** * When migrating from older versions of the forum it used post ID as the url token * as of forum 1.0 we now use ThreadID. If you want to enable 301 redirects from post to thread ID * set this to true * * @var bool */ static $redirect_post_urls_to_thread = false; /** * Check if the user can view the forum. */ public function canView($member = null) { if (!$member) { $member = Member::currentUser(); } return (parent::canView($member) || $this->canModerate($member)); } /** * Check if the user can post to the forum and edit his own posts. */ public function canPost($member = null) { if (!$member) { $member = Member::currentUser(); } if ($this->CanPostType == "Inherit") { $holder = $this->getForumHolder(); if ($holder) { return $holder->canPost($member); } return false; } if ($this->CanPostType == "NoOne") { return false; } if ($this->CanPostType == "Anyone" || $this->canEdit($member)) { return true; } if ($member = Member::currentUser()) { if ($member->IsSuspended()) { return false; } if ($member->IsBanned()) { return false; } if ($this->CanPostType == "LoggedInUsers") { return true; } if ($groups = $this->PosterGroups()) { foreach ($groups as $group) { if ($member->inGroup($group)) { return true; } } } } return false; } /** * Check if user has access to moderator panel and can delete posts and threads. */ public function canModerate($member = null) { if (!$member) { $member = Member::currentUser(); } if (!$member) { return false; } // Admins if (Permission::checkMember($member, 'ADMIN')) { return true; } // Moderators if ($member->isModeratingForum($this)) { return true; } return false; } /** * Can we attach files to topics/posts inside this forum? * * @return bool Set to TRUE if the user is allowed to, to FALSE if they're * not */ public function canAttach($member = null) { return $this->CanAttachFiles ? true : false; } public function requireTable() { // Migrate permission columns if (DB::getConn()->hasTable('Forum')) { $fields = DB::getConn()->fieldList('Forum'); if (in_array('ForumPosters', array_keys($fields)) && !in_array('CanPostType', array_keys($fields))) { DB::getConn()->renameField('Forum', 'ForumPosters', 'CanPostType'); DB::alteration_message('Migrated forum permissions from "ForumPosters" to "CanPostType"', "created"); } } parent::requireTable(); } /** * Add default records to database * * This function is called whenever the database is built, after the * database tables have all been created. */ public function requireDefaultRecords() { parent::requireDefaultRecords(); $code = "ACCESS_FORUM"; if (!($forumGroup = Group::get()->filter('Code', 'forum-members')->first())) { $group = new Group(); $group->Code = 'forum-members'; $group->Title = "Forum Members"; $group->write(); Permission::grant($group->ID, $code); DB::alteration_message(_t('Forum.GROUPCREATED', 'Forum Members group created'), 'created'); } elseif (!Permission::get()->filter(array('GroupID' => $forumGroup->ID, 'Code' => $code))->exists()) { Permission::grant($forumGroup->ID, $code); } if (!($category = ForumCategory::get()->first())) { $category = new ForumCategory(); $category->Title = _t('Forum.DEFAULTCATEGORY', 'General'); $category->write(); } if (!ForumHolder::get()->exists()) { $forumholder = new ForumHolder(); $forumholder->Title = "Forums"; $forumholder->URLSegment = "forums"; $forumholder->Content = "<p>"._t('Forum.WELCOMEFORUMHOLDER', 'Welcome to SilverStripe Forum Module! This is the default ForumHolder page. You can now add forums.')."</p>"; $forumholder->Status = "Published"; $forumholder->write(); $forumholder->publish("Stage", "Live"); DB::alteration_message(_t('Forum.FORUMHOLDERCREATED', 'ForumHolder page created'), "created"); $forum = new Forum(); $forum->Title = _t('Forum.TITLE', 'General Discussion'); $forum->URLSegment = "general-discussion"; $forum->ParentID = $forumholder->ID; $forum->Content = "<p>"._t('Forum.WELCOMEFORUM', 'Welcome to SilverStripe Forum Module! This is the default Forum page. You can now add topics.')."</p>"; $forum->Status = "Published"; $forum->CategoryID = $category->ID; $forum->write(); $forum->publish("Stage", "Live"); DB::alteration_message(_t('Forum.FORUMCREATED', 'Forum page created'), "created"); } } /** * Check if we can and should show forums in categories */ public function getShowInCategories() { $holder = $this->getForumHolder(); if ($holder) { return $holder->getShowInCategories(); } } /** * Returns a FieldList with which to create the CMS editing form * * @return FieldList The fields to be displayed in the CMS. */ public function getCMSFields() { $self = $this; $this->beforeUpdateCMSFields(function ($fields) use ($self) { Requirements::javascript("forum/javascript/ForumAccess.js"); Requirements::css("forum/css/Forum_CMS.css"); $fields->addFieldToTab("Root.Access", new HeaderField(_t('Forum.ACCESSPOST', 'Who can post to the forum?'), 2)); $fields->addFieldToTab("Root.Access", $optionSetField = new OptionsetField("CanPostType", "", array( "Inherit" => "Inherit", "Anyone" => _t('Forum.READANYONE', 'Anyone'), "LoggedInUsers" => _t('Forum.READLOGGEDIN', 'Logged-in users'), "OnlyTheseUsers" => _t('Forum.READLIST', 'Only these people (choose from list)'), "NoOne" => _t('Forum.READNOONE', 'Nobody. Make Forum Read Only') ))); $optionSetField->addExtraClass('ForumCanPostTypeSelector'); $fields->addFieldsToTab("Root.Access", array( new TreeMultiselectField("PosterGroups", _t('Forum.GROUPS', "Groups")), new OptionsetField("CanAttachFiles", _t('Forum.ACCESSATTACH', 'Can users attach files?'), array( "1" => _t('Forum.YES', 'Yes'), "0" => _t('Forum.NO', 'No') )) )); //Dropdown of forum category selection. $categories = ForumCategory::get()->map(); $fields->addFieldsToTab( "Root.Main", DropdownField::create('CategoryID', _t('Forum.FORUMCATEGORY', 'Forum Category'), $categories), 'Content' ); //GridField Config - only need to attach or detach Moderators with existing Member accounts. $moderatorsConfig = GridFieldConfig::create() ->addComponent(new GridFieldButtonRow('before')) ->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right')) ->addComponent(new GridFieldToolbarHeader()) ->addComponent($sort = new GridFieldSortableHeader()) ->addComponent($columns = new GridFieldDataColumns()) ->addComponent(new GridFieldDeleteAction(true)) ->addComponent(new GridFieldPageCount('toolbar-header-right')) ->addComponent($pagination = new GridFieldPaginator()); // Use GridField for Moderator management $moderators = GridField::create( 'Moderators', _t('MODERATORS', 'Moderators for this forum'), $self->Moderators(), $moderatorsConfig ); $columns->setDisplayFields(array( 'Nickname' => 'Nickname', 'FirstName' => 'First name', 'Surname' => 'Surname', 'Email'=> 'Email', 'LastVisited.Long' => 'Last Visit' )); $sort->setThrowExceptionOnBadDataType(false); $pagination->setThrowExceptionOnBadDataType(false); $fields->addFieldToTab('Root.Moderators', $moderators); }); $fields = parent::getCMSFields(); return $fields; } /** * Create breadcrumbs * * @param int $maxDepth Maximal lenght of the breadcrumb navigation * @param bool $unlinked Set to TRUE if the breadcrumb should consist of * links, otherwise FALSE. * @param bool $stopAtPageType Currently not used * @param bool $showHidden Set to TRUE if also hidden pages should be * displayed * @return string HTML code to display breadcrumbs */ public function Breadcrumbs($maxDepth = null, $unlinked = false, $stopAtPageType = false, $showHidden = false) { $page = $this; $nonPageParts = array(); $parts = array(); $controller = Controller::curr(); $params = $controller->getURLParams(); $forumThreadID = $params['ID']; if (is_numeric($forumThreadID)) { if ($topic = ForumThread::get()->byID($forumThreadID)) { $nonPageParts[] = Convert::raw2xml($topic->getTitle()); } } while ($page && (!$maxDepth || sizeof($parts) < $maxDepth)) { if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) { if ($page->URLSegment == 'home') { $hasHome = true; } if ($nonPageParts) { $parts[] = '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>'; } else { $parts[] = (($page->ID == $this->ID) || $unlinked) ? Convert::raw2xml($page->Title) : '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>'; } } $page = $page->Parent; } return implode(" » ", array_reverse(array_merge($nonPageParts, $parts))); } /** * Helper Method from the template includes. Uses $ForumHolder so in order for it work * it needs to be included on this page * * @return ForumHolder */ public function getForumHolder() { $holder = $this->Parent(); if ($holder->ClassName=='ForumHolder') { return $holder; } } /** * Get the latest posting of the forum. For performance the forum ID is stored on the * {@link Post} object as well as the {@link Forum} object * * @return Post */ public function getLatestPost() { return Post::get()->filter('ForumID', $this->ID)->sort('"Post"."ID" DESC')->first(); } /** * Get the number of total topics (threads) in this Forum * * @return int Returns the number of topics (threads) */ public function getNumTopics() { $sqlQuery = new SQLQuery(); $sqlQuery->setFrom('"Post"'); $sqlQuery->setSelect('COUNT(DISTINCT("ThreadID"))'); $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"'); $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\''); $sqlQuery->addWhere('"ForumID" = ' . $this->ID); return $sqlQuery->execute()->value(); } /** * Get the number of total posts * * @return int Returns the number of posts */ public function getNumPosts() { $sqlQuery = new SQLQuery(); $sqlQuery->setFrom('"Post"'); $sqlQuery->setSelect('COUNT("Post"."ID")'); $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"'); $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\''); $sqlQuery->addWhere('"ForumID" = ' . $this->ID); return $sqlQuery->execute()->value(); } /** * Get the number of distinct Authors * * @return int */ public function getNumAuthors() { $sqlQuery = new SQLQuery(); $sqlQuery->setFrom('"Post"'); $sqlQuery->setSelect('COUNT(DISTINCT("AuthorID"))'); $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"'); $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\''); $sqlQuery->addWhere('"ForumID" = ' . $this->ID); return $sqlQuery->execute()->value(); } /** * Returns the Topics (the first Post of each Thread) for this Forum * @return DataList */ public function getTopics() { // Get a list of Posts $posts = Post::get(); // Get the underlying query and change it to return the ThreadID and Max(Created) and Max(ID) for each thread // of those posts $postQuery = $posts->dataQuery()->query(); $postQuery ->setSelect(array()) ->selectField('MAX("Post"."Created")', 'PostCreatedMax') ->selectField('MAX("Post"."ID")', 'PostIDMax') ->selectField('"ThreadID"') ->setGroupBy('"ThreadID"') ->addWhere(sprintf('"ForumID" = \'%s\'', $this->ID)) ->setDistinct(false); // Get a list of forum threads inside this forum that aren't sticky $threads = ForumThread::get()->filter(array( 'ForumID' => $this->ID, 'IsGlobalSticky' => 0, 'IsSticky' => 0 )); // Get the underlying query and change it to inner join on the posts list to just show threads that // have approved (and maybe awaiting) posts, and sort the threads by the most recent post $threadQuery = $threads->dataQuery()->query(); $threadQuery ->addSelect(array('"PostMax"."PostCreatedMax", "PostMax"."PostIDMax"')) ->addFrom('INNER JOIN ('.$postQuery->sql().') AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")') ->addOrderBy(array('"PostMax"."PostCreatedMax" DESC', '"PostMax"."PostIDMax" DESC')) ->setDistinct(false); // Alter the forum threads list to use the new query $threads = $threads->setDataQuery(new Forum_DataQuery('ForumThread', $threadQuery)); // And return the results return $threads->exists() ? new PaginatedList($threads, $_GET) : null; } /* * Returns the Sticky Threads * @param boolean $include_global Include Global Sticky Threads in the results (default: true) * @return DataList */ public function getStickyTopics($include_global = true) { // Get Threads that are sticky & in this forum $where = '("ForumThread"."ForumID" = '.$this->ID.' AND "ForumThread"."IsSticky" = 1)'; // Get Threads that are globally sticky if ($include_global) { $where .= ' OR ("ForumThread"."IsGlobalSticky" = 1)'; } // Get the underlying query $query = ForumThread::get()->where($where)->dataQuery()->query(); // Sort by the latest Post in each thread's Created date $query ->addSelect('"PostMax"."PostMax"') // TODO: Confirm this works in non-MySQL DBs ->addFrom(sprintf( 'LEFT JOIN (SELECT MAX("Created") AS "PostMax", "ThreadID" FROM "Post" WHERE "ForumID" = \'%s\' GROUP BY "ThreadID") AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")', $this->ID )) ->addOrderBy('"PostMax"."PostMax" DESC') ->setDistinct(false); // Build result as ArrayList $res = new ArrayList(); $rows = $query->execute(); if ($rows) { foreach ($rows as $row) { $res->push(new ForumThread($row)); } } return $res; } } /** * The forum controller class * * @package forum */ class Forum_Controller extends Page_Controller { private static $allowed_actions = array( 'AdminFormFeatures', 'deleteattachment', 'deletepost', 'editpost', 'markasspam', 'PostMessageForm', 'reply', 'show', 'starttopic', 'subscribe', 'unsubscribe', 'rss', 'ban', 'ghost' ); public function init() { parent::init(); if ($this->redirectedTo()) { return; } Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js"); Requirements::javascript("forum/javascript/Forum.js"); Requirements::javascript("forum/javascript/jquery.MultiFile.js"); Requirements::themedCSS('Forum', 'forum', 'all'); RSSFeed::linkToFeed($this->Parent()->Link("rss/forum/$this->ID"), sprintf(_t('Forum.RSSFORUM', "Posts to the '%s' forum"), $this->Title)); RSSFeed::linkToFeed($this->Parent()->Link("rss"), _t('Forum.RSSFORUMS', 'Posts to all forums')); if (!$this->canView()) { $messageSet = array( 'default' => _t('Forum.LOGINDEFAULT', 'Enter your email address and password to view this forum.'), 'alreadyLoggedIn' => _t('Forum.LOGINALREADY', 'I’m sorry, but you can’t access this forum until you’ve logged in. If you want to log in as someone else, do so below'), 'logInAgain' => _t('Forum.LOGINAGAIN', 'You have been logged out of the forums. If you would like to log in again, enter a username and password below.') ); Security::permissionFailure($this, $messageSet); return; } // Log this visit to the ForumMember if they exist $member = Member::currentUser(); if ($member && Config::inst()->get('ForumHolder', 'currently_online_enabled')) { $member->LastViewed = date("Y-m-d H:i:s"); $member->write(); } // Set the back url if (isset($_SERVER['REQUEST_URI'])) { Session::set('BackURL', $_SERVER['REQUEST_URI']); } else { Session::set('BackURL', $this->Link()); } } /** * A convenience function which provides nice URLs for an rss feed on this forum. */ public function rss() { $this->redirect($this->Parent()->Link("rss/forum/$this->ID"), 301); } /** * Is OpenID support available? * * This method checks if the {@link OpenIDAuthenticator} is available and * registered. * * @return bool Returns TRUE if OpenID is available, FALSE otherwise. */ public function OpenIDAvailable() { return $this->Parent()->OpenIDAvailable(); } /** * Subscribe a user to a thread given by an ID. * * Designed to be called via AJAX so return true / false * * @return bool */ public function subscribe(SS_HTTPRequest $request) { // Check CSRF if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } $subscribed = false; if (Member::currentUser() && !ForumThread_Subscription::already_subscribed($this->urlParams['ID'])) { $obj = new ForumThread_Subscription(); $obj->ThreadID = (int) $this->urlParams['ID']; $obj->MemberID = Member::currentUserID(); $obj->LastSent = date("Y-m-d H:i:s"); $obj->write(); $subscribed = true; } return ($request->isAjax()) ? $subscribed : $this->redirectBack(); } /** * Unsubscribe a user from a thread by an ID * * Designed to be called via AJAX so return true / false * * @return bool */ public function unsubscribe(SS_HTTPRequest $request) { $member = Member::currentUser(); $unsubscribed = false; if (!$member) { Security::permissionFailure($this, _t('LOGINTOUNSUBSCRIBE', 'To unsubscribe from that thread, please log in first.')); } if (ForumThread_Subscription::already_subscribed($this->urlParams['ID'], $member->ID)) { DB::query(" DELETE FROM \"ForumThread_Subscription\" WHERE \"ThreadID\" = '". Convert::raw2sql($this->urlParams['ID']) ."' AND \"MemberID\" = '$member->ID'"); $unsubscribed = true; } return ($request->isAjax()) ? $unsubscribed : $this->redirectBack(); } /** * Mark a post as spam. Deletes any posts or threads created by that user * and removes their user account from the site * * Must be logged in and have the correct permissions to do marking */ public function markasspam(SS_HTTPRequest $request) { $currentUser = Member::currentUser(); if (!isset($this->urlParams['ID'])) { return $this->httpError(400); } if (!$this->canModerate()) { return $this->httpError(403); } // Check CSRF token if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } $post = Post::get()->byID($this->urlParams['ID']); if ($post) { // post was the start of a thread, Delete the whole thing if ($post->isFirstPost()) { $post->Thread()->delete(); } // Delete the current post $post->delete(); $post->extend('onAfterMarkAsSpam'); // Log deletion event SS_Log::log(sprintf( 'Marked post #%d as spam, by moderator %s (#%d)', $post->ID, $currentUser->Email, $currentUser->ID ), SS_Log::NOTICE); // Suspend the member (rather than deleting him), // which gives him or a moderator the chance to revoke a decision. if ($author = $post->Author()) { $author->SuspendedUntil = date('Y-m-d', strtotime('+99 years', SS_Datetime::now()->Format('U'))); $author->write(); } SS_Log::log(sprintf( 'Suspended member %s (#%d) for spam activity, by moderator %s (#%d)', $author->Email, $author->ID, $currentUser->Email, $currentUser->ID ), SS_Log::NOTICE); } return (Director::is_ajax()) ? true : $this->redirect($this->Link()); } public function ban(SS_HTTPRequest $r) { if (!$r->param('ID')) { return $this->httpError(404); } if (!$this->canModerate()) { return $this->httpError(403); } $member = Member::get()->byID($r->param('ID')); if (!$member || !$member->exists()) { return $this->httpError(404); } $member->ForumStatus = 'Banned'; $member->write(); // Log event $currentUser = Member::currentUser(); SS_Log::log(sprintf( 'Banned member %s (#%d), by moderator %s (#%d)', $member->Email, $member->ID, $currentUser->Email, $currentUser->ID ), SS_Log::NOTICE); return ($r->isAjax()) ? true : $this->redirectBack(); } public function ghost(SS_HTTPRequest $r) { if (!$r->param('ID')) { return $this->httpError(400); } if (!$this->canModerate()) { return $this->httpError(403); } $member = Member::get()->byID($r->param('ID')); if (!$member || !$member->exists()) { return $this->httpError(404); } $member->ForumStatus = 'Ghost'; $member->write(); // Log event $currentUser = Member::currentUser(); SS_Log::log(sprintf( 'Ghosted member %s (#%d), by moderator %s (#%d)', $member->Email, $member->ID, $currentUser->Email, $currentUser->ID ), SS_Log::NOTICE); return ($r->isAjax()) ? true : $this->redirectBack(); } /** * Get posts to display. This method assumes an URL parameter "ID" which contains the thread ID. * @param string sortDirection The sort order direction, either ASC for ascending (default) or DESC for descending * @return DataObjectSet Posts */ public function Posts($sortDirection = "ASC") { $numPerPage = Forum::$posts_per_page; $posts = Post::get() ->filter('ThreadID', $this->urlParams['ID']) ->sort('Created', $sortDirection); if (isset($_GET['showPost']) && !isset($_GET['start'])) { $postIDList = clone $posts; $postIDList = $postIDList->select('ID')->toArray(); if ($postIDList->exists()) { $foundPos = array_search($_GET['showPost'], $postIDList); $_GET['start'] = floor($foundPos / $numPerPage) * $numPerPage; } } if (!isset($_GET['start'])) { $_GET['start'] = 0; } $member = Member::currentUser(); /* * Don't show posts of banned or ghost members, unless current Member * is a ghost member and owner of current post */ $posts = $posts->exclude(array( 'Author.ForumStatus' => 'Banned' )); if ($member) { $posts = $posts->exclude(array( 'Author.ForumStatus' => 'Ghost', 'Author.ID:not' => $member->ID )); } else { $posts = $posts->exclude(array( 'Author.ForumStatus' => 'Ghost' )); } $paginated = new PaginatedList($posts, $_GET); $paginated->setPageLength(Forum::$posts_per_page); return $paginated; } /** * Get the usable BB codes * * @return DataObjectSet Returns the usable BB codes * @see BBCodeParser::usable_tags() */ public function BBTags() { return BBCodeParser::usable_tags(); } /** * Section for dealing with reply / edit / create threads form * * @return Form Returns the post message form */ public function PostMessageForm($addMode = false, $post = false) { $thread = false; if ($post) { $thread = $post->Thread(); } elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) { $thread = DataObject::get_by_id('ForumThread', $this->urlParams['ID']); } // Check permissions $messageSet = array( 'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'), 'alreadyLoggedIn' => _t( 'Forum.LOGINTOPOSTLOGGEDIN', 'I\'m sorry, but you can\'t post to this forum until you\'ve logged in.' .'If you want to log in as someone else, do so below. If you\'re logged in and you still can\'t post, you don\'t have the correct permissions to post.' ), 'logInAgain' => _t('Forum.LOGINTOPOSTAGAIN', 'You have been logged out of the forums. If you would like to log in again to post, enter a username and password below.'), ); // Creating new thread if ($addMode && !$this->canPost()) { Security::permissionFailure($this, $messageSet); return false; } // Replying to existing thread if (!$addMode && !$post && $thread && !$thread->canPost()) { Security::permissionFailure($this, $messageSet); return false; } // Editing existing post if (!$addMode && $post && !$post->canEdit()) { Security::permissionFailure($this, $messageSet); return false; } $forumBBCodeHint = $this->renderWith('Forum_BBCodeHint'); $fields = new FieldList( ($post && $post->isFirstPost() || !$thread) ? new TextField("Title", _t('Forum.FORUMTHREADTITLE', 'Title')) : new ReadonlyField('Title', _t('Forum.FORUMTHREADTITLE', ''), 'Re:'. $thread->Title), new TextareaField("Content", _t('Forum.FORUMREPLYCONTENT', 'Content')), new LiteralField( "BBCodeHelper", "<div class=\"BBCodeHint\">[ <a href=\"#BBTagsHolder\" id=\"BBCodeHint\">" . _t('Forum.BBCODEHINT', 'View Formatting Help') . "</a> ]</div>" . $forumBBCodeHint ), new CheckboxField( "TopicSubscription", _t('Forum.SUBSCRIBETOPIC', 'Subscribe to this topic (Receive email notifications when a new reply is added)'), ($thread) ? $thread->getHasSubscribed() : false ) ); if ($thread) { $fields->push(new HiddenField('ThreadID', 'ThreadID', $thread->ID)); } if ($post) { $fields->push(new HiddenField('ID', 'ID', $post->ID)); } // Check if we can attach files to this forum's posts if ($this->canAttach()) { $fields->push(FileField::create("Attachment", _t('Forum.ATTACH', 'Attach file'))); } // If this is an existing post check for current attachments and generate // a list of the uploaded attachments if ($post && $attachmentList = $post->Attachments()) { if ($attachmentList->exists()) { $attachments = "<div id=\"CurrentAttachments\"><h4>". _t('Forum.CURRENTATTACHMENTS', 'Current Attachments') ."</h4><ul>"; $link = $this->Link(); // An instance of the security token $token = SecurityToken::inst(); foreach ($attachmentList as $attachment) { // Generate a link properly, since it requires a security token $attachmentLink = Controller::join_links($link, 'deleteattachment', $attachment->ID); $attachmentLink = $token->addToUrl($attachmentLink); $attachments .= "<li class='attachment-$attachment->ID'>$attachment->Name [<a href='{$attachmentLink}' rel='$attachment->ID' class='deleteAttachment'>" . _t('Forum.REMOVE', 'remove') . "</a>]</li>"; } $attachments .= "</ul></div>"; $fields->push(new LiteralField('CurrentAttachments', $attachments)); } } $actions = new FieldList( new FormAction("doPostMessageForm", _t('Forum.REPLYFORMPOST', 'Post')) ); $required = $addMode === true ? new RequiredFields("Title", "Content") : new RequiredFields("Content"); $form = new Form($this, 'PostMessageForm', $fields, $actions, $required); $this->extend('updatePostMessageForm', $form, $post); if ($post) { $form->loadDataFrom($post); } return $form; } /** * Wrapper for older templates. Previously the new, reply and edit forms were 3 separate * forms, they have now been refactored into 1 form. But in order to not break existing * themes too much just include this. * * @deprecated 0.5 * @return Form */ public function ReplyForm() { user_error('Please Use $PostMessageForm in your template rather that $ReplyForm', E_USER_WARNING); return $this->PostMessageForm(); } /** * Post a message to the forum. This method is called whenever you want to make a * new post or edit an existing post on the forum * * @param Array - Data * @param Form - Submitted Form */ public function doPostMessageForm($data, $form) { $member = Member::currentUser(); //Allows interception of a Member posting content to perform some action before the post is made. $this->extend('beforePostMessage', $data, $member); $content = (isset($data['Content'])) ? $this->filterLanguage($data["Content"]) : ""; $title = (isset($data['Title'])) ? $this->filterLanguage($data["Title"]) : false; // If a thread id is passed append the post to the thread. Otherwise create // a new thread $thread = false; if (isset($data['ThreadID'])) { $thread = DataObject::get_by_id('ForumThread', $data['ThreadID']); } // If this is a simple edit the post then handle it here. Look up the correct post, // make sure we have edit rights to it then update the post $post = false; if (isset($data['ID'])) { $post = DataObject::get_by_id('Post', $data['ID']); if ($post && $post->isFirstPost()) { if ($title) { $thread->Title = $title; } } } // Check permissions $messageSet = array( 'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'), 'alreadyLoggedIn' => _t('Forum.NOPOSTPERMISSION', 'I\'m sorry, but you do not have permission post to this forum.'), 'logInAgain' => _t('Forum.LOGINTOPOSTAGAIN', 'You have been logged out of the forums. If you would like to log in again to post, enter a username and password below.'), ); // Creating new thread if (!$thread && !$this->canPost()) { Security::permissionFailure($this, $messageSet); return false; } // Replying to existing thread if ($thread && !$post && !$thread->canPost()) { Security::permissionFailure($this, $messageSet); return false; } // Editing existing post if ($thread && $post && !$post->canEdit()) { Security::permissionFailure($this, $messageSet); return false; } if (!$thread) { $thread = new ForumThread(); $thread->ForumID = $this->ID; if ($title) { $thread->Title = $title; } $starting_thread = true; } // Upload and Save all files attached to the field // Attachment will always be blank, If they had an image it will be at least in Attachment-0 //$attachments = new DataObjectSet(); $attachments = new ArrayList(); if (!empty($data['Attachment-0']) && !empty($data['Attachment-0']['tmp_name'])) { $id = 0; // // @todo this only supports ajax uploads. Needs to change the key (to simply Attachment). // while (isset($data['Attachment-' . $id])) { $image = $data['Attachment-' . $id]; if ($image && !empty($image['tmp_name'])) { $file = Post_Attachment::create(); $file->OwnerID = Member::currentUserID(); $folder = Config::inst()->get('ForumHolder', 'attachments_folder'); try { $upload = Upload::create()->loadIntoFile($image, $file, $folder); $file->write(); $attachments->push($file); } catch (ValidationException $e) { $message = _t('Forum.UPLOADVALIDATIONFAIL', 'Unallowed file uploaded. Please only upload files of the following: '); $message .= implode(', ', Config::inst()->get('File', 'allowed_extensions')); $form->addErrorMessage('Attachment', $message, 'bad'); Session::set("FormInfo.Form_PostMessageForm.data", $data); return $this->redirectBack(); } } $id++; } } // from now on the user has the correct permissions. save the current thread settings $thread->write(); if (!$post || !$post->canEdit()) { $post = new Post(); $post->AuthorID = ($member) ? $member->ID : 0; $post->ThreadID = $thread->ID; } $post->ForumID = $thread->ForumID; $post->Content = $content; $post->write(); if ($attachments) { foreach ($attachments as $attachment) { $attachment->PostID = $post->ID; $attachment->write(); } } // Add a topic subscription entry if required $isSubscribed = ForumThread_Subscription::already_subscribed($thread->ID); if (isset($data['TopicSubscription'])) { if (!$isSubscribed) { // Create a new topic subscription for this member $obj = new ForumThread_Subscription(); $obj->ThreadID = $thread->ID; $obj->MemberID = Member::currentUserID(); $obj->write(); } } elseif ($isSubscribed) { // See if the member wanted to remove themselves DB::query("DELETE FROM \"ForumThread_Subscription\" WHERE \"ThreadID\" = '$post->ThreadID' AND \"MemberID\" = '$member->ID'"); } // Send any notifications that need to be sent ForumThread_Subscription::notify($post); // Send any notifications to moderators of the forum if (Forum::$notify_moderators) { if (isset($starting_thread) && $starting_thread) { $this->notifyModerators($post, $thread, true); } else { $this->notifyModerators($post, $thread); } } return $this->redirect($post->Link()); } /** * Send email to moderators notifying them the thread has been created or post added/edited. */ public function notifyModerators($post, $thread, $starting_thread = false) { $moderators = $this->Moderators(); if ($moderators && $moderators->exists()) { foreach ($moderators as $moderator) { if ($moderator->Email) { $adminEmail = Config::inst()->get('Email', 'admin_email'); $email = new Email(); $email->setFrom($adminEmail); $email->setTo($moderator->Email); if ($starting_thread) { $email->setSubject('New thread "' . $thread->Title . '" in forum ['. $this->Title.']'); } else { $email->setSubject('New post "' . $post->Title. '" in forum ['.$this->Title.']'); } $email->setTemplate('ForumMember_NotifyModerator'); $email->populateTemplate(new ArrayData(array( 'NewThread' => $starting_thread, 'Moderator' => $moderator, 'Author' => $post->Author(), 'Forum' => $this, 'Post' => $post ))); $email->send(); } } } } /** * Return the Forbidden Words in this Forum * * @return Text */ public function getForbiddenWords() { return $this->Parent()->ForbiddenWords; } /** * This function filters $content by forbidden words, entered in forum holder. * * @param String $content (it can be Post Content or Post Title) * @return String $content (filtered string) */ public function filterLanguage($content) { $words = $this->getForbiddenWords(); if ($words != "") { $words = explode(",", $words); foreach ($words as $word) { $content = str_ireplace(trim($word), "*", $content); } } return $content; } /** * Get the link for the reply action * * @return string URL for the reply action */ public function ReplyLink() { return $this->Link() . 'reply/' . $this->urlParams['ID']; } /** * Show will get the selected thread to the user. Also increments the forums view count. * * If the thread does not exist it will pass the user to the 404 error page * * @return array|SS_HTTPResponse_Exception */ public function show() { $title = Convert::raw2xml($this->Title); if ($thread = $this->getForumThread()) { //If there is not first post either the thread has been removed or thread if a banned spammer. if (!$thread->getFirstPost()) { // don't hide the post for logged in admins or moderators $member = Member::currentUser(); if (!$this->canModerate($member)) { return $this->httpError(404); } } $thread->incNumViews(); $posts = sprintf(_t('Forum.POSTTOTOPIC', "Posts to the %s topic"), $thread->Title); RSSFeed::linkToFeed($this->Link("rss") . '/thread/' . (int) $this->urlParams['ID'], $posts); $title = Convert::raw2xml($thread->Title) . ' » ' . $title; $field = DBField::create_field('HTMLText', $title); return array( 'Thread' => $thread, 'Title' => $field ); } else { // if redirecting post ids to thread id is enabled then we need // to check to see if this matches a post and if it does redirect if (Forum::$redirect_post_urls_to_thread && isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) { if ($post = Post::get()->byID($this->urlParams['ID'])) { return $this->redirect($post->Link(), 301); } } } return $this->httpError(404); } /** * Start topic action * * @return array Returns an array to render the start topic page */ public function starttopic() { $topic = array( 'Subtitle' => DBField::create_field('HTMLText', _t('Forum.NEWTOPIC', 'Start a new topic')), 'Abstract' => DBField::create_field('HTMLText', DataObject::get_one("ForumHolder")->ForumAbstract) ); return $topic; } /** * Get the forum title * * @return string Returns the forum title */ public function getHolderSubtitle() { return $this->dbObject('Title'); } /** * Get the currently viewed forum. Ensure that the user can access it * * @return ForumThread */ public function getForumThread() { if (isset($this->urlParams['ID'])) { $SQL_id = Convert::raw2sql($this->urlParams['ID']); if (is_numeric($SQL_id)) { if ($thread = DataObject::get_by_id('ForumThread', $SQL_id)) { if (!$thread->canView()) { Security::permissionFailure($this); return false; } return $thread; } } } return false; } /** * Delete an Attachment * Called from the EditPost method. Its Done via Ajax * * @return boolean */ public function deleteattachment(SS_HTTPRequest $request) { // Check CSRF token if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } // check we were passed an id and member is logged in if (!isset($this->urlParams['ID'])) { return false; } $file = DataObject::get_by_id("Post_Attachment", (int) $this->urlParams['ID']); if ($file && $file->canDelete()) { $file->delete(); return (!Director::is_ajax()) ? $this->redirectBack() : true; } return false; } /** * Edit post action * * @return array Returns an array to render the edit post page */ public function editpost() { return array( 'Subtitle' => _t('Forum.EDITPOST', 'Edit post') ); } /** * Get the post edit form if the user has the necessary permissions * * @return Form */ public function EditForm() { $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null; $post = DataObject::get_by_id('Post', $id); return $this->PostMessageForm(false, $post); } /** * Delete a post via the url. * * @return bool */ public function deletepost(SS_HTTPRequest $request) { // Check CSRF token if (!SecurityToken::inst()->checkRequest($request)) { return $this->httpError(400); } if (isset($this->urlParams['ID'])) { if ($post = DataObject::get_by_id('Post', (int) $this->urlParams['ID'])) { if ($post->canDelete()) { // delete the whole thread if this is the first one. The delete action // on thread takes care of the posts. if ($post->isFirstPost()) { $thread = DataObject::get_by_id("ForumThread", $post->ThreadID); $thread->delete(); } else { // delete the post $post->delete(); } } } } return (Director::is_ajax()) ? true : $this->redirect($this->Link()); } /** * Returns the Forum Message from Session. This * is used for things like Moving thread messages * @return String */ public function ForumAdminMsg() { $message = Session::get('ForumAdminMsg'); Session::clear('ForumAdminMsg'); return $message; } /** * Forum Admin Features form. * Handles the dropdown to select the new forum category and the checkbox for stickyness * * @return Form */ public function AdminFormFeatures() { if (!$this->canModerate()) { return; } $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : false; $fields = new FieldList( new CheckboxField('IsSticky', _t('Forum.ISSTICKYTHREAD', 'Is this a Sticky Thread?')), new CheckboxField('IsGlobalSticky', _t('Forum.ISGLOBALSTICKY', 'Is this a Global Sticky (shown on all forums)')), new CheckboxField('IsReadOnly', _t('Forum.ISREADONLYTHREAD', 'Is this a Read only Thread?')), new HiddenField("ID", "Thread") ); if (($forums = Forum::get()) && $forums->exists()) { $fields->push(new DropdownField("ForumID", _t('Forum.CHANGETHREADFORUM', "Change Thread Forum"), $forums->map('ID', 'Title', 'Select New Category:')), '', null, 'Select New Location:'); } $actions = new FieldList( new FormAction('doAdminFormFeatures', _t('Forum.SAVE', 'Save')) ); $form = new Form($this, 'AdminFormFeatures', $fields, $actions); // need this id wrapper since the form method is called on save as // well and needs to return a valid form object if ($id) { $thread = ForumThread::get()->byID($id); $form->loadDataFrom($thread); } $this->extend('updateAdminFormFeatures', $form); return $form; } /** * Process's the moving of a given topic. Has to check for admin privledges, * passed an old topic id (post id in URL) and a new topic id */ public function doAdminFormFeatures($data, $form) { if (isset($data['ID'])) { $thread = ForumThread::get()->byID($data['ID']); if ($thread) { if (!$thread->canModerate()) { return Security::permissionFailure($this); } $form->saveInto($thread); $thread->write(); } } return $this->redirect($this->Link()); } } /** * This is a DataQuery that allows us to replace the underlying query. Hopefully this will * be a native ability in 3.1, but for now we need to. * TODO: Remove once API in core */ class Forum_DataQuery extends DataQuery { public function __construct($dataClass, $query) { parent::__construct($dataClass); $this->query = $query; } } |