Completed
Pull Request — master (#203)
by
unknown
40:36
created

Forum_Controller   D

Complexity

Total Complexity 157

Size/Duplication

Total Lines 948
Duplicated Lines 7.49 %

Coupling/Cohesion

Components 3
Dependencies 1

Importance

Changes 0
Metric Value
wmc 157
lcom 3
cbo 1
dl 71
loc 948
rs 4
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
B init() 5 41 6
A rss() 0 4 1
A OpenIDAvailable() 0 4 1
A subscribe() 0 19 4
A unsubscribe() 0 19 3
C markasspam() 0 52 8
B ban() 29 29 6
B ghost() 29 29 6
B Posts() 0 48 6
A BBTags() 0 4 1
F PostMessageForm() 4 109 26
A ReplyForm() 0 6 1
F doPostMessageForm() 4 152 35
B notifyModerators() 0 30 6
A getForbiddenWords() 0 4 1
A filterLanguage() 0 12 3
A ReplyLink() 0 4 1
C show() 0 39 8
A starttopic() 0 8 1
A getHolderSubtitle() 0 4 1
B getForumThread() 0 20 5
B deleteattachment() 0 22 6
A editpost() 0 6 1
A EditForm() 0 7 2
C deletepost() 0 25 7
A ForumAdminMsg() 0 7 1
B AdminFormFeatures() 0 36 6
A doAdminFormFeatures() 0 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Forum_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Forum_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forum\Pages;
4
5
use SilverStripe\Security\Member;
6
use SilverStripe\Security\Permission;
7
use SilverStripe\ORM\DB;
8
use SilverStripe\Security\Group;
9
use SilverStripe\View\Requirements;
10
use SilverStripe\Forms\HeaderField;
11
use SilverStripe\Forms\OptionsetField;
12
use SilverStripe\Forms\TreeMultiselectField;
13
use SilverStripe\Forms\DropdownField;
14
use SilverStripe\Forms\GridField\GridFieldConfig;
15
use SilverStripe\Forms\GridField\GridFieldButtonRow;
16
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
17
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
18
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
19
use SilverStripe\Forms\GridField\GridFieldDataColumns;
20
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
21
use SilverStripe\Forms\GridField\GridFieldPageCount;
22
use SilverStripe\Forms\GridField\GridFieldPaginator;
23
use SilverStripe\Forms\GridField\GridField;
24
use SilverStripe\Control\Controller;
25
use SilverStripe\Core\Convert;
26
use SilverStripe\ORM\PaginatedList;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\Control\RSS\RSSFeed;
29
use SilverStripe\Security\Security;
30
use SilverStripe\Core\Config\Config;
31
use SilverStripe\Control\Session;
32
use SilverStripe\Control\HTTPRequest;
33
use SilverStripe\Security\SecurityToken;
34
use SilverStripe\Logging\Log;
35
use SilverStripe\ORM\FieldType\DBDatetime;
36
use SilverStripe\Control\Director;
37
use SilverStripe\View\Parsers\BBCodeParser;
38
use SilverStripe\ORM\DataObject;
39
use SilverStripe\Forms\TextField;
40
use SilverStripe\Forms\ReadonlyField;
41
use SilverStripe\Forms\TextareaField;
42
use SilverStripe\Forms\LiteralField;
43
use SilverStripe\Forms\CheckboxField;
44
use SilverStripe\Forms\FieldList;
45
use SilverStripe\Forms\HiddenField;
46
use SilverStripe\Forms\FileField;
47
use SilverStripe\Forms\FormAction;
48
use SilverStripe\Forms\RequiredFields;
49
use SilverStripe\Forms\Form;
50
use SilverStripe\Assets\Upload;
51
use SilverStripe\ORM\ValidationException;
52
use SilverStripe\Control\Email\Email;
53
use SilverStripe\View\ArrayData;
54
use SilverStripe\ORM\FieldType\DBField;
55
use PageController;
56
use SilverStripe\ORM\DataQuery;
57
use Page;
58
use ForumCategory;
59
use ForumThread;
60
use Post;
61
use SQLQuery;
62
use ForumThread_Subscription;
63
use Post_Attachment;
64
65
/**
66
 * Forum represents a collection of forum threads. Each thread is a different topic on
67
 * the site. You can customize permissions on a per forum basis in the CMS.
68
 *
69
 * @todo Implement PermissionProvider for editing, creating forums.
70
 *
71
 * @package forum
72
 */
73
74
class Forum extends Page
75
{
76
77
    private static $allowed_children = 'none';
78
79
    private static $icon = "forum/images/treeicons/user";
80
81
    /**
82
     * Enable this to automatically notify moderators when a message is posted
83
     * or edited on his forums.
84
     */
85
    static $notify_moderators = false;
86
87
    private static $db = array(
88
        "Abstract" => "Text",
89
        "CanPostType" => "Enum('Inherit, Anyone, LoggedInUsers, OnlyTheseUsers, NoOne', 'Inherit')",
90
        "CanAttachFiles" => "Boolean",
91
    );
92
93
    private static $has_one = array(
94
        "Moderator" => "SilverStripe\\Security\\Member",
95
        "Category" => "ForumCategory"
96
    );
97
98
    private static $many_many = array(
99
        'Moderators' => 'SilverStripe\\Security\\Member',
100
        'PosterGroups' => 'SilverStripe\\Security\\Group'
101
    );
102
103
    private static $defaults = array(
104
        "ForumPosters" => "LoggedInUsers"
105
    );
106
107
    /**
108
     * Number of posts to include in the thread view before pagination takes effect.
109
     *
110
     * @var int
111
     */
112
    static $posts_per_page = 8;
113
114
    /**
115
     * When migrating from older versions of the forum it used post ID as the url token
116
     * as of forum 1.0 we now use ThreadID. If you want to enable 301 redirects from post to thread ID
117
     * set this to true
118
     *
119
     * @var bool
120
     */
121
    static $redirect_post_urls_to_thread = false;
122
123
    /**
124
     * Check if the user can view the forum.
125
     */
126
    public function canView($member = null)
127
    {
128
        if (!$member) {
129
            $member = Member::currentUser();
130
        }
131
        return (parent::canView($member) || $this->canModerate($member));
132
    }
133
134
    /**
135
     * Check if the user can post to the forum and edit his own posts.
136
     */
137
    public function canPost($member = null)
138
    {
139
        if (!$member) {
140
            $member = Member::currentUser();
141
        }
142
143
        if ($this->CanPostType == "Inherit") {
144
            $holder = $this->getForumHolder();
145
            if ($holder) {
146
                return $holder->canPost($member);
147
            }
148
149
            return false;
150
        }
151
152
        if ($this->CanPostType == "NoOne") {
153
            return false;
154
        }
155
156
        if ($this->CanPostType == "Anyone" || $this->canEdit($member)) {
157
            return true;
158
        }
159
160 View Code Duplication
        if ($member = Member::currentUser()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
161
            if ($member->IsSuspended()) {
162
                return false;
163
            }
164
            if ($member->IsBanned()) {
165
                return false;
166
            }
167
168
            if ($this->CanPostType == "LoggedInUsers") {
169
                return true;
170
            }
171
172
            if ($groups = $this->PosterGroups()) {
173
                foreach ($groups as $group) {
174
                    if ($member->inGroup($group)) {
175
                        return true;
176
                    }
177
                }
178
            }
179
        }
180
181
        return false;
182
    }
183
184
    /**
185
     * Check if user has access to moderator panel and can delete posts and threads.
186
     */
187
    public function canModerate($member = null)
188
    {
189
        if (!$member) {
190
            $member = Member::currentUser();
191
        }
192
193
        if (!$member) {
194
            return false;
195
        }
196
197
        // Admins
198
        if (Permission::checkMember($member, 'ADMIN')) {
199
            return true;
200
        }
201
202
        // Moderators
203
        if ($member->isModeratingForum($this)) {
204
            return true;
205
        }
206
207
        return false;
208
    }
209
210
    /**
211
     * Can we attach files to topics/posts inside this forum?
212
     *
213
     * @return bool Set to TRUE if the user is allowed to, to FALSE if they're
214
     *              not
215
     */
216
    public function canAttach($member = null)
217
    {
218
        return $this->CanAttachFiles ? true : false;
219
    }
220
221
    public function requireTable()
222
    {
223
        // Migrate permission columns
224
        if (DB::getConn()->hasTable('Forum')) {
225
            $fields = DB::getConn()->fieldList('Forum');
226
            if (in_array('ForumPosters', array_keys($fields)) && !in_array('CanPostType', array_keys($fields))) {
227
                DB::getConn()->renameField('Forum', 'ForumPosters', 'CanPostType');
228
                DB::alteration_message('Migrated forum permissions from "ForumPosters" to "CanPostType"', "created");
229
            }
230
        }
231
232
        parent::requireTable();
233
    }
234
235
    /**
236
     * Add default records to database
237
     *
238
     * This function is called whenever the database is built, after the
239
     * database tables have all been created.
240
     */
241
    public function requireDefaultRecords()
242
    {
243
        parent::requireDefaultRecords();
244
245
        $code = "ACCESS_FORUM";
246
        if (!($forumGroup = Group::get()->filter('Code', 'forum-members')->first())) {
247
            $group = new Group();
248
            $group->Code = 'forum-members';
249
            $group->Title = "Forum Members";
250
            $group->write();
251
252
            Permission::grant($group->ID, $code);
253
            DB::alteration_message(_t('Forum.GROUPCREATED', 'Forum Members group created'), 'created');
254
        } elseif (!Permission::get()->filter(array('GroupID' => $forumGroup->ID, 'Code' => $code))->exists()) {
255
            Permission::grant($forumGroup->ID, $code);
256
        }
257
258
        if (!($category = ForumCategory::get()->first())) {
259
            $category = new ForumCategory();
260
            $category->Title = _t('Forum.DEFAULTCATEGORY', 'General');
261
            $category->write();
262
        }
263
264
        if (!ForumHolder::get()->exists()) {
265
            $forumholder = new ForumHolder();
266
            $forumholder->Title = "Forums";
267
            $forumholder->URLSegment = "forums";
268
            $forumholder->Content = "<p>"._t('Forum.WELCOMEFORUMHOLDER', 'Welcome to SilverStripe Forum Module! This is the default ForumHolder page. You can now add forums.')."</p>";
269
            $forumholder->Status = "Published";
270
            $forumholder->write();
271
            $forumholder->publish("Stage", "Live");
272
            DB::alteration_message(_t('Forum.FORUMHOLDERCREATED', 'ForumHolder page created'), "created");
273
274
            $forum = new Forum();
275
            $forum->Title = _t('Forum.TITLE', 'General Discussion');
276
            $forum->URLSegment = "general-discussion";
277
            $forum->ParentID = $forumholder->ID;
278
            $forum->Content = "<p>"._t('Forum.WELCOMEFORUM', 'Welcome to SilverStripe Forum Module! This is the default Forum page. You can now add topics.')."</p>";
279
            $forum->Status = "Published";
280
            $forum->CategoryID = $category->ID;
281
            $forum->write();
282
            $forum->publish("Stage", "Live");
283
284
            DB::alteration_message(_t('Forum.FORUMCREATED', 'Forum page created'), "created");
285
        }
286
    }
287
288
    /**
289
     * Check if we can and should show forums in categories
290
     */
291
    public function getShowInCategories()
292
    {
293
        $holder = $this->getForumHolder();
294
        if ($holder) {
295
            return $holder->getShowInCategories();
296
        }
297
    }
298
299
    /**
300
     * Returns a FieldList with which to create the CMS editing form
301
     *
302
     * @return FieldList The fields to be displayed in the CMS.
303
     */
304
    public function getCMSFields()
305
    {
306
        $self = $this;
307
308
        $this->beforeUpdateCMSFields(function ($fields) use ($self) {
309
            Requirements::javascript("forum/javascript/ForumAccess.js");
310
            Requirements::css("forum/css/Forum_CMS.css");
311
312
            $fields->addFieldToTab("Root.Access", new HeaderField(_t('Forum.ACCESSPOST', 'Who can post to the forum?'), 2));
313
            $fields->addFieldToTab("Root.Access", $optionSetField = new OptionsetField("CanPostType", "", array(
314
                "Inherit" => "Inherit",
315
                "Anyone" => _t('Forum.READANYONE', 'Anyone'),
316
                "LoggedInUsers" => _t('Forum.READLOGGEDIN', 'Logged-in users'),
317
                "OnlyTheseUsers" => _t('Forum.READLIST', 'Only these people (choose from list)'),
318
                "NoOne" => _t('Forum.READNOONE', 'Nobody. Make Forum Read Only')
319
            )));
320
321
            $optionSetField->addExtraClass('ForumCanPostTypeSelector');
322
323
            $fields->addFieldsToTab("Root.Access", array(
324
                new TreeMultiselectField("PosterGroups", _t('Forum.GROUPS', "Groups")),
325
                new OptionsetField("CanAttachFiles", _t('Forum.ACCESSATTACH', 'Can users attach files?'), array(
326
                    "1" => _t('Forum.YES', 'Yes'),
327
                    "0" => _t('Forum.NO', 'No')
328
                ))
329
            ));
330
331
332
            //Dropdown of forum category selection.
333
            $categories = ForumCategory::get()->map();
334
335
            $fields->addFieldsToTab(
336
                "Root.Main",
337
                DropdownField::create('CategoryID', _t('Forum.FORUMCATEGORY', 'Forum Category'), $categories),
338
                'Content'
339
            );
340
341
            //GridField Config - only need to attach or detach Moderators with existing Member accounts.
342
            $moderatorsConfig = GridFieldConfig::create()
343
                ->addComponent(new GridFieldButtonRow('before'))
344
                ->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right'))
345
                ->addComponent(new GridFieldToolbarHeader())
346
                ->addComponent($sort = new GridFieldSortableHeader())
347
                ->addComponent($columns = new GridFieldDataColumns())
348
                ->addComponent(new GridFieldDeleteAction(true))
349
                ->addComponent(new GridFieldPageCount('toolbar-header-right'))
350
                ->addComponent($pagination = new GridFieldPaginator());
351
352
            // Use GridField for Moderator management
353
            $moderators = GridField::create(
354
                'Moderators',
355
                _t('MODERATORS', 'Moderators for this forum'),
356
                $self->Moderators(),
357
                $moderatorsConfig
358
            );
359
360
            $columns->setDisplayFields(array(
361
                'Nickname' => 'Nickname',
362
                'FirstName' => 'First name',
363
                'Surname' => 'Surname',
364
                'Email'=> 'SilverStripe\\Control\\Email\\Email',
365
                'LastVisited.Long' => 'Last Visit'
366
            ));
367
368
            $sort->setThrowExceptionOnBadDataType(false);
369
            $pagination->setThrowExceptionOnBadDataType(false);
370
371
            $fields->addFieldToTab('Root.Moderators', $moderators);
372
        });
373
374
        $fields = parent::getCMSFields();
375
376
        return $fields;
377
    }
378
379
    /**
380
     * Create breadcrumbs
381
     *
382
     * @param int $maxDepth Maximal lenght of the breadcrumb navigation
383
     * @param bool $unlinked Set to TRUE if the breadcrumb should consist of
384
     *                       links, otherwise FALSE.
385
     * @param bool $stopAtPageType Currently not used
386
     * @param bool $showHidden Set to TRUE if also hidden pages should be
387
     *                         displayed
388
     * @return string HTML code to display breadcrumbs
389
     */
390
    public function Breadcrumbs($maxDepth = null, $unlinked = false, $stopAtPageType = false, $showHidden = false)
391
    {
392
        $page = $this;
393
        $nonPageParts = array();
394
        $parts = array();
395
396
        $controller = Controller::curr();
397
        $params = $controller->getURLParams();
398
399
        $forumThreadID = $params['ID'];
400
        if (is_numeric($forumThreadID)) {
401
            if ($topic = ForumThread::get()->byID($forumThreadID)) {
402
                $nonPageParts[] = Convert::raw2xml($topic->getTitle());
403
            }
404
        }
405
406
        while ($page && (!$maxDepth || sizeof($parts) < $maxDepth)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $maxDepth of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
407
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
408
                if ($page->URLSegment == 'home') {
409
                    $hasHome = true;
0 ignored issues
show
Unused Code introduced by
$hasHome is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
410
                }
411
412
                if ($nonPageParts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nonPageParts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
413
                    $parts[] = '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
414
                } else {
415
                    $parts[] = (($page->ID == $this->ID) || $unlinked)
416
                            ? Convert::raw2xml($page->Title)
417
                            : '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
418
                }
419
            }
420
421
            $page = $page->Parent;
422
        }
423
424
        return implode(" &raquo; ", array_reverse(array_merge($nonPageParts, $parts)));
425
    }
426
427
    /**
428
     * Helper Method from the template includes. Uses $ForumHolder so in order for it work
429
     * it needs to be included on this page
430
     *
431
     * @return ForumHolder
432
     */
433
    public function getForumHolder()
434
    {
435
        $holder = $this->Parent();
436
        if ($holder->ClassName=='ForumHolder') {
437
            return $holder;
438
        }
439
    }
440
441
    /**
442
     * Get the latest posting of the forum. For performance the forum ID is stored on the
443
     * {@link Post} object as well as the {@link Forum} object
444
     *
445
     * @return Post
446
     */
447
    public function getLatestPost()
448
    {
449
        return Post::get()->filter('ForumID', $this->ID)->sort('"Post"."ID" DESC')->first();
450
    }
451
452
    /**
453
     * Get the number of total topics (threads) in this Forum
454
     *
455
     * @return int Returns the number of topics (threads)
456
     */
457
    public function getNumTopics()
458
    {
459
        $sqlQuery = new SQLQuery();
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
460
        $sqlQuery->setFrom('"Post"');
461
        $sqlQuery->setSelect('COUNT(DISTINCT("ThreadID"))');
462
        $sqlQuery->addInnerJoin('SilverStripe\\Security\\Member', '"Post"."AuthorID" = "Member"."ID"');
463
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
464
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
465
        return $sqlQuery->execute()->value();
466
    }
467
468
    /**
469
     * Get the number of total posts
470
     *
471
     * @return int Returns the number of posts
472
     */
473
    public function getNumPosts()
474
    {
475
        $sqlQuery = new SQLQuery();
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
476
        $sqlQuery->setFrom('"Post"');
477
        $sqlQuery->setSelect('COUNT("Post"."ID")');
478
        $sqlQuery->addInnerJoin('SilverStripe\\Security\\Member', '"Post"."AuthorID" = "Member"."ID"');
479
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
480
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
481
        return $sqlQuery->execute()->value();
482
    }
483
484
485
    /**
486
     * Get the number of distinct Authors
487
     *
488
     * @return int
489
     */
490
    public function getNumAuthors()
491
    {
492
        $sqlQuery = new SQLQuery();
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
493
        $sqlQuery->setFrom('"Post"');
494
        $sqlQuery->setSelect('COUNT(DISTINCT("AuthorID"))');
495
        $sqlQuery->addInnerJoin('SilverStripe\\Security\\Member', '"Post"."AuthorID" = "Member"."ID"');
496
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
497
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
498
        return $sqlQuery->execute()->value();
499
    }
500
501
    /**
502
     * Returns the Topics (the first Post of each Thread) for this Forum
503
     * @return DataList
504
     */
505
    public function getTopics()
506
    {
507
        // Get a list of Posts
508
        $posts = Post::get();
509
510
        // Get the underlying query and change it to return the ThreadID and Max(Created) and Max(ID) for each thread
511
        // of those posts
512
        $postQuery = $posts->dataQuery()->query();
513
514
        $postQuery
515
            ->setSelect(array())
516
            ->selectField('MAX("Post"."Created")', 'PostCreatedMax')
517
            ->selectField('MAX("Post"."ID")', 'PostIDMax')
518
            ->selectField('"ThreadID"')
519
            ->setGroupBy('"ThreadID"')
520
            ->addWhere(sprintf('"ForumID" = \'%s\'', $this->ID))
521
            ->setDistinct(false);
522
523
        // Get a list of forum threads inside this forum that aren't sticky
524
        $threads = ForumThread::get()->filter(array(
525
            'ForumID' => $this->ID,
526
            'IsGlobalSticky' => 0,
527
            'IsSticky' => 0
528
        ));
529
530
        // Get the underlying query and change it to inner join on the posts list to just show threads that
531
        // have approved (and maybe awaiting) posts, and sort the threads by the most recent post
532
        $threadQuery = $threads->dataQuery()->query();
533
        $threadQuery
534
            ->addSelect(array('"PostMax"."PostCreatedMax", "PostMax"."PostIDMax"'))
535
            ->addFrom('INNER JOIN ('.$postQuery->sql().') AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")')
536
            ->addOrderBy(array('"PostMax"."PostCreatedMax" DESC', '"PostMax"."PostIDMax" DESC'))
537
            ->setDistinct(false);
538
539
        // Alter the forum threads list to use the new query
540
        $threads = $threads->setDataQuery(new Forum_DataQuery('ForumThread', $threadQuery));
541
542
        // And return the results
543
        return $threads->exists() ? new PaginatedList($threads, $_GET) : null;
544
    }
545
546
547
548
    /*
549
	 * Returns the Sticky Threads
550
	 * @param boolean $include_global Include Global Sticky Threads in the results (default: true)
551
	 * @return DataList
552
	 */
553
    public function getStickyTopics($include_global = true)
554
    {
555
        // Get Threads that are sticky & in this forum
556
        $where = '("ForumThread"."ForumID" = '.$this->ID.' AND "ForumThread"."IsSticky" = 1)';
557
        // Get Threads that are globally sticky
558
        if ($include_global) {
559
            $where .= ' OR ("ForumThread"."IsGlobalSticky" = 1)';
560
        }
561
562
        // Get the underlying query
563
        $query = ForumThread::get()->where($where)->dataQuery()->query();
564
565
        // Sort by the latest Post in each thread's Created date
566
        $query
567
          ->addSelect('"PostMax"."PostMax"')
568
          // TODO: Confirm this works in non-MySQL DBs
569
          ->addFrom(sprintf(
570
              'LEFT JOIN (SELECT MAX("Created") AS "PostMax", "ThreadID" FROM "Post" WHERE "ForumID" = \'%s\' GROUP BY "ThreadID") AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")',
571
              $this->ID
572
          ))
573
          ->addOrderBy('"PostMax"."PostMax" DESC')
574
          ->setDistinct(false);
575
576
       // Build result as ArrayList
577
        $res = new ArrayList();
578
        $rows = $query->execute();
579
        if ($rows) {
580
            foreach ($rows as $row) {
581
                $res->push(new ForumThread($row));
582
            }
583
        }
584
585
        return $res;
586
    }
587
}
588
589
/**
590
 * The forum controller class
591
 *
592
 * @package forum
593
 */
594
class Forum_Controller extends PageController
595
{
596
597
    private static $allowed_actions = array(
598
        'AdminFormFeatures',
599
        'deleteattachment',
600
        'deletepost',
601
        'editpost',
602
        'markasspam',
603
        'PostMessageForm',
604
        'reply',
605
        'show',
606
        'starttopic',
607
        'subscribe',
608
        'unsubscribe',
609
        'rss',
610
        'ban',
611
        'ghost'
612
    );
613
614
615
    public function init()
616
    {
617
        parent::init();
618
        if ($this->redirectedTo()) {
619
            return;
620
        }
621
622
        Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
623
        Requirements::javascript("forum/javascript/Forum.js");
624
        Requirements::javascript("forum/javascript/jquery.MultiFile.js");
625
626
        Requirements::themedCSS('Forum', 'forum', 'all');
627
628
        RSSFeed::linkToFeed($this->Parent()->Link("rss/forum/$this->ID"), sprintf(_t('Forum.RSSFORUM', "Posts to the '%s' forum"), $this->Title));
629
        RSSFeed::linkToFeed($this->Parent()->Link("rss"), _t('Forum.RSSFORUMS', 'Posts to all forums'));
630
631
        if (!$this->canView()) {
632
            $messageSet = array(
633
                'default' => _t('Forum.LOGINDEFAULT', 'Enter your email address and password to view this forum.'),
634
                'alreadyLoggedIn' => _t('Forum.LOGINALREADY', 'I&rsquo;m sorry, but you can&rsquo;t access this forum until you&rsquo;ve logged in. If you want to log in as someone else, do so below'),
635
                '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.')
636
            );
637
638
            Security::permissionFailure($this, $messageSet);
639
            return;
640
        }
641
642
        // Log this visit to the ForumMember if they exist
643
        $member = Member::currentUser();
644
        if ($member && Config::inst()->get('ForumHolder', 'currently_online_enabled')) {
645
            $member->LastViewed = date("Y-m-d H:i:s");
646
            $member->write();
647
        }
648
649
        // Set the back url
650 View Code Duplication
        if (isset($_SERVER['REQUEST_URI'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
651
            Session::set('BackURL', $_SERVER['REQUEST_URI']);
652
        } else {
653
            Session::set('BackURL', $this->Link());
654
        }
655
    }
656
657
    /**
658
     * A convenience function which provides nice URLs for an rss feed on this forum.
659
     */
660
    public function rss()
661
    {
662
        $this->redirect($this->Parent()->Link("rss/forum/$this->ID"), 301);
663
    }
664
665
    /**
666
     * Is OpenID support available?
667
     *
668
     * This method checks if the {@link OpenIDAuthenticator} is available and
669
     * registered.
670
     *
671
     * @return bool Returns TRUE if OpenID is available, FALSE otherwise.
672
     */
673
    public function OpenIDAvailable()
674
    {
675
        return $this->Parent()->OpenIDAvailable();
676
    }
677
678
    /**
679
     * Subscribe a user to a thread given by an ID.
680
     *
681
     * Designed to be called via AJAX so return true / false
682
     *
683
     * @return bool
684
     */
685
    public function subscribe(HTTPRequest $request)
686
    {
687
        // Check CSRF
688
        if (!SecurityToken::inst()->checkRequest($request)) {
689
            return $this->httpError(400);
690
        }
691
692
        if (Member::currentUser() && !ForumThread_Subscription::already_subscribed($this->urlParams['ID'])) {
693
            $obj = new ForumThread_Subscription();
694
            $obj->ThreadID = (int) $this->urlParams['ID'];
695
            $obj->MemberID = Member::currentUserID();
696
            $obj->LastSent = date("Y-m-d H:i:s");
697
            $obj->write();
698
699
            die('1');
700
        }
701
702
        return false;
703
    }
704
705
    /**
706
     * Unsubscribe a user from a thread by an ID
707
     *
708
     * Designed to be called via AJAX so return true / false
709
     *
710
     * @return bool
711
     */
712
    public function unsubscribe(HTTPRequest $request)
713
    {
714
        $member = Member::currentUser();
715
716
        if (!$member) {
717
            Security::permissionFailure($this, _t('LOGINTOUNSUBSCRIBE', 'To unsubscribe from that thread, please log in first.'));
718
        }
719
720
        if (ForumThread_Subscription::already_subscribed($this->urlParams['ID'], $member->ID)) {
721
            DB::query("
722
				DELETE FROM \"ForumThread_Subscription\"
723
				WHERE \"ThreadID\" = '". Convert::raw2sql($this->urlParams['ID']) ."'
724
				AND \"MemberID\" = '$member->ID'");
725
726
            die('1');
727
        }
728
729
        return false;
730
    }
731
732
    /**
733
     * Mark a post as spam. Deletes any posts or threads created by that user
734
     * and removes their user account from the site
735
     *
736
     * Must be logged in and have the correct permissions to do marking
737
     */
738
    public function markasspam(HTTPRequest $request)
739
    {
740
        $currentUser = Member::currentUser();
741
        if (!isset($this->urlParams['ID'])) {
742
            return $this->httpError(400);
743
        }
744
        if (!$this->canModerate()) {
745
            return $this->httpError(403);
746
        }
747
748
        // Check CSRF token
749
        if (!SecurityToken::inst()->checkRequest($request)) {
750
            return $this->httpError(400);
751
        }
752
753
        $post = Post::get()->byID($this->urlParams['ID']);
754
        if ($post) {
755
            // post was the start of a thread, Delete the whole thing
756
            if ($post->isFirstPost()) {
757
                $post->Thread()->delete();
758
            }
759
760
            // Delete the current post
761
            $post->delete();
762
            $post->extend('onAfterMarkAsSpam');
763
764
            // Log deletion event
765
            Log::log(sprintf(
766
                'Marked post #%d as spam, by moderator %s (#%d)',
767
                $post->ID,
768
                $currentUser->Email,
769
                $currentUser->ID
770
            ), Log::NOTICE);
771
772
            // Suspend the member (rather than deleting him),
773
            // which gives him or a moderator the chance to revoke a decision.
774
            if ($author = $post->Author()) {
775
                $author->SuspendedUntil = date('Y-m-d', strtotime('+99 years', DBDatetime::now()->Format('U')));
776
                $author->write();
777
            }
778
779
            Log::log(sprintf(
780
                'Suspended member %s (#%d) for spam activity, by moderator %s (#%d)',
781
                $author->Email,
782
                $author->ID,
783
                $currentUser->Email,
784
                $currentUser->ID
785
            ), Log::NOTICE);
786
        }
787
788
        return (Director::is_ajax()) ? true : $this->redirect($this->Link());
789
    }
790
791
792 View Code Duplication
    public function ban(HTTPRequest $r)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
793
    {
794
        if (!$r->param('ID')) {
795
            return $this->httpError(404);
796
        }
797
        if (!$this->canModerate()) {
798
            return $this->httpError(403);
799
        }
800
801
        $member = Member::get()->byID($r->param('ID'));
802
        if (!$member || !$member->exists()) {
803
            return $this->httpError(404);
804
        }
805
806
        $member->ForumStatus = 'Banned';
807
        $member->write();
808
809
        // Log event
810
        $currentUser = Member::currentUser();
811
        Log::log(sprintf(
812
            'Banned member %s (#%d), by moderator %s (#%d)',
813
            $member->Email,
814
            $member->ID,
815
            $currentUser->Email,
816
            $currentUser->ID
817
        ), Log::NOTICE);
818
819
        return ($r->isAjax()) ? true : $this->redirectBack();
820
    }
821
822 View Code Duplication
    public function ghost(HTTPRequest $r)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
823
    {
824
        if (!$r->param('ID')) {
825
            return $this->httpError(400);
826
        }
827
        if (!$this->canModerate()) {
828
            return $this->httpError(403);
829
        }
830
831
        $member = Member::get()->byID($r->param('ID'));
832
        if (!$member || !$member->exists()) {
833
            return $this->httpError(404);
834
        }
835
836
        $member->ForumStatus = 'Ghost';
837
        $member->write();
838
839
        // Log event
840
        $currentUser = Member::currentUser();
841
        Log::log(sprintf(
842
            'Ghosted member %s (#%d), by moderator %s (#%d)',
843
            $member->Email,
844
            $member->ID,
845
            $currentUser->Email,
846
            $currentUser->ID
847
        ), Log::NOTICE);
848
849
        return ($r->isAjax()) ? true : $this->redirectBack();
850
    }
851
852
    /**
853
     * Get posts to display. This method assumes an URL parameter "ID" which contains the thread ID.
854
     * @param string sortDirection The sort order direction, either ASC for ascending (default) or DESC for descending
855
     * @return DataObjectSet Posts
856
     */
857
    public function Posts($sortDirection = "ASC")
858
    {
859
        $numPerPage = Forum::$posts_per_page;
0 ignored issues
show
Bug introduced by
The property posts_per_page cannot be accessed from this context as it is declared private in class SilverStripe\Forum\Pages\Forum.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
860
861
        $posts = Post::get()
862
            ->filter('ThreadID', $this->urlParams['ID'])
863
            ->sort('Created', $sortDirection);
864
865
        if (isset($_GET['showPost']) && !isset($_GET['start'])) {
866
            $postIDList = clone $posts;
867
            $postIDList = $postIDList->select('ID')->toArray();
868
869
            if ($postIDList->exists()) {
870
                $foundPos = array_search($_GET['showPost'], $postIDList);
871
                $_GET['start'] = floor($foundPos / $numPerPage) * $numPerPage;
872
            }
873
        }
874
875
        if (!isset($_GET['start'])) {
876
            $_GET['start'] = 0;
877
        }
878
879
        $member = Member::currentUser();
880
881
        /*
882
		 * Don't show posts of banned or ghost members, unless current Member
883
		 * is a ghost member and owner of current post
884
		 */
885
886
        $posts = $posts->exclude(array(
887
            'Author.ForumStatus' => 'Banned'
888
        ));
889
890
        if ($member) {
891
            $posts = $posts->exclude(array(
892
                'Author.ForumStatus' => 'Ghost',
893
                'Author.ID:not' => $member->ID
894
            ));
895
        } else {
896
            $posts = $posts->exclude(array(
897
                'Author.ForumStatus' => 'Ghost'
898
            ));
899
        }
900
901
        $paginated = new PaginatedList($posts, $_GET);
902
        $paginated->setPageLength(Forum::$posts_per_page);
0 ignored issues
show
Bug introduced by
The property posts_per_page cannot be accessed from this context as it is declared private in class SilverStripe\Forum\Pages\Forum.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
903
        return $paginated;
904
    }
905
906
    /**
907
     * Get the usable BB codes
908
     *
909
     * @return DataObjectSet Returns the usable BB codes
910
     * @see BBCodeParser::usable_tags()
911
     */
912
    public function BBTags()
913
    {
914
        return BBCodeParser::usable_tags();
915
    }
916
917
    /**
918
     * Section for dealing with reply / edit / create threads form
919
     *
920
     * @return Form Returns the post message form
921
     */
922
    public function PostMessageForm($addMode = false, $post = false)
923
    {
924
        $thread = false;
925
926
        if ($post) {
927
            $thread = $post->Thread();
0 ignored issues
show
Bug introduced by
The method Thread cannot be called on $post (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
928
        } elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
929
            $thread = DataObject::get_by_id('ForumThread', $this->urlParams['ID']);
930
        }
931
932
        // Check permissions
933
        $messageSet = array(
934
            'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'),
935
            'alreadyLoggedIn' => _t(
936
                'Forum.LOGINTOPOSTLOGGEDIN',
937
                'I\'m sorry, but you can\'t post to this forum until you\'ve logged in.'
938
                .'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.'
939
            ),
940
            '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.'),
941
        );
942
943
        // Creating new thread
944
        if ($addMode && !$this->canPost()) {
945
            Security::permissionFailure($this, $messageSet);
946
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Pages...roller::PostMessageForm of type SilverStripe\Forms\Form.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
947
        }
948
949
        // Replying to existing thread
950 View Code Duplication
        if (!$addMode && !$post && $thread && !$thread->canPost()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
951
            Security::permissionFailure($this, $messageSet);
952
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Pages...roller::PostMessageForm of type SilverStripe\Forms\Form.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
953
        }
954
955
        // Editing existing post
956
        if (!$addMode && $post && !$post->canEdit()) {
0 ignored issues
show
Bug introduced by
The method canEdit cannot be called on $post (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
957
            Security::permissionFailure($this, $messageSet);
958
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Pages...roller::PostMessageForm of type SilverStripe\Forms\Form.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
959
        }
960
961
        $forumBBCodeHint = $this->renderWith('Forum_BBCodeHint');
962
963
        $fields = new FieldList(
964
            ($post && $post->isFirstPost() || !$thread) ? new TextField("Title", _t('Forum.FORUMTHREADTITLE', 'Title')) : new ReadonlyField('Title', _t('Forum.FORUMTHREADTITLE', ''), 'Re:'. $thread->Title),
0 ignored issues
show
Bug introduced by
The method isFirstPost cannot be called on $post (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
965
            new TextareaField("Content", _t('Forum.FORUMREPLYCONTENT', 'Content')),
966
            new LiteralField(
967
                "BBCodeHelper",
968
                "<div class=\"BBCodeHint\">[ <a href=\"#BBTagsHolder\" id=\"BBCodeHint\">" .
969
                _t('Forum.BBCODEHINT', 'View Formatting Help') .
970
                "</a> ]</div>" .
971
                $forumBBCodeHint
972
            ),
973
            new CheckboxField(
974
                "TopicSubscription",
975
                _t('Forum.SUBSCRIBETOPIC', 'Subscribe to this topic (Receive email notifications when a new reply is added)'),
976
                ($thread) ? $thread->getHasSubscribed() : false
977
            )
978
        );
979
980
        if ($thread) {
981
            $fields->push(new HiddenField('ThreadID', 'ThreadID', $thread->ID));
982
        }
983
        if ($post) {
984
            $fields->push(new HiddenField('ID', 'ID', $post->ID));
985
        }
986
987
        // Check if we can attach files to this forum's posts
988
        if ($this->canAttach()) {
989
            $fields->push(FileField::create("Attachment", _t('Forum.ATTACH', 'Attach file')));
990
        }
991
992
        // If this is an existing post check for current attachments and generate
993
        // a list of the uploaded attachments
994
        if ($post && $attachmentList = $post->Attachments()) {
0 ignored issues
show
Bug introduced by
The method Attachments cannot be called on $post (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
995
            if ($attachmentList->exists()) {
996
                $attachments = "<div id=\"CurrentAttachments\"><h4>". _t('Forum.CURRENTATTACHMENTS', 'Current Attachments') ."</h4><ul>";
997
                $link = $this->Link();
998
                // An instance of the security token
999
                $token = SecurityToken::inst();
1000
1001
                foreach ($attachmentList as $attachment) {
1002
                    // Generate a link properly, since it requires a security token
1003
                    $attachmentLink = Controller::join_links($link, 'deleteattachment', $attachment->ID);
1004
                    $attachmentLink = $token->addToUrl($attachmentLink);
1005
1006
                    $attachments .= "<li class='attachment-$attachment->ID'>$attachment->Name [<a href='{$attachmentLink}' rel='$attachment->ID' class='deleteAttachment'>"
1007
                            . _t('Forum.REMOVE', 'remove') . "</a>]</li>";
1008
                }
1009
                $attachments .= "</ul></div>";
1010
1011
                $fields->push(new LiteralField('CurrentAttachments', $attachments));
1012
            }
1013
        }
1014
1015
        $actions = new FieldList(
1016
            new FormAction("doPostMessageForm", _t('Forum.REPLYFORMPOST', 'Post'))
1017
        );
1018
1019
        $required = $addMode === true ? new RequiredFields("Title", "Content") : new RequiredFields("Content");
1020
1021
        $form = new Form($this, 'PostMessageForm', $fields, $actions, $required);
1022
1023
        $this->extend('updatePostMessageForm', $form, $post);
1024
1025
        if ($post) {
1026
            $form->loadDataFrom($post);
1027
        }
1028
1029
        return $form;
1030
    }
1031
1032
    /**
1033
     * Wrapper for older templates. Previously the new, reply and edit forms were 3 separate
1034
     * forms, they have now been refactored into 1 form. But in order to not break existing
1035
     * themes too much just include this.
1036
     *
1037
     * @deprecated 0.5
1038
     * @return Form
1039
     */
1040
    public function ReplyForm()
1041
    {
1042
        user_error('Please Use $PostMessageForm in your template rather that $ReplyForm', E_USER_WARNING);
1043
1044
        return $this->PostMessageForm();
1045
    }
1046
1047
    /**
1048
     * Post a message to the forum. This method is called whenever you want to make a
1049
     * new post or edit an existing post on the forum
1050
     *
1051
     * @param Array - Data
1052
     * @param Form - Submitted Form
1053
     */
1054
    public function doPostMessageForm($data, $form)
1055
    {
1056
        $member = Member::currentUser();
1057
1058
        //Allows interception of a Member posting content to perform some action before the post is made.
1059
        $this->extend('beforePostMessage', $data, $member);
1060
1061
        $content = (isset($data['Content'])) ? $this->filterLanguage($data["Content"]) : "";
1062
        $title = (isset($data['Title'])) ? $this->filterLanguage($data["Title"]) : false;
1063
1064
        // If a thread id is passed append the post to the thread. Otherwise create
1065
        // a new thread
1066
        $thread = false;
1067
        if (isset($data['ThreadID'])) {
1068
            $thread = DataObject::get_by_id('ForumThread', $data['ThreadID']);
1069
        }
1070
1071
        // If this is a simple edit the post then handle it here. Look up the correct post,
1072
        // make sure we have edit rights to it then update the post
1073
        $post = false;
1074
        if (isset($data['ID'])) {
1075
            $post = DataObject::get_by_id('Post', $data['ID']);
1076
1077
            if ($post && $post->isFirstPost()) {
1078
                if ($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1079
                    $thread->Title = $title;
1080
                }
1081
            }
1082
        }
1083
1084
1085
        // Check permissions
1086
        $messageSet = array(
1087
            'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'),
1088
            'alreadyLoggedIn' => _t('Forum.NOPOSTPERMISSION', 'I\'m sorry, but you do not have permission post to this forum.'),
1089
            '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.'),
1090
        );
1091
1092
        // Creating new thread
1093
        if (!$thread && !$this->canPost()) {
1094
            Security::permissionFailure($this, $messageSet);
1095
            return false;
1096
        }
1097
1098
        // Replying to existing thread
1099 View Code Duplication
        if ($thread && !$post && !$thread->canPost()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1100
            Security::permissionFailure($this, $messageSet);
1101
            return false;
1102
        }
1103
1104
        // Editing existing post
1105
        if ($thread && $post && !$post->canEdit()) {
1106
            Security::permissionFailure($this, $messageSet);
1107
            return false;
1108
        }
1109
1110
        if (!$thread) {
1111
            $thread = new ForumThread();
1112
            $thread->ForumID = $this->ID;
1113
            if ($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1114
                $thread->Title = $title;
1115
            }
1116
            $starting_thread = true;
1117
        }
1118
1119
        // Upload and Save all files attached to the field
1120
        // Attachment will always be blank, If they had an image it will be at least in Attachment-0
1121
        //$attachments = new DataObjectSet();
1122
        $attachments = new ArrayList();
1123
1124
        if (!empty($data['Attachment-0']) && !empty($data['Attachment-0']['tmp_name'])) {
1125
            $id = 0;
1126
            //
1127
            // @todo this only supports ajax uploads. Needs to change the key (to simply Attachment).
1128
            //
1129
            while (isset($data['Attachment-' . $id])) {
1130
                $image = $data['Attachment-' . $id];
1131
1132
                if ($image && !empty($image['tmp_name'])) {
1133
                    $file = Post_Attachment::create();
1134
                    $file->OwnerID = Member::currentUserID();
1135
                    $folder = Config::inst()->get('ForumHolder', 'attachments_folder');
1136
1137
                    try {
1138
                        $upload = Upload::create()->loadIntoFile($image, $file, $folder);
0 ignored issues
show
Unused Code introduced by
$upload is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1139
                        $file->write();
1140
                        $attachments->push($file);
1141
                    } catch (ValidationException $e) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\ORM\ValidationException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1142
                        $message = _t('Forum.UPLOADVALIDATIONFAIL', 'Unallowed file uploaded. Please only upload files of the following: ');
1143
                        $message .= implode(', ', Config::inst()->get('SilverStripe\\Assets\\File', 'allowed_extensions'));
1144
                        $form->addErrorMessage('Attachment', $message, 'bad');
1145
1146
                        Session::set("FormInfo.Form_PostMessageForm.data", $data);
1147
1148
                        return $this->redirectBack();
1149
                    }
1150
                }
1151
1152
                $id++;
1153
            }
1154
        }
1155
1156
        // from now on the user has the correct permissions. save the current thread settings
1157
        $thread->write();
1158
1159
        if (!$post || !$post->canEdit()) {
1160
            $post = new Post();
1161
            $post->AuthorID = ($member) ? $member->ID : 0;
1162
            $post->ThreadID = $thread->ID;
1163
        }
1164
1165
        $post->ForumID = $thread->ForumID;
1166
        $post->Content = $content;
1167
        $post->write();
1168
1169
1170
        if ($attachments) {
1171
            foreach ($attachments as $attachment) {
1172
                $attachment->PostID = $post->ID;
1173
                $attachment->write();
1174
            }
1175
        }
1176
1177
        // Add a topic subscription entry if required
1178
        $isSubscribed = ForumThread_Subscription::already_subscribed($thread->ID);
1179
        if (isset($data['TopicSubscription'])) {
1180
            if (!$isSubscribed) {
1181
                // Create a new topic subscription for this member
1182
                $obj = new ForumThread_Subscription();
1183
                $obj->ThreadID = $thread->ID;
1184
                $obj->MemberID = Member::currentUserID();
1185
                $obj->write();
1186
            }
1187
        } elseif ($isSubscribed) {
1188
            // See if the member wanted to remove themselves
1189
            DB::query("DELETE FROM \"ForumThread_Subscription\" WHERE \"ThreadID\" = '$post->ThreadID' AND \"MemberID\" = '$member->ID'");
1190
        }
1191
1192
        // Send any notifications that need to be sent
1193
        ForumThread_Subscription::notify($post);
1194
1195
        // Send any notifications to moderators of the forum
1196
        if (Forum::$notify_moderators) {
0 ignored issues
show
Bug introduced by
The property notify_moderators cannot be accessed from this context as it is declared private in class SilverStripe\Forum\Pages\Forum.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
1197
            if (isset($starting_thread) && $starting_thread) {
1198
                $this->notifyModerators($post, $thread, true);
1199
            } else {
1200
                $this->notifyModerators($post, $thread);
1201
            }
1202
        }
1203
1204
        return $this->redirect($post->Link());
1205
    }
1206
1207
    /**
1208
     * Send email to moderators notifying them the thread has been created or post added/edited.
1209
     */
1210
    public function notifyModerators($post, $thread, $starting_thread = false)
1211
    {
1212
        $moderators = $this->Moderators();
1213
        if ($moderators && $moderators->exists()) {
1214
            foreach ($moderators as $moderator) {
1215
                if ($moderator->Email) {
1216
                    $adminEmail = Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'admin_email');
1217
1218
                    $email = new Email();
1219
                    $email->setFrom($adminEmail);
1220
                    $email->setTo($moderator->Email);
1221
                    if ($starting_thread) {
1222
                        $email->setSubject('New thread "' . $thread->Title . '" in forum ['. $this->Title.']');
1223
                    } else {
1224
                        $email->setSubject('New post "' . $post->Title. '" in forum ['.$this->Title.']');
1225
                    }
1226
                    $email->setTemplate('ForumMember_NotifyModerator');
1227
                    $email->populateTemplate(new ArrayData(array(
1228
                        'NewThread' => $starting_thread,
1229
                        'Moderator' => $moderator,
1230
                        'Author' => $post->Author(),
1231
                        'Forum' => $this,
1232
                        'Post' => $post
1233
                    )));
1234
1235
                    $email->send();
1236
                }
1237
            }
1238
        }
1239
    }
1240
1241
    /**
1242
     * Return the Forbidden Words in this Forum
1243
     *
1244
     * @return Text
1245
     */
1246
    public function getForbiddenWords()
1247
    {
1248
        return $this->Parent()->ForbiddenWords;
1249
    }
1250
1251
    /**
1252
    * This function filters $content by forbidden words, entered in forum holder.
1253
    *
1254
    * @param String $content (it can be Post Content or Post Title)
1255
    * @return String $content (filtered string)
1256
    */
1257
    public function filterLanguage($content)
1258
    {
1259
        $words = $this->getForbiddenWords();
1260
        if ($words != "") {
1261
            $words = explode(",", $words);
1262
            foreach ($words as $word) {
1263
                $content = str_ireplace(trim($word), "*", $content);
1264
            }
1265
        }
1266
1267
        return $content;
1268
    }
1269
1270
    /**
1271
     * Get the link for the reply action
1272
     *
1273
     * @return string URL for the reply action
1274
     */
1275
    public function ReplyLink()
1276
    {
1277
        return $this->Link() . 'reply/' . $this->urlParams['ID'];
1278
    }
1279
1280
    /**
1281
     * Show will get the selected thread to the user. Also increments the forums view count.
1282
     *
1283
     * If the thread does not exist it will pass the user to the 404 error page
1284
     *
1285
     * @return array|SS_HTTPResponse_Exception
1286
     */
1287
    public function show()
1288
    {
1289
        $title = Convert::raw2xml($this->Title);
1290
1291
        if ($thread = $this->getForumThread()) {
1292
            //If there is not first post either the thread has been removed or thread if a banned spammer.
1293
            if (!$thread->getFirstPost()) {
1294
                // don't hide the post for logged in admins or moderators
1295
                $member = Member::currentUser();
1296
                if (!$this->canModerate($member)) {
1297
                    return $this->httpError(404);
1298
                }
1299
            }
1300
1301
            $thread->incNumViews();
1302
1303
            $posts = sprintf(_t('Forum.POSTTOTOPIC', "Posts to the %s topic"), $thread->Title);
1304
1305
            RSSFeed::linkToFeed($this->Link("rss") . '/thread/' . (int) $this->urlParams['ID'], $posts);
1306
1307
            $title = Convert::raw2xml($thread->Title) . ' &raquo; ' . $title;
1308
            $field = DBField::create_field('HTMLText', $title);
1309
1310
            return array(
1311
                'Thread' => $thread,
1312
                'Title' => $field
1313
            );
1314
        } else {
1315
            // if redirecting post ids to thread id is enabled then we need
1316
            // to check to see if this matches a post and if it does redirect
1317
            if (Forum::$redirect_post_urls_to_thread && isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
0 ignored issues
show
Bug introduced by
The property redirect_post_urls_to_thread cannot be accessed from this context as it is declared private in class SilverStripe\Forum\Pages\Forum.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
1318
                if ($post = Post::get()->byID($this->urlParams['ID'])) {
1319
                    return $this->redirect($post->Link(), 301);
1320
                }
1321
            }
1322
        }
1323
1324
        return $this->httpError(404);
1325
    }
1326
1327
    /**
1328
     * Start topic action
1329
     *
1330
     * @return array Returns an array to render the start topic page
1331
     */
1332
    public function starttopic()
1333
    {
1334
        $topic = array(
1335
            'Subtitle' => DBField::create_field('HTMLText', _t('Forum.NEWTOPIC', 'Start a new topic')),
1336
            'Abstract' => DBField::create_field('HTMLText', DataObject::get_one("ForumHolder")->ForumAbstract)
1337
        );
1338
        return $topic;
1339
    }
1340
1341
    /**
1342
     * Get the forum title
1343
     *
1344
     * @return string Returns the forum title
1345
     */
1346
    public function getHolderSubtitle()
1347
    {
1348
        return $this->dbObject('Title');
1349
    }
1350
1351
    /**
1352
     * Get the currently viewed forum. Ensure that the user can access it
1353
     *
1354
     * @return ForumThread
1355
     */
1356
    public function getForumThread()
1357
    {
1358
        if (isset($this->urlParams['ID'])) {
1359
            $SQL_id = Convert::raw2sql($this->urlParams['ID']);
1360
1361
            if (is_numeric($SQL_id)) {
1362
                if ($thread = DataObject::get_by_id('ForumThread', $SQL_id)) {
1363
                    if (!$thread->canView()) {
1364
                        Security::permissionFailure($this);
1365
1366
                        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Pages...troller::getForumThread of type ForumThread.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1367
                    }
1368
1369
                    return $thread;
1370
                }
1371
            }
1372
        }
1373
1374
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Pages...troller::getForumThread of type ForumThread.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1375
    }
1376
1377
    /**
1378
     * Delete an Attachment
1379
     * Called from the EditPost method. Its Done via Ajax
1380
     *
1381
     * @return boolean
1382
     */
1383
    public function deleteattachment(HTTPRequest $request)
1384
    {
1385
        // Check CSRF token
1386
        if (!SecurityToken::inst()->checkRequest($request)) {
1387
            return $this->httpError(400);
1388
        }
1389
1390
        // check we were passed an id and member is logged in
1391
        if (!isset($this->urlParams['ID'])) {
1392
            return false;
1393
        }
1394
1395
        $file = DataObject::get_by_id("Post_Attachment", (int) $this->urlParams['ID']);
1396
1397
        if ($file && $file->canDelete()) {
1398
            $file->delete();
1399
1400
            return (!Director::is_ajax()) ? $this->redirectBack() : true;
1401
        }
1402
1403
        return false;
1404
    }
1405
1406
    /**
1407
     * Edit post action
1408
     *
1409
     * @return array Returns an array to render the edit post page
1410
     */
1411
    public function editpost()
1412
    {
1413
        return array(
1414
            'Subtitle' => _t('Forum.EDITPOST', 'Edit post')
1415
        );
1416
    }
1417
1418
    /**
1419
     * Get the post edit form if the user has the necessary permissions
1420
     *
1421
     * @return Form
1422
     */
1423
    public function EditForm()
1424
    {
1425
        $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
1426
        $post = DataObject::get_by_id('Post', $id);
1427
1428
        return $this->PostMessageForm(false, $post);
1429
    }
1430
1431
1432
    /**
1433
     * Delete a post via the url.
1434
     *
1435
     * @return bool
1436
     */
1437
    public function deletepost(HTTPRequest $request)
1438
    {
1439
        // Check CSRF token
1440
        if (!SecurityToken::inst()->checkRequest($request)) {
1441
            return $this->httpError(400);
1442
        }
1443
1444
        if (isset($this->urlParams['ID'])) {
1445
            if ($post = DataObject::get_by_id('Post', (int) $this->urlParams['ID'])) {
1446
                if ($post->canDelete()) {
1447
                    // delete the whole thread if this is the first one. The delete action
1448
                    // on thread takes care of the posts.
1449
                    if ($post->isFirstPost()) {
1450
                        $thread = DataObject::get_by_id("ForumThread", $post->ThreadID);
1451
                        $thread->delete();
1452
                    } else {
1453
                        // delete the post
1454
                        $post->delete();
1455
                    }
1456
                }
1457
            }
1458
        }
1459
1460
        return (Director::is_ajax()) ? true : $this->redirect($this->Link());
1461
    }
1462
1463
    /**
1464
     * Returns the Forum Message from Session. This
1465
     * is used for things like Moving thread messages
1466
     * @return String
1467
     */
1468
    public function ForumAdminMsg()
1469
    {
1470
        $message = Session::get('ForumAdminMsg');
1471
        Session::clear('ForumAdminMsg');
1472
1473
        return $message;
1474
    }
1475
1476
1477
    /**
1478
     * Forum Admin Features form.
1479
     * Handles the dropdown to select the new forum category and the checkbox for stickyness
1480
     *
1481
     * @return Form
1482
     */
1483
    public function AdminFormFeatures()
1484
    {
1485
        if (!$this->canModerate()) {
1486
            return;
1487
        }
1488
1489
        $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : false;
1490
1491
        $fields = new FieldList(
1492
            new CheckboxField('IsSticky', _t('Forum.ISSTICKYTHREAD', 'Is this a Sticky Thread?')),
1493
            new CheckboxField('IsGlobalSticky', _t('Forum.ISGLOBALSTICKY', 'Is this a Global Sticky (shown on all forums)')),
1494
            new CheckboxField('IsReadOnly', _t('Forum.ISREADONLYTHREAD', 'Is this a Read only Thread?')),
1495
            new HiddenField("ID", "Thread")
1496
        );
1497
1498
        if (($forums = Forum::get()) && $forums->exists()) {
1499
            $fields->push(new DropdownField("ForumID", _t('Forum.CHANGETHREADFORUM', "Change Thread Forum"), $forums->map('ID', 'Title', 'Select New Category:')), '', null, 'Select New Location:');
1500
        }
1501
1502
        $actions = new FieldList(
1503
            new FormAction('doAdminFormFeatures', _t('Forum.SAVE', 'Save'))
1504
        );
1505
1506
        $form = new Form($this, 'AdminFormFeatures', $fields, $actions);
1507
1508
        // need this id wrapper since the form method is called on save as
1509
        // well and needs to return a valid form object
1510
        if ($id) {
1511
            $thread = ForumThread::get()->byID($id);
1512
            $form->loadDataFrom($thread);
1513
        }
1514
1515
        $this->extend('updateAdminFormFeatures', $form);
1516
1517
        return $form;
1518
    }
1519
1520
    /**
1521
     * Process's the moving of a given topic. Has to check for admin privledges,
1522
     * passed an old topic id (post id in URL) and a new topic id
1523
     */
1524
    public function doAdminFormFeatures($data, $form)
1525
    {
1526
        if (isset($data['ID'])) {
1527
            $thread = ForumThread::get()->byID($data['ID']);
1528
1529
            if ($thread) {
1530
                if (!$thread->canModerate()) {
1531
                    return Security::permissionFailure($this);
1532
                }
1533
1534
                $form->saveInto($thread);
1535
                $thread->write();
1536
            }
1537
        }
1538
1539
        return $this->redirect($this->Link());
1540
    }
1541
}
1542
1543
/**
1544
 * This is a DataQuery that allows us to replace the underlying query. Hopefully this will
1545
 * be a native ability in 3.1, but for now we need to.
1546
 * TODO: Remove once API in core
1547
 */
1548
class Forum_DataQuery extends DataQuery
1549
{
1550
    public function __construct($dataClass, $query)
1551
    {
1552
        parent::__construct($dataClass);
1553
        $this->query = $query;
1554
    }
1555
}
1556