Forum_Controller::show()   C
last analyzed

Complexity

Conditions 8
Paths 6

Size

Total Lines 39
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 6
nop 0
1
<?php
2
3
/**
4
 * Forum represents a collection of forum threads. Each thread is a different topic on
5
 * the site. You can customize permissions on a per forum basis in the CMS.
6
 *
7
 * @todo Implement PermissionProvider for editing, creating forums.
8
 *
9
 * @package forum
10
 */
11
12
class Forum extends Page
13
{
14
15
    private static $allowed_children = 'none';
16
17
    private static $icon = "forum/images/treeicons/user";
18
19
    /**
20
     * Enable this to automatically notify moderators when a message is posted
21
     * or edited on his forums.
22
     */
23
    static $notify_moderators = false;
24
25
    private static $db = array(
26
        "Abstract" => "Text",
27
        "CanPostType" => "Enum('Inherit, Anyone, LoggedInUsers, OnlyTheseUsers, NoOne', 'Inherit')",
28
        "CanAttachFiles" => "Boolean",
29
    );
30
31
    private static $has_one = array(
32
        "Moderator" => "Member",
33
        "Category" => "ForumCategory"
34
    );
35
36
    private static $many_many = array(
37
        'Moderators' => 'Member',
38
        'PosterGroups' => 'Group'
39
    );
40
41
    private static $defaults = array(
42
        "ForumPosters" => "LoggedInUsers"
43
    );
44
45
    /**
46
     * Number of posts to include in the thread view before pagination takes effect.
47
     *
48
     * @var int
49
     */
50
    static $posts_per_page = 8;
51
52
    /**
53
     * When migrating from older versions of the forum it used post ID as the url token
54
     * as of forum 1.0 we now use ThreadID. If you want to enable 301 redirects from post to thread ID
55
     * set this to true
56
     *
57
     * @var bool
58
     */
59
    static $redirect_post_urls_to_thread = false;
60
61
    /**
62
     * Check if the user can view the forum.
63
     */
64
    public function canView($member = null)
65
    {
66
        if (!$member) {
67
            $member = Member::currentUser();
68
        }
69
        return (parent::canView($member) || $this->canModerate($member));
70
    }
71
72
    /**
73
     * Check if the user can post to the forum and edit his own posts.
74
     */
75
    public function canPost($member = null)
76
    {
77
        if (!$member) {
78
            $member = Member::currentUser();
79
        }
80
81
        if ($this->CanPostType == "Inherit") {
82
            $holder = $this->getForumHolder();
83
            if ($holder) {
84
                return $holder->canPost($member);
85
            }
86
87
            return false;
88
        }
89
90
        if ($this->CanPostType == "NoOne") {
91
            return false;
92
        }
93
94
        if ($this->CanPostType == "Anyone" || $this->canEdit($member)) {
95
            return true;
96
        }
97
98 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...
99
            if ($member->IsSuspended()) {
100
                return false;
101
            }
102
            if ($member->IsBanned()) {
103
                return false;
104
            }
105
106
            if ($this->CanPostType == "LoggedInUsers") {
107
                return true;
108
            }
109
110
            if ($groups = $this->PosterGroups()) {
111
                foreach ($groups as $group) {
112
                    if ($member->inGroup($group)) {
113
                        return true;
114
                    }
115
                }
116
            }
117
        }
118
119
        return false;
120
    }
121
122
    /**
123
     * Check if user has access to moderator panel and can delete posts and threads.
124
     */
125
    public function canModerate($member = null)
126
    {
127
        if (!$member) {
128
            $member = Member::currentUser();
129
        }
130
131
        if (!$member) {
132
            return false;
133
        }
134
135
        // Admins
136
        if (Permission::checkMember($member, 'ADMIN')) {
137
            return true;
138
        }
139
140
        // Moderators
141
        if ($member->isModeratingForum($this)) {
142
            return true;
143
        }
144
145
        return false;
146
    }
147
148
    /**
149
     * Can we attach files to topics/posts inside this forum?
150
     *
151
     * @return bool Set to TRUE if the user is allowed to, to FALSE if they're
152
     *              not
153
     */
154
    public function canAttach($member = null)
155
    {
156
        return $this->CanAttachFiles ? true : false;
157
    }
158
159
    public function requireTable()
160
    {
161
        // Migrate permission columns
162
        if (DB::getConn()->hasTable('Forum')) {
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
Deprecated Code introduced by
The method SS_Database::hasTable() has been deprecated with message: since version 4.0 Use DB::get_schema()->hasTable() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
163
            $fields = DB::getConn()->fieldList('Forum');
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
Deprecated Code introduced by
The method SS_Database::fieldList() has been deprecated with message: since version 4.0 Use DB::field_list instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
164
            if (in_array('ForumPosters', array_keys($fields)) && !in_array('CanPostType', array_keys($fields))) {
165
                DB::getConn()->renameField('Forum', 'ForumPosters', 'CanPostType');
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
Deprecated Code introduced by
The method SS_Database::renameField() has been deprecated with message: since version 4.0 Use DB::get_schema()->renameField() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

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

Loading history...
166
                DB::alteration_message('Migrated forum permissions from "ForumPosters" to "CanPostType"', "created");
167
            }
168
        }
169
170
        parent::requireTable();
171
    }
172
173
    /**
174
     * Add default records to database
175
     *
176
     * This function is called whenever the database is built, after the
177
     * database tables have all been created.
178
     */
179
    public function requireDefaultRecords()
180
    {
181
        parent::requireDefaultRecords();
182
183
        $code = "ACCESS_FORUM";
184
        if (!($forumGroup = Group::get()->filter('Code', 'forum-members')->first())) {
185
            $group = new Group();
186
            $group->Code = 'forum-members';
187
            $group->Title = "Forum Members";
188
            $group->write();
189
190
            Permission::grant($group->ID, $code);
191
            DB::alteration_message(_t('Forum.GROUPCREATED', 'Forum Members group created'), 'created');
192
        } elseif (!Permission::get()->filter(array('GroupID' => $forumGroup->ID, 'Code' => $code))->exists()) {
193
            Permission::grant($forumGroup->ID, $code);
194
        }
195
196
        if (!($category = ForumCategory::get()->first())) {
197
            $category = new ForumCategory();
198
            $category->Title = _t('Forum.DEFAULTCATEGORY', 'General');
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<ForumCategory>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
199
            $category->write();
200
        }
201
202
        if (!ForumHolder::get()->exists()) {
203
            $forumholder = new ForumHolder();
204
            $forumholder->Title = "Forums";
205
            $forumholder->URLSegment = "forums";
206
            $forumholder->Content = "<p>"._t('Forum.WELCOMEFORUMHOLDER', 'Welcome to SilverStripe Forum Module! This is the default ForumHolder page. You can now add forums.')."</p>";
207
            $forumholder->Status = "Published";
208
            $forumholder->write();
209
            $forumholder->publish("Stage", "Live");
210
            DB::alteration_message(_t('Forum.FORUMHOLDERCREATED', 'ForumHolder page created'), "created");
211
212
            $forum = new Forum();
213
            $forum->Title = _t('Forum.TITLE', 'General Discussion');
214
            $forum->URLSegment = "general-discussion";
215
            $forum->ParentID = $forumholder->ID;
216
            $forum->Content = "<p>"._t('Forum.WELCOMEFORUM', 'Welcome to SilverStripe Forum Module! This is the default Forum page. You can now add topics.')."</p>";
217
            $forum->Status = "Published";
218
            $forum->CategoryID = $category->ID;
219
            $forum->write();
220
            $forum->publish("Stage", "Live");
221
222
            DB::alteration_message(_t('Forum.FORUMCREATED', 'Forum page created'), "created");
223
        }
224
    }
225
226
    /**
227
     * Check if we can and should show forums in categories
228
     */
229
    public function getShowInCategories()
230
    {
231
        $holder = $this->getForumHolder();
232
        if ($holder) {
233
            return $holder->getShowInCategories();
234
        }
235
    }
236
237
    /**
238
     * Returns a FieldList with which to create the CMS editing form
239
     *
240
     * @return FieldList The fields to be displayed in the CMS.
241
     */
242
    public function getCMSFields()
243
    {
244
        $self = $this;
245
246
        $this->beforeUpdateCMSFields(function ($fields) use ($self) {
247
            Requirements::javascript("forum/javascript/ForumAccess.js");
248
            Requirements::css("forum/css/Forum_CMS.css");
249
250
            $fields->addFieldToTab("Root.Access", new HeaderField(_t('Forum.ACCESSPOST', 'Who can post to the forum?'), 2));
251
            $fields->addFieldToTab("Root.Access", $optionSetField = new OptionsetField("CanPostType", "", array(
252
                "Inherit" => "Inherit",
253
                "Anyone" => _t('Forum.READANYONE', 'Anyone'),
254
                "LoggedInUsers" => _t('Forum.READLOGGEDIN', 'Logged-in users'),
255
                "OnlyTheseUsers" => _t('Forum.READLIST', 'Only these people (choose from list)'),
256
                "NoOne" => _t('Forum.READNOONE', 'Nobody. Make Forum Read Only')
257
            )));
258
259
            $optionSetField->addExtraClass('ForumCanPostTypeSelector');
260
261
            $fields->addFieldsToTab("Root.Access", array(
262
                new TreeMultiselectField("PosterGroups", _t('Forum.GROUPS', "Groups")),
263
                new OptionsetField("CanAttachFiles", _t('Forum.ACCESSATTACH', 'Can users attach files?'), array(
264
                    "1" => _t('Forum.YES', 'Yes'),
265
                    "0" => _t('Forum.NO', 'No')
266
                ))
267
            ));
268
269
270
            //Dropdown of forum category selection.
271
            $categories = ForumCategory::get()->map();
272
273
            $fields->addFieldsToTab(
274
                "Root.Main",
275
                DropdownField::create('CategoryID', _t('Forum.FORUMCATEGORY', 'Forum Category'), $categories),
276
                'Content'
277
            );
278
279
            //GridField Config - only need to attach or detach Moderators with existing Member accounts.
280
            $moderatorsConfig = GridFieldConfig::create()
281
                ->addComponent(new GridFieldButtonRow('before'))
282
                ->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right'))
283
                ->addComponent(new GridFieldToolbarHeader())
284
                ->addComponent($sort = new GridFieldSortableHeader())
285
                ->addComponent($columns = new GridFieldDataColumns())
286
                ->addComponent(new GridFieldDeleteAction(true))
287
                ->addComponent(new GridFieldPageCount('toolbar-header-right'))
288
                ->addComponent($pagination = new GridFieldPaginator());
289
290
            // Use GridField for Moderator management
291
            $moderators = GridField::create(
292
                'Moderators',
293
                _t('MODERATORS', 'Moderators for this forum'),
294
                $self->Moderators(),
295
                $moderatorsConfig
296
            );
297
298
            $columns->setDisplayFields(array(
299
                'Nickname' => 'Nickname',
300
                'FirstName' => 'First name',
301
                'Surname' => 'Surname',
302
                'Email'=> 'Email',
303
                'LastVisited.Long' => 'Last Visit'
304
            ));
305
306
            $sort->setThrowExceptionOnBadDataType(false);
307
            $pagination->setThrowExceptionOnBadDataType(false);
308
309
            $fields->addFieldToTab('Root.Moderators', $moderators);
310
        });
311
312
        $fields = parent::getCMSFields();
313
314
        return $fields;
315
    }
316
317
    /**
318
     * Create breadcrumbs
319
     *
320
     * @param int $maxDepth Maximal lenght of the breadcrumb navigation
321
     * @param bool $unlinked Set to TRUE if the breadcrumb should consist of
322
     *                       links, otherwise FALSE.
323
     * @param bool $stopAtPageType Currently not used
324
     * @param bool $showHidden Set to TRUE if also hidden pages should be
325
     *                         displayed
326
     * @return string HTML code to display breadcrumbs
327
     */
328
    public function Breadcrumbs($maxDepth = null, $unlinked = false, $stopAtPageType = false, $showHidden = false)
329
    {
330
        $page = $this;
331
        $nonPageParts = array();
332
        $parts = array();
333
334
        $controller = Controller::curr();
335
        $params = $controller->getURLParams();
336
337
        $forumThreadID = $params['ID'];
338
        if (is_numeric($forumThreadID)) {
339
            if ($topic = ForumThread::get()->byID($forumThreadID)) {
340
                $nonPageParts[] = Convert::raw2xml($topic->getTitle());
341
            }
342
        }
343
344
        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...
345
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
346
                if ($page->URLSegment == 'home') {
347
                    $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...
348
                }
349
350
                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...
351
                    $parts[] = '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
352
                } else {
353
                    $parts[] = (($page->ID == $this->ID) || $unlinked)
354
                            ? Convert::raw2xml($page->Title)
355
                            : '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
356
                }
357
            }
358
359
            $page = $page->Parent;
360
        }
361
362
        return implode(" &raquo; ", array_reverse(array_merge($nonPageParts, $parts)));
363
    }
364
365
    /**
366
     * Helper Method from the template includes. Uses $ForumHolder so in order for it work
367
     * it needs to be included on this page
368
     *
369
     * @return ForumHolder
370
     */
371
    public function getForumHolder()
372
    {
373
        $holder = $this->Parent();
374
        if ($holder->ClassName=='ForumHolder') {
375
            return $holder;
376
        }
377
    }
378
379
    /**
380
     * Get the latest posting of the forum. For performance the forum ID is stored on the
381
     * {@link Post} object as well as the {@link Forum} object
382
     *
383
     * @return Post
384
     */
385
    public function getLatestPost()
386
    {
387
        return Post::get()->filter('ForumID', $this->ID)->sort('"Post"."ID" DESC')->first();
388
    }
389
390
    /**
391
     * Get the number of total topics (threads) in this Forum
392
     *
393
     * @return int Returns the number of topics (threads)
394
     */
395
    public function getNumTopics()
396
    {
397
        $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...
398
        $sqlQuery->setFrom('"Post"');
399
        $sqlQuery->setSelect('COUNT(DISTINCT("ThreadID"))');
400
        $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
401
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
402
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
403
        return $sqlQuery->execute()->value();
404
    }
405
406
    /**
407
     * Get the number of total posts
408
     *
409
     * @return int Returns the number of posts
410
     */
411
    public function getNumPosts()
412
    {
413
        $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...
414
        $sqlQuery->setFrom('"Post"');
415
        $sqlQuery->setSelect('COUNT("Post"."ID")');
416
        $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
417
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
418
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
419
        return $sqlQuery->execute()->value();
420
    }
421
422
423
    /**
424
     * Get the number of distinct Authors
425
     *
426
     * @return int
427
     */
428
    public function getNumAuthors()
429
    {
430
        $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...
431
        $sqlQuery->setFrom('"Post"');
432
        $sqlQuery->setSelect('COUNT(DISTINCT("AuthorID"))');
433
        $sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
434
        $sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
435
        $sqlQuery->addWhere('"ForumID" = ' . $this->ID);
436
        return $sqlQuery->execute()->value();
437
    }
438
439
    /**
440
     * Returns the Topics (the first Post of each Thread) for this Forum
441
     * @return DataList
442
     */
443
    public function getTopics()
444
    {
445
        // Get a list of Posts
446
        $posts = Post::get();
447
448
        // Get the underlying query and change it to return the ThreadID and Max(Created) and Max(ID) for each thread
449
        // of those posts
450
        $postQuery = $posts->dataQuery()->query();
451
452
        $postQuery
453
            ->setSelect(array())
454
            ->selectField('MAX("Post"."Created")', 'PostCreatedMax')
455
            ->selectField('MAX("Post"."ID")', 'PostIDMax')
456
            ->selectField('"ThreadID"')
457
            ->setGroupBy('"ThreadID"')
458
            ->addWhere(sprintf('"ForumID" = \'%s\'', $this->ID))
459
            ->setDistinct(false);
460
461
        // Get a list of forum threads inside this forum that aren't sticky
462
        $threads = ForumThread::get()->filter(array(
463
            'ForumID' => $this->ID,
464
            'IsGlobalSticky' => 0,
465
            'IsSticky' => 0
466
        ));
467
468
        // Get the underlying query and change it to inner join on the posts list to just show threads that
469
        // have approved (and maybe awaiting) posts, and sort the threads by the most recent post
470
        $threadQuery = $threads->dataQuery()->query();
471
        $threadQuery
472
            ->addSelect(array('"PostMax"."PostCreatedMax", "PostMax"."PostIDMax"'))
473
            ->addFrom('INNER JOIN ('.$postQuery->sql().') AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")')
474
            ->addOrderBy(array('"PostMax"."PostCreatedMax" DESC', '"PostMax"."PostIDMax" DESC'))
475
            ->setDistinct(false);
476
477
        // Alter the forum threads list to use the new query
478
        $threads = $threads->setDataQuery(new Forum_DataQuery('ForumThread', $threadQuery));
479
480
        // And return the results
481
        return $threads->exists() ? new PaginatedList($threads, $_GET) : null;
482
    }
483
484
485
486
    /*
487
	 * Returns the Sticky Threads
488
	 * @param boolean $include_global Include Global Sticky Threads in the results (default: true)
489
	 * @return DataList
490
	 */
491
    public function getStickyTopics($include_global = true)
492
    {
493
        // Get Threads that are sticky & in this forum
494
        $where = '("ForumThread"."ForumID" = '.$this->ID.' AND "ForumThread"."IsSticky" = 1)';
495
        // Get Threads that are globally sticky
496
        if ($include_global) {
497
            $where .= ' OR ("ForumThread"."IsGlobalSticky" = 1)';
498
        }
499
500
        // Get the underlying query
501
        $query = ForumThread::get()->where($where)->dataQuery()->query();
502
503
        // Sort by the latest Post in each thread's Created date
504
        $query
505
          ->addSelect('"PostMax"."PostMax"')
506
          // TODO: Confirm this works in non-MySQL DBs
507
          ->addFrom(sprintf(
508
              'LEFT JOIN (SELECT MAX("Created") AS "PostMax", "ThreadID" FROM "Post" WHERE "ForumID" = \'%s\' GROUP BY "ThreadID") AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")',
509
              $this->ID
510
          ))
511
          ->addOrderBy('"PostMax"."PostMax" DESC')
512
          ->setDistinct(false);
513
514
       // Build result as ArrayList
515
        $res = new ArrayList();
516
        $rows = $query->execute();
517
        if ($rows) {
518
            foreach ($rows as $row) {
519
                $res->push(new ForumThread($row));
520
            }
521
        }
522
523
        return $res;
524
    }
525
}
526
527
/**
528
 * The forum controller class
529
 *
530
 * @package forum
531
 */
532
class Forum_Controller extends Page_Controller
533
{
534
535
    private static $allowed_actions = array(
536
        'AdminFormFeatures',
537
        'deleteattachment',
538
        'deletepost',
539
        'editpost',
540
        'markasspam',
541
        'PostMessageForm',
542
        'reply',
543
        'show',
544
        'starttopic',
545
        'subscribe',
546
        'unsubscribe',
547
        'rss',
548
        'ban',
549
        'ghost'
550
    );
551
552
553
    public function init()
554
    {
555
        parent::init();
556
        if ($this->redirectedTo()) {
557
            return;
558
        }
559
560
        Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
561
        Requirements::javascript("forum/javascript/Forum.js");
562
        Requirements::javascript("forum/javascript/jquery.MultiFile.js");
563
564
        Requirements::themedCSS('Forum', 'forum', 'all');
565
566
        RSSFeed::linkToFeed($this->Parent()->Link("rss/forum/$this->ID"), sprintf(_t('Forum.RSSFORUM', "Posts to the '%s' forum"), $this->Title));
567
        RSSFeed::linkToFeed($this->Parent()->Link("rss"), _t('Forum.RSSFORUMS', 'Posts to all forums'));
568
569
        if (!$this->canView()) {
570
            $messageSet = array(
571
                'default' => _t('Forum.LOGINDEFAULT', 'Enter your email address and password to view this forum.'),
572
                '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'),
573
                '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.')
574
            );
575
576
            Security::permissionFailure($this, $messageSet);
577
            return;
578
        }
579
580
        // Log this visit to the ForumMember if they exist
581
        $member = Member::currentUser();
582
        if ($member && Config::inst()->get('ForumHolder', 'currently_online_enabled')) {
583
            $member->LastViewed = date("Y-m-d H:i:s");
584
            $member->write();
585
        }
586
587
        // Set the back url
588 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...
589
            Session::set('BackURL', $_SERVER['REQUEST_URI']);
590
        } else {
591
            Session::set('BackURL', $this->Link());
592
        }
593
    }
594
595
    /**
596
     * A convenience function which provides nice URLs for an rss feed on this forum.
597
     */
598
    public function rss()
599
    {
600
        $this->redirect($this->Parent()->Link("rss/forum/$this->ID"), 301);
601
    }
602
603
    /**
604
     * Is OpenID support available?
605
     *
606
     * This method checks if the {@link OpenIDAuthenticator} is available and
607
     * registered.
608
     *
609
     * @return bool Returns TRUE if OpenID is available, FALSE otherwise.
610
     */
611
    public function OpenIDAvailable()
612
    {
613
        return $this->Parent()->OpenIDAvailable();
614
    }
615
616
    /**
617
     * Subscribe a user to a thread given by an ID.
618
     *
619
     * Designed to be called via AJAX so return true / false
620
     *
621
     * @return bool
622
     */
623
    public function subscribe(SS_HTTPRequest $request)
624
    {
625
        // Check CSRF
626
        if (!SecurityToken::inst()->checkRequest($request)) {
627
            return $this->httpError(400);
628
        }
629
		
630
		$subscribed = false;
631
632
        if (Member::currentUser() && !ForumThread_Subscription::already_subscribed($this->urlParams['ID'])) {
633
            $obj = new ForumThread_Subscription();
634
            $obj->ThreadID = (int) $this->urlParams['ID'];
0 ignored issues
show
Documentation introduced by
The property ThreadID does not exist on object<ForumThread_Subscription>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
635
            $obj->MemberID = Member::currentUserID();
0 ignored issues
show
Documentation introduced by
The property MemberID does not exist on object<ForumThread_Subscription>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
636
            $obj->LastSent = date("Y-m-d H:i:s");
0 ignored issues
show
Documentation introduced by
The property LastSent does not exist on object<ForumThread_Subscription>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
637
            $obj->write();
638
639
            $subscribed = true;
640
        }
641
642
        return ($request->isAjax()) ? $subscribed : $this->redirectBack();
643
    }
644
645
    /**
646
     * Unsubscribe a user from a thread by an ID
647
     *
648
     * Designed to be called via AJAX so return true / false
649
     *
650
     * @return bool
651
     */
652
    public function unsubscribe(SS_HTTPRequest $request)
653
    {
654
        $member = Member::currentUser();
655
		$unsubscribed = false;
656
657
        if (!$member) {
658
            Security::permissionFailure($this, _t('LOGINTOUNSUBSCRIBE', 'To unsubscribe from that thread, please log in first.'));
659
        }
660
661
        if (ForumThread_Subscription::already_subscribed($this->urlParams['ID'], $member->ID)) {
662
            DB::query("
663
				DELETE FROM \"ForumThread_Subscription\"
664
				WHERE \"ThreadID\" = '". Convert::raw2sql($this->urlParams['ID']) ."'
665
				AND \"MemberID\" = '$member->ID'");
666
			
667
			$unsubscribed = true;
668
        }
669
670
        return ($request->isAjax()) ? $unsubscribed : $this->redirectBack();
671
    }
672
673
    /**
674
     * Mark a post as spam. Deletes any posts or threads created by that user
675
     * and removes their user account from the site
676
     *
677
     * Must be logged in and have the correct permissions to do marking
678
     */
679
    public function markasspam(SS_HTTPRequest $request)
680
    {
681
        $currentUser = Member::currentUser();
682
        if (!isset($this->urlParams['ID'])) {
683
            return $this->httpError(400);
684
        }
685
        if (!$this->canModerate()) {
686
            return $this->httpError(403);
687
        }
688
689
        // Check CSRF token
690
        if (!SecurityToken::inst()->checkRequest($request)) {
691
            return $this->httpError(400);
692
        }
693
694
        $post = Post::get()->byID($this->urlParams['ID']);
695
        if ($post) {
696
            // post was the start of a thread, Delete the whole thing
697
            if ($post->isFirstPost()) {
698
                $post->Thread()->delete();
699
            }
700
701
            // Delete the current post
702
            $post->delete();
703
            $post->extend('onAfterMarkAsSpam');
704
705
            // Log deletion event
706
            SS_Log::log(sprintf(
707
                'Marked post #%d as spam, by moderator %s (#%d)',
708
                $post->ID,
709
                $currentUser->Email,
710
                $currentUser->ID
711
            ), SS_Log::NOTICE);
712
713
            // Suspend the member (rather than deleting him),
714
            // which gives him or a moderator the chance to revoke a decision.
715
            if ($author = $post->Author()) {
716
                $author->SuspendedUntil = date('Y-m-d', strtotime('+99 years', SS_Datetime::now()->Format('U')));
717
                $author->write();
718
            }
719
720
            SS_Log::log(sprintf(
721
                'Suspended member %s (#%d) for spam activity, by moderator %s (#%d)',
722
                $author->Email,
723
                $author->ID,
724
                $currentUser->Email,
725
                $currentUser->ID
726
            ), SS_Log::NOTICE);
727
        }
728
729
        return (Director::is_ajax()) ? true : $this->redirect($this->Link());
730
    }
731
732
733 View Code Duplication
    public function ban(SS_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...
734
    {
735
        if (!$r->param('ID')) {
736
            return $this->httpError(404);
737
        }
738
        if (!$this->canModerate()) {
739
            return $this->httpError(403);
740
        }
741
742
        $member = Member::get()->byID($r->param('ID'));
743
        if (!$member || !$member->exists()) {
744
            return $this->httpError(404);
745
        }
746
747
        $member->ForumStatus = 'Banned';
748
        $member->write();
749
750
        // Log event
751
        $currentUser = Member::currentUser();
752
        SS_Log::log(sprintf(
753
            'Banned member %s (#%d), by moderator %s (#%d)',
754
            $member->Email,
755
            $member->ID,
756
            $currentUser->Email,
757
            $currentUser->ID
758
        ), SS_Log::NOTICE);
759
760
        return ($r->isAjax()) ? true : $this->redirectBack();
761
    }
762
763 View Code Duplication
    public function ghost(SS_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...
764
    {
765
        if (!$r->param('ID')) {
766
            return $this->httpError(400);
767
        }
768
        if (!$this->canModerate()) {
769
            return $this->httpError(403);
770
        }
771
772
        $member = Member::get()->byID($r->param('ID'));
773
        if (!$member || !$member->exists()) {
774
            return $this->httpError(404);
775
        }
776
777
        $member->ForumStatus = 'Ghost';
778
        $member->write();
779
780
        // Log event
781
        $currentUser = Member::currentUser();
782
        SS_Log::log(sprintf(
783
            'Ghosted member %s (#%d), by moderator %s (#%d)',
784
            $member->Email,
785
            $member->ID,
786
            $currentUser->Email,
787
            $currentUser->ID
788
        ), SS_Log::NOTICE);
789
790
        return ($r->isAjax()) ? true : $this->redirectBack();
791
    }
792
793
    /**
794
     * Get posts to display. This method assumes an URL parameter "ID" which contains the thread ID.
795
     * @param string sortDirection The sort order direction, either ASC for ascending (default) or DESC for descending
796
     * @return DataObjectSet Posts
797
     */
798
    public function Posts($sortDirection = "ASC")
799
    {
800
        $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 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...
801
802
        $posts = Post::get()
803
            ->filter('ThreadID', $this->urlParams['ID'])
804
            ->sort('Created', $sortDirection);
805
806
        if (isset($_GET['showPost']) && !isset($_GET['start'])) {
807
            $postIDList = clone $posts;
808
            $postIDList = $postIDList->select('ID')->toArray();
809
810
            if ($postIDList->exists()) {
811
                $foundPos = array_search($_GET['showPost'], $postIDList);
812
                $_GET['start'] = floor($foundPos / $numPerPage) * $numPerPage;
813
            }
814
        }
815
816
        if (!isset($_GET['start'])) {
817
            $_GET['start'] = 0;
818
        }
819
820
        $member = Member::currentUser();
821
822
        /*
823
		 * Don't show posts of banned or ghost members, unless current Member
824
		 * is a ghost member and owner of current post
825
		 */
826
827
        $posts = $posts->exclude(array(
828
            'Author.ForumStatus' => 'Banned'
829
        ));
830
831
        if ($member) {
832
            $posts = $posts->exclude(array(
833
                'Author.ForumStatus' => 'Ghost',
834
                'Author.ID:not' => $member->ID
835
            ));
836
        } else {
837
            $posts = $posts->exclude(array(
838
                'Author.ForumStatus' => 'Ghost'
839
            ));
840
        }
841
842
        $paginated = new PaginatedList($posts, $_GET);
843
        $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 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...
844
        return $paginated;
845
    }
846
847
    /**
848
     * Get the usable BB codes
849
     *
850
     * @return DataObjectSet Returns the usable BB codes
851
     * @see BBCodeParser::usable_tags()
852
     */
853
    public function BBTags()
854
    {
855
        return BBCodeParser::usable_tags();
856
    }
857
858
    /**
859
     * Section for dealing with reply / edit / create threads form
860
     *
861
     * @return Form Returns the post message form
862
     */
863
    public function PostMessageForm($addMode = false, $post = false)
864
    {
865
        $thread = false;
866
867
        if ($post) {
868
            $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...
869
        } elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
870
            $thread = DataObject::get_by_id('ForumThread', $this->urlParams['ID']);
871
        }
872
873
        // Check permissions
874
        $messageSet = array(
875
            'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'),
876
            'alreadyLoggedIn' => _t(
877
                'Forum.LOGINTOPOSTLOGGEDIN',
878
                'I\'m sorry, but you can\'t post to this forum until you\'ve logged in.'
879
                .'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.'
880
            ),
881
            '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.'),
882
        );
883
884
        // Creating new thread
885
        if ($addMode && !$this->canPost()) {
886
            Security::permissionFailure($this, $messageSet);
887
            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 Forum_Controller::PostMessageForm of type 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...
888
        }
889
890
        // Replying to existing thread
891 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...
892
            Security::permissionFailure($this, $messageSet);
893
            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 Forum_Controller::PostMessageForm of type 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...
894
        }
895
896
        // Editing existing post
897
        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...
898
            Security::permissionFailure($this, $messageSet);
899
            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 Forum_Controller::PostMessageForm of type 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...
900
        }
901
902
        $forumBBCodeHint = $this->renderWith('Forum_BBCodeHint');
903
904
        $fields = new FieldList(
905
            ($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...
906
            new TextareaField("Content", _t('Forum.FORUMREPLYCONTENT', 'Content')),
907
            new LiteralField(
908
                "BBCodeHelper",
909
                "<div class=\"BBCodeHint\">[ <a href=\"#BBTagsHolder\" id=\"BBCodeHint\">" .
910
                _t('Forum.BBCODEHINT', 'View Formatting Help') .
911
                "</a> ]</div>" .
912
                $forumBBCodeHint
913
            ),
914
            new CheckboxField(
915
                "TopicSubscription",
916
                _t('Forum.SUBSCRIBETOPIC', 'Subscribe to this topic (Receive email notifications when a new reply is added)'),
917
                ($thread) ? $thread->getHasSubscribed() : false
918
            )
919
        );
920
921
        if ($thread) {
922
            $fields->push(new HiddenField('ThreadID', 'ThreadID', $thread->ID));
923
        }
924
        if ($post) {
925
            $fields->push(new HiddenField('ID', 'ID', $post->ID));
926
        }
927
928
        // Check if we can attach files to this forum's posts
929
        if ($this->canAttach()) {
930
            $fields->push(FileField::create("Attachment", _t('Forum.ATTACH', 'Attach file')));
931
        }
932
933
        // If this is an existing post check for current attachments and generate
934
        // a list of the uploaded attachments
935
        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...
936
            if ($attachmentList->exists()) {
937
                $attachments = "<div id=\"CurrentAttachments\"><h4>". _t('Forum.CURRENTATTACHMENTS', 'Current Attachments') ."</h4><ul>";
938
                $link = $this->Link();
939
                // An instance of the security token
940
                $token = SecurityToken::inst();
941
942
                foreach ($attachmentList as $attachment) {
943
                    // Generate a link properly, since it requires a security token
944
                    $attachmentLink = Controller::join_links($link, 'deleteattachment', $attachment->ID);
945
                    $attachmentLink = $token->addToUrl($attachmentLink);
946
947
                    $attachments .= "<li class='attachment-$attachment->ID'>$attachment->Name [<a href='{$attachmentLink}' rel='$attachment->ID' class='deleteAttachment'>"
948
                            . _t('Forum.REMOVE', 'remove') . "</a>]</li>";
949
                }
950
                $attachments .= "</ul></div>";
951
952
                $fields->push(new LiteralField('CurrentAttachments', $attachments));
953
            }
954
        }
955
956
        $actions = new FieldList(
957
            new FormAction("doPostMessageForm", _t('Forum.REPLYFORMPOST', 'Post'))
958
        );
959
960
        $required = $addMode === true ? new RequiredFields("Title", "Content") : new RequiredFields("Content");
961
962
        $form = new Form($this, 'PostMessageForm', $fields, $actions, $required);
963
964
        $this->extend('updatePostMessageForm', $form, $post);
965
966
        if ($post) {
967
            $form->loadDataFrom($post);
0 ignored issues
show
Documentation introduced by
$post is of type boolean, but the function expects a array|object<DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
968
        }
969
970
        return $form;
971
    }
972
973
    /**
974
     * Wrapper for older templates. Previously the new, reply and edit forms were 3 separate
975
     * forms, they have now been refactored into 1 form. But in order to not break existing
976
     * themes too much just include this.
977
     *
978
     * @deprecated 0.5
979
     * @return Form
980
     */
981
    public function ReplyForm()
982
    {
983
        user_error('Please Use $PostMessageForm in your template rather that $ReplyForm', E_USER_WARNING);
984
985
        return $this->PostMessageForm();
986
    }
987
988
    /**
989
     * Post a message to the forum. This method is called whenever you want to make a
990
     * new post or edit an existing post on the forum
991
     *
992
     * @param Array - Data
993
     * @param Form - Submitted Form
994
     */
995
    public function doPostMessageForm($data, $form)
996
    {
997
        $member = Member::currentUser();
998
999
        //Allows interception of a Member posting content to perform some action before the post is made.
1000
        $this->extend('beforePostMessage', $data, $member);
1001
1002
        $content = (isset($data['Content'])) ? $this->filterLanguage($data["Content"]) : "";
1003
        $title = (isset($data['Title'])) ? $this->filterLanguage($data["Title"]) : false;
1004
1005
        // If a thread id is passed append the post to the thread. Otherwise create
1006
        // a new thread
1007
        $thread = false;
1008
        if (isset($data['ThreadID'])) {
1009
            $thread = DataObject::get_by_id('ForumThread', $data['ThreadID']);
1010
        }
1011
1012
        // If this is a simple edit the post then handle it here. Look up the correct post,
1013
        // make sure we have edit rights to it then update the post
1014
        $post = false;
1015
        if (isset($data['ID'])) {
1016
            $post = DataObject::get_by_id('Post', $data['ID']);
1017
1018
            if ($post && $post->isFirstPost()) {
1019
                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...
1020
                    $thread->Title = $title;
1021
                }
1022
            }
1023
        }
1024
1025
1026
        // Check permissions
1027
        $messageSet = array(
1028
            'default' => _t('Forum.LOGINTOPOST', 'You\'ll need to login before you can post to that forum. Please do so below.'),
1029
            'alreadyLoggedIn' => _t('Forum.NOPOSTPERMISSION', 'I\'m sorry, but you do not have permission post to this forum.'),
1030
            '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.'),
1031
        );
1032
1033
        // Creating new thread
1034
        if (!$thread && !$this->canPost()) {
1035
            Security::permissionFailure($this, $messageSet);
1036
            return false;
1037
        }
1038
1039
        // Replying to existing thread
1040 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...
1041
            Security::permissionFailure($this, $messageSet);
1042
            return false;
1043
        }
1044
1045
        // Editing existing post
1046
        if ($thread && $post && !$post->canEdit()) {
1047
            Security::permissionFailure($this, $messageSet);
1048
            return false;
1049
        }
1050
1051
        if (!$thread) {
1052
            $thread = new ForumThread();
1053
            $thread->ForumID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property ForumID does not exist on object<ForumThread>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1054
            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...
1055
                $thread->Title = $title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<ForumThread>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1056
            }
1057
            $starting_thread = true;
1058
        }
1059
1060
        // Upload and Save all files attached to the field
1061
        // Attachment will always be blank, If they had an image it will be at least in Attachment-0
1062
        //$attachments = new DataObjectSet();
1063
        $attachments = new ArrayList();
1064
1065
        if (!empty($data['Attachment-0']) && !empty($data['Attachment-0']['tmp_name'])) {
1066
            $id = 0;
1067
            //
1068
            // @todo this only supports ajax uploads. Needs to change the key (to simply Attachment).
1069
            //
1070
            while (isset($data['Attachment-' . $id])) {
1071
                $image = $data['Attachment-' . $id];
1072
1073
                if ($image && !empty($image['tmp_name'])) {
1074
                    $file = Post_Attachment::create();
1075
                    $file->OwnerID = Member::currentUserID();
1076
                    $folder = Config::inst()->get('ForumHolder', 'attachments_folder');
1077
1078
                    try {
1079
                        $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...
1080
                        $file->write();
1081
                        $attachments->push($file);
1082
                    } catch (ValidationException $e) {
1083
                        $message = _t('Forum.UPLOADVALIDATIONFAIL', 'Unallowed file uploaded. Please only upload files of the following: ');
1084
                        $message .= implode(', ', Config::inst()->get('File', 'allowed_extensions'));
1085
                        $form->addErrorMessage('Attachment', $message, 'bad');
1086
1087
                        Session::set("FormInfo.Form_PostMessageForm.data", $data);
1088
1089
                        return $this->redirectBack();
1090
                    }
1091
                }
1092
1093
                $id++;
1094
            }
1095
        }
1096
1097
        // from now on the user has the correct permissions. save the current thread settings
1098
        $thread->write();
1099
1100
        if (!$post || !$post->canEdit()) {
1101
            $post = new Post();
1102
            $post->AuthorID = ($member) ? $member->ID : 0;
0 ignored issues
show
Documentation introduced by
The property AuthorID does not exist on object<Post>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1103
            $post->ThreadID = $thread->ID;
0 ignored issues
show
Documentation introduced by
The property ThreadID does not exist on object<Post>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1104
        }
1105
1106
        $post->ForumID = $thread->ForumID;
1107
        $post->Content = $content;
1108
        $post->write();
1109
1110
1111
        if ($attachments) {
1112
            foreach ($attachments as $attachment) {
1113
                $attachment->PostID = $post->ID;
1114
                $attachment->write();
1115
            }
1116
        }
1117
1118
        // Add a topic subscription entry if required
1119
        $isSubscribed = ForumThread_Subscription::already_subscribed($thread->ID);
1120
        if (isset($data['TopicSubscription'])) {
1121
            if (!$isSubscribed) {
1122
                // Create a new topic subscription for this member
1123
                $obj = new ForumThread_Subscription();
1124
                $obj->ThreadID = $thread->ID;
0 ignored issues
show
Documentation introduced by
The property ThreadID does not exist on object<ForumThread_Subscription>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1125
                $obj->MemberID = Member::currentUserID();
0 ignored issues
show
Documentation introduced by
The property MemberID does not exist on object<ForumThread_Subscription>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1126
                $obj->write();
1127
            }
1128
        } elseif ($isSubscribed) {
1129
            // See if the member wanted to remove themselves
1130
            DB::query("DELETE FROM \"ForumThread_Subscription\" WHERE \"ThreadID\" = '$post->ThreadID' AND \"MemberID\" = '$member->ID'");
1131
        }
1132
1133
        // Send any notifications that need to be sent
1134
        ForumThread_Subscription::notify($post);
0 ignored issues
show
Compatibility introduced by
$post of type object<DataObject> is not a sub-type of object<Post>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1135
1136
        // Send any notifications to moderators of the forum
1137
        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 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...
1138
            if (isset($starting_thread) && $starting_thread) {
1139
                $this->notifyModerators($post, $thread, true);
1140
            } else {
1141
                $this->notifyModerators($post, $thread);
1142
            }
1143
        }
1144
1145
        return $this->redirect($post->Link());
1146
    }
1147
1148
    /**
1149
     * Send email to moderators notifying them the thread has been created or post added/edited.
1150
     */
1151
    public function notifyModerators($post, $thread, $starting_thread = false)
1152
    {
1153
        $moderators = $this->Moderators();
1154
        if ($moderators && $moderators->exists()) {
1155
            foreach ($moderators as $moderator) {
1156
                if ($moderator->Email) {
1157
                    $adminEmail = Config::inst()->get('Email', 'admin_email');
1158
1159
                    $email = new Email();
1160
                    $email->setFrom($adminEmail);
1161
                    $email->setTo($moderator->Email);
1162
                    if ($starting_thread) {
1163
                        $email->setSubject('New thread "' . $thread->Title . '" in forum ['. $this->Title.']');
1164
                    } else {
1165
                        $email->setSubject('New post "' . $post->Title. '" in forum ['.$this->Title.']');
1166
                    }
1167
                    $email->setTemplate('ForumMember_NotifyModerator');
1168
                    $email->populateTemplate(new ArrayData(array(
1169
                        'NewThread' => $starting_thread,
1170
                        'Moderator' => $moderator,
1171
                        'Author' => $post->Author(),
1172
                        'Forum' => $this,
1173
                        'Post' => $post
1174
                    )));
1175
1176
                    $email->send();
1177
                }
1178
            }
1179
        }
1180
    }
1181
1182
    /**
1183
     * Return the Forbidden Words in this Forum
1184
     *
1185
     * @return Text
1186
     */
1187
    public function getForbiddenWords()
1188
    {
1189
        return $this->Parent()->ForbiddenWords;
1190
    }
1191
1192
    /**
1193
    * This function filters $content by forbidden words, entered in forum holder.
1194
    *
1195
    * @param String $content (it can be Post Content or Post Title)
1196
    * @return String $content (filtered string)
1197
    */
1198
    public function filterLanguage($content)
1199
    {
1200
        $words = $this->getForbiddenWords();
1201
        if ($words != "") {
1202
            $words = explode(",", $words);
1203
            foreach ($words as $word) {
1204
                $content = str_ireplace(trim($word), "*", $content);
1205
            }
1206
        }
1207
1208
        return $content;
1209
    }
1210
1211
    /**
1212
     * Get the link for the reply action
1213
     *
1214
     * @return string URL for the reply action
1215
     */
1216
    public function ReplyLink()
1217
    {
1218
        return $this->Link() . 'reply/' . $this->urlParams['ID'];
1219
    }
1220
1221
    /**
1222
     * Show will get the selected thread to the user. Also increments the forums view count.
1223
     *
1224
     * If the thread does not exist it will pass the user to the 404 error page
1225
     *
1226
     * @return array|SS_HTTPResponse_Exception
1227
     */
1228
    public function show()
1229
    {
1230
        $title = Convert::raw2xml($this->Title);
1231
1232
        if ($thread = $this->getForumThread()) {
1233
            //If there is not first post either the thread has been removed or thread if a banned spammer.
1234
            if (!$thread->getFirstPost()) {
1235
                // don't hide the post for logged in admins or moderators
1236
                $member = Member::currentUser();
1237
                if (!$this->canModerate($member)) {
1238
                    return $this->httpError(404);
1239
                }
1240
            }
1241
1242
            $thread->incNumViews();
1243
1244
            $posts = sprintf(_t('Forum.POSTTOTOPIC', "Posts to the %s topic"), $thread->Title);
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<ForumThread>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1245
1246
            RSSFeed::linkToFeed($this->Link("rss") . '/thread/' . (int) $this->urlParams['ID'], $posts);
1247
1248
            $title = Convert::raw2xml($thread->Title) . ' &raquo; ' . $title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<ForumThread>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1249
            $field = DBField::create_field('HTMLText', $title);
1250
1251
            return array(
1252
                'Thread' => $thread,
1253
                'Title' => $field
1254
            );
1255
        } else {
1256
            // if redirecting post ids to thread id is enabled then we need
1257
            // to check to see if this matches a post and if it does redirect
1258
            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 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...
1259
                if ($post = Post::get()->byID($this->urlParams['ID'])) {
1260
                    return $this->redirect($post->Link(), 301);
1261
                }
1262
            }
1263
        }
1264
1265
        return $this->httpError(404);
1266
    }
1267
1268
    /**
1269
     * Start topic action
1270
     *
1271
     * @return array Returns an array to render the start topic page
1272
     */
1273
    public function starttopic()
1274
    {
1275
        $topic = array(
1276
            'Subtitle' => DBField::create_field('HTMLText', _t('Forum.NEWTOPIC', 'Start a new topic')),
1277
            'Abstract' => DBField::create_field('HTMLText', DataObject::get_one("ForumHolder")->ForumAbstract)
1278
        );
1279
        return $topic;
1280
    }
1281
1282
    /**
1283
     * Get the forum title
1284
     *
1285
     * @return string Returns the forum title
1286
     */
1287
    public function getHolderSubtitle()
1288
    {
1289
        return $this->dbObject('Title');
1290
    }
1291
1292
    /**
1293
     * Get the currently viewed forum. Ensure that the user can access it
1294
     *
1295
     * @return ForumThread
1296
     */
1297
    public function getForumThread()
1298
    {
1299
        if (isset($this->urlParams['ID'])) {
1300
            $SQL_id = Convert::raw2sql($this->urlParams['ID']);
1301
1302
            if (is_numeric($SQL_id)) {
1303
                if ($thread = DataObject::get_by_id('ForumThread', $SQL_id)) {
1304
                    if (!$thread->canView()) {
1305
                        Security::permissionFailure($this);
1306
1307
                        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 Forum_Controller::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...
1308
                    }
1309
1310
                    return $thread;
1311
                }
1312
            }
1313
        }
1314
1315
        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 Forum_Controller::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...
1316
    }
1317
1318
    /**
1319
     * Delete an Attachment
1320
     * Called from the EditPost method. Its Done via Ajax
1321
     *
1322
     * @return boolean
1323
     */
1324
    public function deleteattachment(SS_HTTPRequest $request)
1325
    {
1326
        // Check CSRF token
1327
        if (!SecurityToken::inst()->checkRequest($request)) {
1328
            return $this->httpError(400);
1329
        }
1330
1331
        // check we were passed an id and member is logged in
1332
        if (!isset($this->urlParams['ID'])) {
1333
            return false;
1334
        }
1335
1336
        $file = DataObject::get_by_id("Post_Attachment", (int) $this->urlParams['ID']);
1337
1338
        if ($file && $file->canDelete()) {
1339
            $file->delete();
1340
1341
            return (!Director::is_ajax()) ? $this->redirectBack() : true;
1342
        }
1343
1344
        return false;
1345
    }
1346
1347
    /**
1348
     * Edit post action
1349
     *
1350
     * @return array Returns an array to render the edit post page
1351
     */
1352
    public function editpost()
1353
    {
1354
        return array(
1355
            'Subtitle' => _t('Forum.EDITPOST', 'Edit post')
1356
        );
1357
    }
1358
1359
    /**
1360
     * Get the post edit form if the user has the necessary permissions
1361
     *
1362
     * @return Form
1363
     */
1364
    public function EditForm()
1365
    {
1366
        $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
1367
        $post = DataObject::get_by_id('Post', $id);
1368
1369
        return $this->PostMessageForm(false, $post);
1370
    }
1371
1372
1373
    /**
1374
     * Delete a post via the url.
1375
     *
1376
     * @return bool
1377
     */
1378
    public function deletepost(SS_HTTPRequest $request)
1379
    {
1380
        // Check CSRF token
1381
        if (!SecurityToken::inst()->checkRequest($request)) {
1382
            return $this->httpError(400);
1383
        }
1384
1385
        if (isset($this->urlParams['ID'])) {
1386
            if ($post = DataObject::get_by_id('Post', (int) $this->urlParams['ID'])) {
1387
                if ($post->canDelete()) {
1388
                    // delete the whole thread if this is the first one. The delete action
1389
                    // on thread takes care of the posts.
1390
                    if ($post->isFirstPost()) {
1391
                        $thread = DataObject::get_by_id("ForumThread", $post->ThreadID);
1392
                        $thread->delete();
1393
                    } else {
1394
                        // delete the post
1395
                        $post->delete();
1396
                    }
1397
                }
1398
            }
1399
        }
1400
1401
        return (Director::is_ajax()) ? true : $this->redirect($this->Link());
1402
    }
1403
1404
    /**
1405
     * Returns the Forum Message from Session. This
1406
     * is used for things like Moving thread messages
1407
     * @return String
1408
     */
1409
    public function ForumAdminMsg()
1410
    {
1411
        $message = Session::get('ForumAdminMsg');
1412
        Session::clear('ForumAdminMsg');
1413
1414
        return $message;
1415
    }
1416
1417
1418
    /**
1419
     * Forum Admin Features form.
1420
     * Handles the dropdown to select the new forum category and the checkbox for stickyness
1421
     *
1422
     * @return Form
1423
     */
1424
    public function AdminFormFeatures()
1425
    {
1426
        if (!$this->canModerate()) {
1427
            return;
1428
        }
1429
1430
        $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : false;
1431
1432
        $fields = new FieldList(
1433
            new CheckboxField('IsSticky', _t('Forum.ISSTICKYTHREAD', 'Is this a Sticky Thread?')),
1434
            new CheckboxField('IsGlobalSticky', _t('Forum.ISGLOBALSTICKY', 'Is this a Global Sticky (shown on all forums)')),
1435
            new CheckboxField('IsReadOnly', _t('Forum.ISREADONLYTHREAD', 'Is this a Read only Thread?')),
1436
            new HiddenField("ID", "Thread")
1437
        );
1438
1439
        if (($forums = Forum::get()) && $forums->exists()) {
1440
            $fields->push(new DropdownField("ForumID", _t('Forum.CHANGETHREADFORUM', "Change Thread Forum"), $forums->map('ID', 'Title', 'Select New Category:')), '', null, 'Select New Location:');
0 ignored issues
show
Unused Code introduced by
The call to FieldList::push() has too many arguments starting with ''.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1441
        }
1442
1443
        $actions = new FieldList(
1444
            new FormAction('doAdminFormFeatures', _t('Forum.SAVE', 'Save'))
1445
        );
1446
1447
        $form = new Form($this, 'AdminFormFeatures', $fields, $actions);
1448
1449
        // need this id wrapper since the form method is called on save as
1450
        // well and needs to return a valid form object
1451
        if ($id) {
1452
            $thread = ForumThread::get()->byID($id);
1453
            $form->loadDataFrom($thread);
0 ignored issues
show
Bug introduced by
It seems like $thread defined by \ForumThread::get()->byID($id) on line 1452 can be null; however, Form::loadDataFrom() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1454
        }
1455
1456
        $this->extend('updateAdminFormFeatures', $form);
1457
1458
        return $form;
1459
    }
1460
1461
    /**
1462
     * Process's the moving of a given topic. Has to check for admin privledges,
1463
     * passed an old topic id (post id in URL) and a new topic id
1464
     */
1465
    public function doAdminFormFeatures($data, $form)
1466
    {
1467
        if (isset($data['ID'])) {
1468
            $thread = ForumThread::get()->byID($data['ID']);
1469
1470
            if ($thread) {
1471
                if (!$thread->canModerate()) {
1472
                    return Security::permissionFailure($this);
1473
                }
1474
1475
                $form->saveInto($thread);
1476
                $thread->write();
1477
            }
1478
        }
1479
1480
        return $this->redirect($this->Link());
1481
    }
1482
}
1483
1484
/**
1485
 * This is a DataQuery that allows us to replace the underlying query. Hopefully this will
1486
 * be a native ability in 3.1, but for now we need to.
1487
 * TODO: Remove once API in core
1488
 */
1489
class Forum_DataQuery extends DataQuery
1490
{
1491
    public function __construct($dataClass, $query)
1492
    {
1493
        parent::__construct($dataClass);
1494
        $this->query = $query;
1495
    }
1496
}
1497