Completed
Pull Request — master (#186)
by Daniel
04:29 queued 02:16
created

Forum_Controller   F

Complexity

Total Complexity 149

Size/Duplication

Total Lines 827
Duplicated Lines 7.26 %

Coupling/Cohesion

Components 3
Dependencies 39

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 149
c 1
b 0
f 0
lcom 3
cbo 39
dl 60
loc 827
rs 1.0434

27 Methods

Rating   Name   Duplication   Size   Complexity  
B init() 5 38 6
A rss() 0 3 1
A OpenIDAvailable() 0 3 1
A subscribe() 0 18 4
A unsubscribe() 0 17 3
B ban() 22 22 6
B ghost() 22 22 6
B Posts() 0 45 6
A BBTags() 0 3 1
F PostMessageForm() 7 98 26
A ReplyForm() 0 5 1
F doPostMessageForm() 4 147 35
B notifyModerators() 0 29 6
A getForbiddenWords() 0 3 1
A filterLanguage() 0 11 3
A ReplyLink() 0 3 1
C show() 0 40 8
A starttopic() 0 7 1
A getHolderSubtitle() 0 3 1
B getForumThread() 0 19 5
B deleteattachment() 0 19 6
A editpost() 0 5 1
A EditForm() 0 6 2
C deletepost() 0 25 7
A ForumAdminMsg() 0 6 1
B AdminFormFeatures() 0 33 6
A doAdminFormFeatures() 0 16 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
/**
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
	private static $allowed_children = 'none';
15
16
	private static $icon = "forum/images/treeicons/user";
17
18
	/**
19
	 * Enable this to automatically notify moderators when a message is posted
20
	 * or edited on his forums.
21
	 */
22
	static $notify_moderators = false;
23
24
	private static $db = array(
25
		"Abstract" => "Text",
26
		"CanPostType" => "Enum('Inherit, Anyone, LoggedInUsers, OnlyTheseUsers, NoOne', 'Inherit')",
27
		"CanAttachFiles" => "Boolean",
28
	);
29
30
	private static $has_one = array(
31
		"Moderator" => "Member",
32
		"Category" => "ForumCategory"
33
	);
34
35
	private static $many_many = array(
36
		'Moderators' => 'Member',
37
		'PosterGroups' => 'Group'
38
	);
39
40
	private static $defaults = array(
41
		"ForumPosters" => "LoggedInUsers"
42
	);
43
44
	/**
45
	 * Number of posts to include in the thread view before pagination takes effect.
46
	 *
47
	 * @var int
48
	 */
49
	static $posts_per_page = 8;
50
51
	/**
52
	 * When migrating from older versions of the forum it used post ID as the url token
53
	 * as of forum 1.0 we now use ThreadID. If you want to enable 301 redirects from post to thread ID
54
	 * set this to true
55
	 *
56
	 * @var bool
57
	 */
58
	static $redirect_post_urls_to_thread = false;
59
60
	/**
61
	 * Check if the user can view the forum.
62
	 */
63
	function canView($member = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
64
		if(!$member) $member = Member::currentUser();
65
		return (parent::canView($member) || $this->canModerate($member));
66
	}
67
68
	/**
69
	 * Check if the user can post to the forum and edit his own posts.
70
	 */
71
	function canPost($member = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
72
		if(!$member) $member = Member::currentUser();
73
74
		if($this->CanPostType == "Inherit") {
75
			$holder = $this->getForumHolder();
76
			if ($holder) {
77
				return $holder->canPost($member);
78
			}
79
80
			return false;
81
		}
82
83
		if($this->CanPostType == "NoOne") return false;
84
85
		if($this->CanPostType == "Anyone" || $this->canEdit($member)) return true;
86
87 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...
88
			if($member->IsSuspended()) return false;
89
			if($member->IsBanned()) return false;
90
91
			if($this->CanPostType == "LoggedInUsers") return true;
92
93
			if($groups = $this->PosterGroups()) {
94
				foreach($groups as $group) {
95
					if($member->inGroup($group)) return true;
96
				}
97
			}
98
		}
99
100
		return false;
101
	}
102
103
	/**
104
	 * Check if user has access to moderator panel and can delete posts and threads.
105
	 */
106
	function canModerate($member = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
107
		if(!$member) $member = Member::currentUser();
108
109
		if(!$member) return false;
110
111
		// Admins
112
		if (Permission::checkMember($member, 'ADMIN')) return true;
113
114
		// Moderators
115
		if ($member->isModeratingForum($this)) return true;
116
117
		return false;
118
	}
119
120
	/**
121
	 * Can we attach files to topics/posts inside this forum?
122
	 *
123
	 * @return bool Set to TRUE if the user is allowed to, to FALSE if they're
124
	 *              not
125
	 */
126
	function canAttach($member = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
127
		return $this->CanAttachFiles ? true : false;
128
	}
129
130
	function requireTable() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
131
		// Migrate permission columns
132
		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...
133
			$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...
134
			if(in_array('ForumPosters', array_keys($fields)) && !in_array('CanPostType', array_keys($fields))) {
135
				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...
136
				DB::alteration_message('Migrated forum permissions from "ForumPosters" to "CanPostType"', "created");
137
			}
138
		}
139
140
		parent::requireTable();
141
	}
142
143
	/**
144
	 * Add default records to database
145
	 *
146
	 * This function is called whenever the database is built, after the
147
	 * database tables have all been created.
148
 	 */
149
	public function requireDefaultRecords() {
150
		parent::requireDefaultRecords();
151
152
		$code = "ACCESS_FORUM";
153
		if(!($forumGroup = Group::get()->filter('Code', 'forum-members')->first())) {
154
			$group = new Group();
155
			$group->Code = 'forum-members';
156
			$group->Title = "Forum Members";
157
			$group->write();
158
159
			Permission::grant( $group->ID, $code );
160
			DB::alteration_message(_t('Forum.GROUPCREATED','Forum Members group created'),'created');
161
		}
162
		else if(!Permission::get()->filter(array('GroupID' => $forumGroup->ID, 'Code' => $code))->exists()) {
163
			Permission::grant($forumGroup->ID, $code);
164
		}
165
166
		if(!($category = ForumCategory::get()->first())) {
167
			$category = new ForumCategory();
168
			$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...
169
			$category->write();
170
		}
171
172
		if(!ForumHolder::get()->exists()) {
173
			$forumholder = new ForumHolder();
174
			$forumholder->Title = "Forums";
175
			$forumholder->URLSegment = "forums";
176
			$forumholder->Content = "<p>"._t('Forum.WELCOMEFORUMHOLDER','Welcome to SilverStripe Forum Module! This is the default ForumHolder page. You can now add forums.')."</p>";
177
			$forumholder->Status = "Published";
178
			$forumholder->write();
179
			$forumholder->publish("Stage", "Live");
180
			DB::alteration_message(_t('Forum.FORUMHOLDERCREATED','ForumHolder page created'),"created");
181
182
			$forum = new Forum();
183
			$forum->Title = _t('Forum.TITLE','General Discussion');
184
			$forum->URLSegment = "general-discussion";
185
			$forum->ParentID = $forumholder->ID;
186
			$forum->Content = "<p>"._t('Forum.WELCOMEFORUM','Welcome to SilverStripe Forum Module! This is the default Forum page. You can now add topics.')."</p>";
187
			$forum->Status = "Published";
188
			$forum->CategoryID = $category->ID;
189
			$forum->write();
190
			$forum->publish("Stage", "Live");
191
192
			DB::alteration_message(_t('Forum.FORUMCREATED','Forum page created'),"created");
193
		}
194
 	}
195
196
	/**
197
	 * Check if we can and should show forums in categories
198
	 */
199
	function getShowInCategories() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
200
		$holder = $this->getForumHolder();
201
		if ($holder) {
202
			return $holder->getShowInCategories();
203
		}
204
	}
205
206
	/**
207
	 * Returns a FieldList with which to create the CMS editing form
208
	 *
209
	 * @return FieldList The fields to be displayed in the CMS.
210
	 */
211
	function getCMSFields() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
212
		$self = $this;
213
214
		$this->beforeUpdateCMSFields(function($fields) use ($self) {
215
			Requirements::javascript("forum/javascript/ForumAccess.js");
216
			Requirements::css("forum/css/Forum_CMS.css");
217
218
			$fields->addFieldToTab("Root.Access", new HeaderField(_t('Forum.ACCESSPOST','Who can post to the forum?'), 2));
219
220
			$fields->addFieldToTab("Root.Access", new HeaderField(_t('Forum.ACCESSPOST','Who can post to the forum?'), 2));
221
			$fields->addFieldToTab("Root.Access", $optionSetField = new OptionsetField("CanPostType", "", array(
222
				"Inherit" => "Inherit",
223
				"Anyone" => _t('Forum.READANYONE', 'Anyone'),
224
				"LoggedInUsers" => _t('Forum.READLOGGEDIN', 'Logged-in users'),
225
				"OnlyTheseUsers" => _t('Forum.READLIST', 'Only these people (choose from list)'),
226
				"NoOne" => _t('Forum.READNOONE', 'Nobody. Make Forum Read Only')
227
			)));
228
229
			$optionSetField->addExtraClass('ForumCanPostTypeSelector');
230
231
			$fields->addFieldsToTab("Root.Access", array(
232
				new TreeMultiselectField("PosterGroups", _t('Forum.GROUPS',"Groups")),
233
				new OptionsetField("CanAttachFiles", _t('Forum.ACCESSATTACH','Can users attach files?'), array(
234
					"1" => _t('Forum.YES','Yes'),
235
					"0" => _t('Forum.NO','No')
236
				))
237
			));
238
239
240
			//Dropdown of forum category selection.
241
			$categories = ForumCategory::get()->map();
242
243
			$fields->addFieldsToTab(
244
				"Root.Main",
245
				DropdownField::create('CategoryID', _t('Forum.FORUMCATEGORY', 'Forum Category'), $categories),
246
				'Content'
247
			);
248
249
			//GridField Config - only need to attach or detach Moderators with existing Member accounts.
250
			$moderatorsConfig = GridFieldConfig::create()
251
				->addComponent(new GridFieldButtonRow('before'))
252
				->addComponent(new GridFieldAddExistingAutocompleter('buttons-before-right'))
253
				->addComponent(new GridFieldToolbarHeader())
254
				->addComponent($sort = new GridFieldSortableHeader())
255
				->addComponent($columns = new GridFieldDataColumns())
256
				->addComponent(new GridFieldDeleteAction(true))
257
				->addComponent(new GridFieldPageCount('toolbar-header-right'))
258
				->addComponent($pagination = new GridFieldPaginator());
259
260
			// Use GridField for Moderator management
261
			$moderators = GridField::create(
262
				'Moderators',
263
				_t('MODERATORS', 'Moderators for this forum'),
264
				$self->Moderators(),
265
				$moderatorsConfig
266
				);
267
268
			$columns->setDisplayFields(array(
269
				'Nickname' => 'Nickname',
270
				'FirstName' => 'First name',
271
				'Surname' => 'Surname',
272
				'Email'=> 'Email',
273
				'LastVisited.Long' => 'Last Visit'
274
			));
275
276
			$sort->setThrowExceptionOnBadDataType(false);
277
			$pagination->setThrowExceptionOnBadDataType(false);
278
279
			$fields->addFieldToTab('Root.Moderators', $moderators);
280
		});
281
282
		$fields = parent::getCMSFields();
283
284
		return $fields;
285
	}
286
287
	/**
288
	 * Create breadcrumbs
289
	 *
290
	 * @param int $maxDepth Maximal lenght of the breadcrumb navigation
291
	 * @param bool $unlinked Set to TRUE if the breadcrumb should consist of
292
	 *                       links, otherwise FALSE.
293
	 * @param bool $stopAtPageType Currently not used
294
	 * @param bool $showHidden Set to TRUE if also hidden pages should be
295
	 *                         displayed
296
	 * @return string HTML code to display breadcrumbs
297
	 */
298
	public function Breadcrumbs($maxDepth = null, $unlinked = false, $stopAtPageType = false, $showHidden = false) {
299
		$page = $this;
300
		$nonPageParts = array();
301
		$parts = array();
302
303
		$controller = Controller::curr();
304
		$params = $controller->getURLParams();
305
306
		$forumThreadID = $params['ID'];
307
		if(is_numeric($forumThreadID)) {
308
			if($topic = ForumThread::get()->byID($forumThreadID)) {
309
				$nonPageParts[] = Convert::raw2xml($topic->getTitle());
310
			}
311
		}
312
313
		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...
314
			if($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
315
				if($page->URLSegment == 'home') $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...
316
317
				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...
318
					$parts[] = '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
319
				}
320
				else {
321
					$parts[] = (($page->ID == $this->ID) || $unlinked)
322
							? Convert::raw2xml($page->Title)
323
							: '<a href="' . $page->Link() . '">' . Convert::raw2xml($page->Title) . '</a>';
324
				}
325
			}
326
327
			$page = $page->Parent;
328
		}
329
330
		return implode(" &raquo; ", array_reverse(array_merge($nonPageParts,$parts)));
331
	}
332
333
	/**
334
	 * Helper Method from the template includes. Uses $ForumHolder so in order for it work
335
	 * it needs to be included on this page
336
	 *
337
	 * @return ForumHolder
338
	 */
339
	function getForumHolder() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
340
		$holder = $this->Parent();
341
		if ($holder->ClassName=='ForumHolder') return $holder;
342
	}
343
344
	/**
345
	 * Get the latest posting of the forum. For performance the forum ID is stored on the
346
	 * {@link Post} object as well as the {@link Forum} object
347
	 *
348
	 * @return Post
349
	 */
350
	function getLatestPost() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
351
		return Post::get()->filter('ForumID', $this->ID)->sort('"Post"."ID" DESC')->first();
352
	}
353
354
	/**
355
	 * Get the number of total topics (threads) in this Forum
356
	 *
357
	 * @return int Returns the number of topics (threads)
358
	 */
359
	function getNumTopics() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
360
		$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...
361
		$sqlQuery->setFrom('"Post"');
362
		$sqlQuery->setSelect('COUNT(DISTINCT("ThreadID"))');
363
		$sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
364
		$sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
365
		$sqlQuery->addWhere('"ForumID" = ' . $this->ID);
366
		return $sqlQuery->execute()->value();
367
	}
368
369
	/**
370
	 * Get the number of total posts
371
	 *
372
	 * @return int Returns the number of posts
373
	 */
374
	function getNumPosts() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
375
		$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...
376
		$sqlQuery->setFrom('"Post"');
377
		$sqlQuery->setSelect('COUNT("Post"."ID")');
378
		$sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
379
		$sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
380
		$sqlQuery->addWhere('"ForumID" = ' . $this->ID);
381
		return $sqlQuery->execute()->value();
382
	}
383
384
385
	/**
386
	 * Get the number of distinct Authors
387
	 *
388
	 * @return int
389
	 */
390
	function getNumAuthors() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
391
		$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...
392
		$sqlQuery->setFrom('"Post"');
393
		$sqlQuery->setSelect('COUNT(DISTINCT("AuthorID"))');
394
		$sqlQuery->addInnerJoin('Member', '"Post"."AuthorID" = "Member"."ID"');
395
		$sqlQuery->addWhere('"Member"."ForumStatus" = \'Normal\'');
396
		$sqlQuery->addWhere('"ForumID" = ' . $this->ID);
397
		return $sqlQuery->execute()->value();
398
	}
399
400
	/**
401
	 * Returns the Topics (the first Post of each Thread) for this Forum
402
	 * @return DataList
403
	 */
404
	function getTopics() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
405
		// Get a list of Posts
406
		$posts = Post::get();
407
408
		// Get the underlying query and change it to return the ThreadID and Max(Created) and Max(ID) for each thread
409
		// of those posts
410
		$postQuery = $posts->dataQuery()->query();
411
412
		$postQuery
413
			->setSelect(array())
414
			->selectField('MAX("Post"."Created")', 'PostCreatedMax')
415
			->selectField('MAX("Post"."ID")', 'PostIDMax')
416
			->selectField('"ThreadID"')
417
			->setGroupBy('"ThreadID"')
418
			->addWhere(sprintf('"ForumID" = \'%s\'', $this->ID))
419
			->setDistinct(false);
420
421
		// Get a list of forum threads inside this forum that aren't sticky
422
		$threads = ForumThread::get()->filter(array(
423
			'ForumID' => $this->ID,
424
			'IsGlobalSticky' => 0,
425
			'IsSticky' => 0
426
		));
427
428
		// Get the underlying query and change it to inner join on the posts list to just show threads that
429
		// have approved (and maybe awaiting) posts, and sort the threads by the most recent post
430
		$threadQuery = $threads->dataQuery()->query();
431
		$threadQuery
432
			->addSelect(array('"PostMax"."PostCreatedMax", "PostMax"."PostIDMax"'))
433
			->addFrom('INNER JOIN ('.$postQuery->sql().') AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")')
434
			->addOrderBy(array('"PostMax"."PostCreatedMax" DESC', '"PostMax"."PostIDMax" DESC'))
435
			->setDistinct(false);
436
437
		// Alter the forum threads list to use the new query
438
		$threads = $threads->setDataQuery(new Forum_DataQuery('ForumThread', $threadQuery));
439
440
		// And return the results
441
		return $threads->exists() ? new PaginatedList($threads, $_GET) : null;
442
	}
443
444
445
446
	/*
447
	 * Returns the Sticky Threads
448
	 * @param boolean $include_global Include Global Sticky Threads in the results (default: true)
449
	 * @return DataList
450
	 */
451
	 function getStickyTopics($include_global = true) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
452
		// Get Threads that are sticky & in this forum
453
		$where = '("ForumThread"."ForumID" = '.$this->ID.' AND "ForumThread"."IsSticky" = 1)';
454
		// Get Threads that are globally sticky
455
		if ($include_global) $where .= ' OR ("ForumThread"."IsGlobalSticky" = 1)';
456
457
		// Get the underlying query
458
		$query = ForumThread::get()->where($where)->dataQuery()->query();
459
460
		// Sort by the latest Post in each thread's Created date
461
		$query
462
			->addSelect('"PostMax"."PostMax"')
463
			// TODO: Confirm this works in non-MySQL DBs
464
			->addFrom(sprintf(
465
				'LEFT JOIN (SELECT MAX("Created") AS "PostMax", "ThreadID" FROM "Post" WHERE "ForumID" = \'%s\' GROUP BY "ThreadID") AS "PostMax" ON ("PostMax"."ThreadID" = "ForumThread"."ID")',
466
				$this->ID
467
			))
468
			->addOrderBy('"PostMax"."PostMax" DESC')
469
			->setDistinct(false);
470
471
		// Build result as ArrayList
472
		$res = new ArrayList();
473
		$rows = $query->execute();
474
		if ($rows) foreach ($rows as $row) $res->push(new ForumThread($row));
475
476
		return $res;
477
	}
478
}
479
480
/**
481
 * The forum controller class
482
 *
483
 * @package forum
484
 */
485
class Forum_Controller extends Page_Controller {
486
487
	private static $allowed_actions = array(
488
		'AdminFormFeatures',
489
		'deleteattachment',
490
		'deletepost',
491
		'editpost',
492
		'PostMessageForm',
493
		'reply',
494
		'show',
495
		'starttopic',
496
		'subscribe',
497
		'unsubscribe',
498
		'rss',
499
		'ban',
500
		'ghost'
501
	);
502
503
504
	function init() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
505
		parent::init();
506
		if($this->redirectedTo()) return;
507
508
		Requirements::javascript(THIRDPARTY_DIR . "/jquery/jquery.js");
509
		Requirements::javascript("forum/javascript/Forum.js");
510
		Requirements::javascript("forum/javascript/jquery.MultiFile.js");
511
512
		Requirements::themedCSS('Forum','forum','all');
513
514
		RSSFeed::linkToFeed($this->Parent()->Link("rss/forum/$this->ID"), sprintf(_t('Forum.RSSFORUM',"Posts to the '%s' forum"),$this->Title));
515
	 	RSSFeed::linkToFeed($this->Parent()->Link("rss"), _t('Forum.RSSFORUMS','Posts to all forums'));
516
517
 	  	if(!$this->canView()) {
518
 		  	$messageSet = array(
519
				'default' => _t('Forum.LOGINDEFAULT','Enter your email address and password to view this forum.'),
520
				'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'),
521
				'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.')
522
			);
523
524
			Security::permissionFailure($this, $messageSet);
525
			return;
526
 		}
527
528
		// Log this visit to the ForumMember if they exist
529
		$member = Member::currentUser();
530
		if($member && Config::inst()->get('ForumHolder', 'currently_online_enabled')) {
531
 			$member->LastViewed = date("Y-m-d H:i:s");
532
 			$member->write();
533
 		}
534
535
		// Set the back url
536 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...
537
			Session::set('BackURL', $_SERVER['REQUEST_URI']);
538
		} else {
539
			Session::set('BackURL', $this->Link());
540
		}
541
	}
542
543
	/**
544
	 * A convenience function which provides nice URLs for an rss feed on this forum.
545
	 */
546
	function rss() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
547
		$this->redirect($this->Parent()->Link("rss/forum/$this->ID"), 301);
548
	}
549
550
	/**
551
	 * Is OpenID support available?
552
	 *
553
	 * This method checks if the {@link OpenIDAuthenticator} is available and
554
	 * registered.
555
	 *
556
	 * @return bool Returns TRUE if OpenID is available, FALSE otherwise.
557
	 */
558
	function OpenIDAvailable() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
559
		return $this->Parent()->OpenIDAvailable();
560
	}
561
562
	/**
563
	 * Subscribe a user to a thread given by an ID.
564
	 *
565
	 * Designed to be called via AJAX so return true / false
566
	 *
567
	 * @return bool
568
	 */
569
	function subscribe(SS_HTTPRequest $request) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
570
		// Check CSRF
571
		if (!SecurityToken::inst()->checkRequest($request)) {
572
			return $this->httpError(400);
573
		}
574
575
		if(Member::currentUser() && !ForumThread_Subscription::already_subscribed($this->urlParams['ID'])) {
576
			$obj = new ForumThread_Subscription();
577
			$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...
578
			$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...
579
			$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...
580
			$obj->write();
581
582
			die('1');
583
		}
584
585
		return false;
586
	}
587
588
	/**
589
	 * Unsubscribe a user from a thread by an ID
590
	 *
591
	 * Designed to be called via AJAX so return true / false
592
	 *
593
	 * @return bool
594
	 */
595
	function unsubscribe(SS_HTTPRequest $request) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
596
		$member = Member::currentUser();
597
598
		if(!$member) Security::permissionFailure($this, _t('LOGINTOUNSUBSCRIBE', 'To unsubscribe from that thread, please log in first.'));
599
600
		if(ForumThread_Subscription::already_subscribed($this->urlParams['ID'], $member->ID)) {
601
602
			DB::query("
603
				DELETE FROM \"ForumThread_Subscription\"
604
				WHERE \"ThreadID\" = '". Convert::raw2sql($this->urlParams['ID']) ."'
605
				AND \"MemberID\" = '$member->ID'");
606
607
			die('1');
608
		}
609
610
		return false;
611
	}
612
613 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...
614
		if(!$r->param('ID')) return $this->httpError(404);
615
		if(!$this->canModerate()) return $this->httpError(403);
616
617
		$member = Member::get()->byID($r->param('ID'));
618
		if (!$member || !$member->exists()) return $this->httpError(404);
619
620
		$member->ForumStatus = 'Banned';
621
		$member->write();
622
623
		// Log event
624
		$currentUser = Member::currentUser();
625
		SS_Log::log(sprintf(
626
			'Banned member %s (#%d), by moderator %s (#%d)',
627
			$member->Email,
628
			$member->ID,
629
			$currentUser->Email,
630
			$currentUser->ID
631
		), SS_Log::NOTICE);
632
633
		return ($r->isAjax()) ? true : $this->redirectBack();
634
	}
635
636 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...
637
		if(!$r->param('ID')) return $this->httpError(400);
638
		if(!$this->canModerate()) return $this->httpError(403);
639
640
		$member = Member::get()->byID($r->param('ID'));
641
		if (!$member || !$member->exists()) return $this->httpError(404);
642
643
		$member->ForumStatus = 'Ghost';
644
		$member->write();
645
646
		// Log event
647
		$currentUser = Member::currentUser();
648
		SS_Log::log(sprintf(
649
			'Ghosted member %s (#%d), by moderator %s (#%d)',
650
			$member->Email,
651
			$member->ID,
652
			$currentUser->Email,
653
			$currentUser->ID
654
		), SS_Log::NOTICE);
655
656
		return ($r->isAjax()) ? true : $this->redirectBack();
657
	}
658
659
	/**
660
	 * Get posts to display. This method assumes an URL parameter "ID" which contains the thread ID.
661
	 * @param string sortDirection The sort order direction, either ASC for ascending (default) or DESC for descending
662
	 * @return DataObjectSet Posts
663
	 */
664
	function Posts($sortDirection = "ASC") {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
665
		$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...
666
667
		$posts = Post::get()
668
			->filter('ThreadID', $this->urlParams['ID'])
669
			->sort('Created', $sortDirection);
670
671
		if(isset($_GET['showPost']) && !isset($_GET['start'])) {
672
			$postIDList = clone $posts;
673
			$postIDList = $postIDList->select('ID')->toArray();
674
675
			if($postIDList->exists()) {
676
				$foundPos = array_search($_GET['showPost'], $postIDList);
677
				$_GET['start'] = floor($foundPos / $numPerPage) * $numPerPage;
678
			}
679
		}
680
681
		if(!isset($_GET['start'])) $_GET['start'] = 0;
682
683
		$member = Member::currentUser();
684
685
		/*
686
		 * Don't show posts of banned or ghost members, unless current Member
687
		 * is a ghost member and owner of current post
688
		 */
689
690
		$posts = $posts->exclude(array(
691
			'Author.ForumStatus' => 'Banned'
692
		));
693
694
		if($member) {
695
			$posts = $posts->exclude(array(
696
				'Author.ForumStatus' => 'Ghost',
697
				'Author.ID:not' => $member->ID
698
			));
699
		} else {
700
			$posts = $posts->exclude(array(
701
				'Author.ForumStatus' => 'Ghost'
702
			));
703
		}
704
705
		$paginated = new PaginatedList($posts, $_GET);
706
		$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...
707
		return $paginated;
708
	}
709
710
	/**
711
	 * Get the usable BB codes
712
	 *
713
	 * @return DataObjectSet Returns the usable BB codes
714
	 * @see BBCodeParser::usable_tags()
715
	 */
716
	function BBTags() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
717
		return BBCodeParser::usable_tags();
718
	}
719
720
	/**
721
	 * Section for dealing with reply / edit / create threads form
722
	 *
723
	 * @return Form Returns the post message form
724
	 */
725
	function PostMessageForm($addMode = false, $post = false) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
726
		$thread = false;
727
728
		if($post) {
729
			$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...
730 View Code Duplication
		} else if(isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
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...
731
			$thread = DataObject::get_by_id('ForumThread', $this->urlParams['ID']);
732
		}
733
734
		// Check permissions
735
		$messageSet = array(
736
			'default' => _t('Forum.LOGINTOPOST','You\'ll need to login before you can post to that forum. Please do so below.'),
737
			'alreadyLoggedIn' => _t('Forum.LOGINTOPOSTLOGGEDIN',
738
				 'I\'m sorry, but you can\'t post to this forum until you\'ve logged in.'
739
			  	.'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.'),
740
			'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.'),
741
		);
742
743
		// Creating new thread
744
		if ($addMode && !$this->canPost()) {
745
 			Security::permissionFailure($this, $messageSet);
746
			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...
747
		}
748
749
		// Replying to existing thread
750 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...
751
 			Security::permissionFailure($this, $messageSet);
752
			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...
753
		}
754
755
		// Editing existing post
756
		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...
757
 			Security::permissionFailure($this, $messageSet);
758
			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...
759
		}
760
761
		$forumBBCodeHint = $this->renderWith('Forum_BBCodeHint');
762
763
		$fields = new FieldList(
764
			($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...
765
			new TextareaField("Content", _t('Forum.FORUMREPLYCONTENT', 'Content')),
766
			new LiteralField(
767
				"BBCodeHelper",
768
				"<div class=\"BBCodeHint\">[ <a href=\"#BBTagsHolder\" id=\"BBCodeHint\">" .
769
				_t('Forum.BBCODEHINT','View Formatting Help') .
770
				"</a> ]</div>" .
771
				$forumBBCodeHint
772
			),
773
			new CheckboxField("TopicSubscription",
774
				_t('Forum.SUBSCRIBETOPIC','Subscribe to this topic (Receive email notifications when a new reply is added)'),
775
				($thread) ? $thread->getHasSubscribed() : false)
776
		);
777
778
		if($thread) $fields->push(new HiddenField('ThreadID', 'ThreadID', $thread->ID));
779
		if($post) $fields->push(new HiddenField('ID', 'ID', $post->ID));
780
781
		// Check if we can attach files to this forum's posts
782
		if($this->canAttach()) {
783
			$fields->push(FileField::create("Attachment", _t('Forum.ATTACH', 'Attach file')));
784
		}
785
786
		// If this is an existing post check for current attachments and generate
787
		// a list of the uploaded attachments
788
		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...
789
			if($attachmentList->exists()) {
790
				$attachments = "<div id=\"CurrentAttachments\"><h4>". _t('Forum.CURRENTATTACHMENTS', 'Current Attachments') ."</h4><ul>";
791
				$link = $this->Link();
792
				// An instance of the security token
793
				$token = SecurityToken::inst();
794
795
				foreach($attachmentList as $attachment) {
796
					// Generate a link properly, since it requires a security token
797
					$attachmentLink = Controller::join_links($link, 'deleteattachment', $attachment->ID);
798
					$attachmentLink = $token->addToUrl($attachmentLink);
799
800
					$attachments .= "<li class='attachment-$attachment->ID'>$attachment->Name [<a href='{$attachmentLink}' rel='$attachment->ID' class='deleteAttachment'>"
801
							. _t('Forum.REMOVE','remove') . "</a>]</li>";
802
				}
803
				$attachments .= "</ul></div>";
804
805
				$fields->push(new LiteralField('CurrentAttachments', $attachments));
806
			}
807
		}
808
809
		$actions = new FieldList(
810
			new FormAction("doPostMessageForm", _t('Forum.REPLYFORMPOST', 'Post'))
811
		);
812
813
		$required = $addMode === true ? new RequiredFields("Title", "Content") : new RequiredFields("Content");
814
815
		$form = new Form($this, 'PostMessageForm', $fields, $actions, $required);
816
817
		$this->extend('updatePostMessageForm', $form, $post);
818
819
		if($post) $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...
820
821
		return $form;
822
	}
823
824
	/**
825
	 * Wrapper for older templates. Previously the new, reply and edit forms were 3 separate
826
	 * forms, they have now been refactored into 1 form. But in order to not break existing
827
	 * themes too much just include this.
828
	 *
829
	 * @deprecated 0.5
830
	 * @return Form
831
	 */
832
	function ReplyForm() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
833
		user_error('Please Use $PostMessageForm in your template rather that $ReplyForm', E_USER_WARNING);
834
835
		return $this->PostMessageForm();
836
	}
837
838
	/**
839
	 * Post a message to the forum. This method is called whenever you want to make a
840
	 * new post or edit an existing post on the forum
841
	 *
842
	 * @param Array - Data
843
	 * @param Form - Submitted Form
844
	 */
845
	function doPostMessageForm($data, $form) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
846
		$member = Member::currentUser();
847
848
		//Allows interception of a Member posting content to perform some action before the post is made.
849
		$this->extend('beforePostMessage', $data, $member);
850
851
		$content = (isset($data['Content'])) ? $this->filterLanguage($data["Content"]) : "";
852
		$title = (isset($data['Title'])) ? $this->filterLanguage($data["Title"]) : false;
853
854
		// If a thread id is passed append the post to the thread. Otherwise create
855
		// a new thread
856
		$thread = false;
857
		if(isset($data['ThreadID'])) {
858
			$thread = DataObject::get_by_id('ForumThread', $data['ThreadID']);
859
		}
860
861
		// If this is a simple edit the post then handle it here. Look up the correct post,
862
		// make sure we have edit rights to it then update the post
863
		$post = false;
864
		if(isset($data['ID'])) {
865
			$post = DataObject::get_by_id('Post', $data['ID']);
866
867
			if($post && $post->isFirstPost()) {
868
				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...
869
					$thread->Title = $title;
870
				}
871
			}
872
		}
873
874
875
		// Check permissions
876
		$messageSet = array(
877
			'default' => _t('Forum.LOGINTOPOST','You\'ll need to login before you can post to that forum. Please do so below.'),
878
			'alreadyLoggedIn' => _t('Forum.NOPOSTPERMISSION','I\'m sorry, but you do not have permission post to this forum.'),
879
			'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.'),
880
		);
881
882
		// Creating new thread
883
		if (!$thread && !$this->canPost()) {
884
 			Security::permissionFailure($this, $messageSet);
885
			return false;
886
		}
887
888
		// Replying to existing thread
889 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...
890
 			Security::permissionFailure($this, $messageSet);
891
			return false;
892
		}
893
894
		// Editing existing post
895
		if ($thread && $post && !$post->canEdit()) {
896
 			Security::permissionFailure($this, $messageSet);
897
			return false;
898
		}
899
900
		if(!$thread) {
901
			$thread = new ForumThread();
902
			$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...
903
			if($title) $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...
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...
904
			$starting_thread = true;
905
		}
906
907
		// Upload and Save all files attached to the field
908
		// Attachment will always be blank, If they had an image it will be at least in Attachment-0
909
		//$attachments = new DataObjectSet();
910
		$attachments = new ArrayList();
911
912
		if(!empty($data['Attachment-0']) && !empty($data['Attachment-0']['tmp_name'])) {
913
			$id = 0;
914
			//
915
			// @todo this only supports ajax uploads. Needs to change the key (to simply Attachment).
916
			//
917
			while(isset($data['Attachment-' . $id])) {
918
				$image = $data['Attachment-' . $id];
919
920
				if($image && !empty($image['tmp_name'])) {
921
					$file = Post_Attachment::create();
922
					$file->OwnerID = Member::currentUserID();
923
					$folder = Config::inst()->get('ForumHolder','attachments_folder');
924
925
					try {
926
						$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...
927
						$file->write();
928
						$attachments->push($file);
929
					}
930
					catch(ValidationException $e) {
931
						$message = _t('Forum.UPLOADVALIDATIONFAIL', 'Unallowed file uploaded. Please only upload files of the following: ');
932
						$message .= implode(', ', Config::inst()->get('File', 'allowed_extensions'));
933
						$form->addErrorMessage('Attachment', $message, 'bad');
934
935
						Session::set("FormInfo.Form_PostMessageForm.data", $data);
936
937
						return $this->redirectBack();
938
					}
939
				}
940
941
				$id++;
942
			}
943
		}
944
945
		// from now on the user has the correct permissions. save the current thread settings
946
		$thread->write();
947
948
		if(!$post || !$post->canEdit()) {
949
			$post = new Post();
950
			$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...
951
			$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...
952
		}
953
954
		$post->ForumID = $thread->ForumID;
955
		$post->Content = $content;
956
		$post->write();
957
958
959
		if($attachments) {
960
			foreach($attachments as $attachment) {
961
				$attachment->PostID = $post->ID;
962
				$attachment->write();
963
			}
964
		}
965
966
		// Add a topic subscription entry if required
967
		$isSubscribed = ForumThread_Subscription::already_subscribed($thread->ID);
968
		if(isset($data['TopicSubscription'])) {
969
			if(!$isSubscribed) {
970
				// Create a new topic subscription for this member
971
				$obj = new ForumThread_Subscription();
972
				$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...
973
				$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...
974
				$obj->write();
975
			}
976
		} elseif($isSubscribed) {
977
			// See if the member wanted to remove themselves
978
			DB::query("DELETE FROM \"ForumThread_Subscription\" WHERE \"ThreadID\" = '$post->ThreadID' AND \"MemberID\" = '$member->ID'");
979
		}
980
981
		// Send any notifications that need to be sent
982
		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...
983
984
		// Send any notifications to moderators of the forum
985
		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...
986
			if(isset($starting_thread) && $starting_thread) $this->notifyModerators($post, $thread, true);
987
			else $this->notifyModerators($post, $thread);
988
		}
989
990
		return $this->redirect($post->Link());
991
	}
992
993
	/**
994
	 * Send email to moderators notifying them the thread has been created or post added/edited.
995
	 */
996
	function notifyModerators($post, $thread, $starting_thread = false) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
997
		$moderators = $this->Moderators();
998
		if($moderators && $moderators->exists()) {
999
			foreach($moderators as $moderator){
1000
				if($moderator->Email){
1001
					$adminEmail = Config::inst()->get('Email', 'admin_email');
1002
1003
					$email = new Email();
1004
					$email->setFrom($adminEmail);
1005
					$email->setTo($moderator->Email);
1006
					if($starting_thread){
1007
						$email->setSubject('New thread "' . $thread->Title . '" in forum ['. $this->Title.']');
1008
					}else{
1009
						$email->setSubject('New post "' . $post->Title. '" in forum ['.$this->Title.']');
1010
					}
1011
					$email->setTemplate('ForumMember_NotifyModerator');
1012
					$email->populateTemplate(new ArrayData(array(
1013
						'NewThread' => $starting_thread,
1014
						'Moderator' => $moderator,
1015
						'Author' => $post->Author(),
1016
						'Forum' => $this,
1017
						'Post' => $post
1018
					)));
1019
1020
					$email->send();
1021
				}
1022
			}
1023
		}
1024
	}
1025
1026
	/**
1027
	 * Return the Forbidden Words in this Forum
1028
	 *
1029
	 * @return Text
1030
	 */
1031
	function getForbiddenWords() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1032
		return $this->Parent()->ForbiddenWords;
1033
	}
1034
1035
	/**
1036
	* This function filters $content by forbidden words, entered in forum holder.
1037
	*
1038
	* @param String $content (it can be Post Content or Post Title)
1039
	* @return String $content (filtered string)
1040
	*/
1041
	function filterLanguage($content) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1042
		$words = $this->getForbiddenWords();
1043
		if($words != ""){
1044
			$words = explode(",",$words);
1045
			foreach($words as $word){
1046
				$content = str_ireplace(trim($word),"*",$content);
1047
			}
1048
		}
1049
1050
		return $content;
1051
	}
1052
1053
	/**
1054
	 * Get the link for the reply action
1055
	 *
1056
	 * @return string URL for the reply action
1057
	 */
1058
	function ReplyLink() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1059
		return $this->Link() . 'reply/' . $this->urlParams['ID'];
1060
	}
1061
1062
	/**
1063
	 * Show will get the selected thread to the user. Also increments the forums view count.
1064
	 *
1065
	 * If the thread does not exist it will pass the user to the 404 error page
1066
	 *
1067
	 * @return array|SS_HTTPResponse_Exception
1068
	 */
1069
 	function show() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1070
		$title = Convert::raw2xml($this->Title);
1071
1072
		if($thread = $this->getForumThread()) {
1073
1074
			//If there is not first post either the thread has been removed or thread if a banned spammer.
1075
			if(!$thread->getFirstPost()){
1076
				// don't hide the post for logged in admins or moderators
1077
				$member = Member::currentUser();
1078
				if(!$this->canModerate($member)) {
1079
					return $this->httpError(404);
1080
				}
1081
			}
1082
1083
			$thread->incNumViews();
1084
1085
			$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...
1086
1087
			RSSFeed::linkToFeed($this->Link("rss") . '/thread/' . (int) $this->urlParams['ID'], $posts);
1088
1089
			$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...
1090
			$field = DBField::create_field('HTMLText', $title);
1091
1092
			return array(
1093
				'Thread' => $thread,
1094
				'Title' => $field
1095
			);
1096
		}
1097
		else {
1098
			// if redirecting post ids to thread id is enabled then we need
1099
			// to check to see if this matches a post and if it does redirect
1100
			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...
1101
				if($post = Post::get()->byID($this->urlParams['ID'])) {
1102
					return $this->redirect($post->Link(), 301);
1103
				}
1104
			}
1105
		}
1106
1107
		return $this->httpError(404);
1108
	}
1109
1110
	/**
1111
	 * Start topic action
1112
	 *
1113
	 * @return array Returns an array to render the start topic page
1114
	 */
1115
	function starttopic() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1116
		$topic = array(
1117
			'Subtitle' => DBField::create_field('HTMLText', _t('Forum.NEWTOPIC','Start a new topic')),
1118
			'Abstract' => DBField::create_field('HTMLText', DataObject::get_one("ForumHolder")->ForumAbstract)
1119
		);
1120
		return $topic;
1121
	}
1122
1123
	/**
1124
	 * Get the forum title
1125
	 *
1126
	 * @return string Returns the forum title
1127
	 */
1128
	function getHolderSubtitle() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1129
		return $this->dbObject('Title');
1130
	}
1131
1132
	/**
1133
	 * Get the currently viewed forum. Ensure that the user can access it
1134
	 *
1135
	 * @return ForumThread
1136
	 */
1137
	function getForumThread() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1138
		if(isset($this->urlParams['ID'])) {
1139
			$SQL_id = Convert::raw2sql($this->urlParams['ID']);
1140
1141
			if(is_numeric($SQL_id)) {
1142
				if($thread = DataObject::get_by_id('ForumThread', $SQL_id)) {
1143
					if(!$thread->canView()) {
1144
						Security::permissionFailure($this);
1145
1146
						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...
1147
					}
1148
1149
					return $thread;
1150
				}
1151
			}
1152
		}
1153
1154
		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...
1155
	}
1156
1157
	/**
1158
	 * Delete an Attachment
1159
	 * Called from the EditPost method. Its Done via Ajax
1160
	 *
1161
	 * @return boolean
1162
	 */
1163
	function deleteattachment(SS_HTTPRequest $request) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1164
		// Check CSRF token
1165
		if (!SecurityToken::inst()->checkRequest($request)) {
1166
			return $this->httpError(400);
1167
		}
1168
1169
		// check we were passed an id and member is logged in
1170
		if(!isset($this->urlParams['ID'])) return false;
1171
1172
		$file = DataObject::get_by_id("Post_Attachment", (int) $this->urlParams['ID']);
1173
1174
		if($file && $file->canDelete()) {
1175
			$file->delete();
1176
1177
			return (!Director::is_ajax()) ? $this->redirectBack() : true;
1178
		}
1179
1180
		return false;
1181
	}
1182
1183
	/**
1184
	 * Edit post action
1185
	 *
1186
	 * @return array Returns an array to render the edit post page
1187
	 */
1188
	function editpost() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1189
		return array(
1190
			'Subtitle' => _t('Forum.EDITPOST','Edit post')
1191
		);
1192
	}
1193
1194
	/**
1195
	 * Get the post edit form if the user has the necessary permissions
1196
	 *
1197
	 * @return Form
1198
	 */
1199
	function EditForm() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1200
		$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
1201
		$post = DataObject::get_by_id('Post', $id);
1202
1203
		return $this->PostMessageForm(false, $post);
1204
	}
1205
1206
1207
	/**
1208
	 * Delete a post via the url.
1209
	 *
1210
	 * @return bool
1211
	 */
1212
	function deletepost(SS_HTTPRequest $request) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1213
		// Check CSRF token
1214
		if (!SecurityToken::inst()->checkRequest($request)) {
1215
			return $this->httpError(400);
1216
		}
1217
1218
		if(isset($this->urlParams['ID'])) {
1219
			if($post = DataObject::get_by_id('Post', (int) $this->urlParams['ID'])) {
1220
				if($post->canDelete()) {
1221
					// delete the whole thread if this is the first one. The delete action
1222
					// on thread takes care of the posts.
1223
					if($post->isFirstPost()) {
1224
						$thread = DataObject::get_by_id("ForumThread", $post->ThreadID);
1225
						$thread->delete();
1226
					}
1227
					else {
1228
						// delete the post
1229
						$post->delete();
1230
					}
1231
				}
1232
			}
1233
	  	}
1234
1235
		return (Director::is_ajax()) ? true : $this->redirect($this->Link());
1236
	}
1237
1238
	/**
1239
	 * Returns the Forum Message from Session. This
1240
	 * is used for things like Moving thread messages
1241
	 * @return String
1242
	 */
1243
	function ForumAdminMsg() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1244
		$message = Session::get('ForumAdminMsg');
1245
		Session::clear('ForumAdminMsg');
1246
1247
		return $message;
1248
	}
1249
1250
1251
	/**
1252
	 * Forum Admin Features form.
1253
	 * Handles the dropdown to select the new forum category and the checkbox for stickyness
1254
	 *
1255
	 * @return Form
1256
	 */
1257
	function AdminFormFeatures() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1258
		if (!$this->canModerate()) return;
1259
1260
		$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : false;
1261
1262
		$fields = new FieldList(
1263
			new CheckboxField('IsSticky', _t('Forum.ISSTICKYTHREAD','Is this a Sticky Thread?')),
1264
			new CheckboxField('IsGlobalSticky', _t('Forum.ISGLOBALSTICKY','Is this a Global Sticky (shown on all forums)')),
1265
			new CheckboxField('IsReadOnly', _t('Forum.ISREADONLYTHREAD','Is this a Read only Thread?')),
1266
			new HiddenField("ID", "Thread")
1267
		);
1268
1269
		if(($forums = Forum::get()) && $forums->exists()) {
1270
			$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...
1271
		}
1272
1273
		$actions = new FieldList(
1274
			new FormAction('doAdminFormFeatures', _t('Forum.SAVE', 'Save'))
1275
		);
1276
1277
		$form = new Form($this, 'AdminFormFeatures', $fields, $actions);
1278
1279
		// need this id wrapper since the form method is called on save as
1280
		// well and needs to return a valid form object
1281
		if($id) {
1282
			$thread = ForumThread::get()->byID($id);
1283
			$form->loadDataFrom($thread);
0 ignored issues
show
Bug introduced by
It seems like $thread defined by \ForumThread::get()->byID($id) on line 1282 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...
1284
		}
1285
		
1286
		$this->extend('updateAdminFormFeatures', $form);
1287
		
1288
		return $form;
1289
	}
1290
1291
	/**
1292
	 * Process's the moving of a given topic. Has to check for admin privledges,
1293
	 * passed an old topic id (post id in URL) and a new topic id
1294
	 */
1295
	function doAdminFormFeatures($data, $form) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1296
		if(isset($data['ID'])) {
1297
			$thread = ForumThread::get()->byID($data['ID']);
1298
1299
			if($thread) {
1300
				if (!$thread->canModerate()) {
1301
					return Security::permissionFailure($this);
1302
				}
1303
1304
				$form->saveInto($thread);
1305
				$thread->write();
1306
			}
1307
		}
1308
1309
		return $this->redirect($this->Link());
1310
	}
1311
}
1312
1313
/**
1314
 * This is a DataQuery that allows us to replace the underlying query. Hopefully this will
1315
 * be a native ability in 3.1, but for now we need to.
1316
 * TODO: Remove once API in core
1317
 */
1318
class Forum_DataQuery extends DataQuery {
1319
	function __construct($dataClass, $query) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1320
		parent::__construct($dataClass);
1321
		$this->query = $query;
1322
	}
1323
}
1324