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

ForumRole   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 1

Importance

Changes 0
Metric Value
wmc 62
lcom 0
cbo 1
dl 0
loc 408
rs 3.8461
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A augmentSQL() 0 3 1
A augmentDatabase() 0 21 3
A onBeforeDelete() 0 9 3
A ForumRank() 0 9 3
A FirstNamePublic() 0 4 2
A SurnamePublic() 0 4 2
A OccupationPublic() 0 4 2
A CompanyPublic() 0 4 2
A CityPublic() 0 4 2
A CountryPublic() 0 4 2
A EmailPublic() 0 4 2
A FullCountry() 0 6 1
A NumPosts() 0 8 2
A isModeratingForum() 0 5 2
A Link() 0 4 1
B getForumFields() 0 63 5
A getForumValidator() 0 11 2
A updateCMSFields() 0 20 3
A IsSuspended() 0 8 2
A IsBanned() 0 4 1
A IsGhost() 0 4 2
A canEdit() 0 16 4
A Nickname() 0 10 4
C getFormattedAvatar() 0 39 7
A ForumSuspensionMessage() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like ForumRole 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 ForumRole, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Forum\Extensions;
4
5
use SilverStripe\ORM\DB;
6
use SilverStripe\Security\Permission;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\Forms\FileField;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Forms\HeaderField;
11
use SilverStripe\Forms\LiteralField;
12
use SilverStripe\Forms\TextField;
13
use SilverStripe\Forms\EmailField;
14
use SilverStripe\Forms\ConfirmedPasswordField;
15
use SilverStripe\Forms\CompositeField;
16
use SilverStripe\Forms\ReadonlyField;
17
use SilverStripe\Forms\FieldList;
18
use SilverStripe\Forms\RequiredFields;
19
use SilverStripe\Forms\CheckboxSetField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\ORM\FieldType\DBDatetime;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Assets\Image;
24
use SilverStripe\ORM\DataExtension;
25
use SilverStripe\View\Requirements;
26
use SilverStripe\Core\Extension;
27
use Zend_Locale;
28
29
/**
30
 * ForumRole
31
 *
32
 * This decorator adds the needed fields and methods to the {@link Member}
33
 * object.
34
 *
35
 * @package forum
36
 */
37
class ForumRole extends DataExtension
38
{
39
40
    /**
41
     * Edit the given query object to support queries for this extension
42
     */
43
    public function augmentSQL(SQLQuery &$query)
44
    {
45
    }
46
47
48
    /**
49
     * Update the database schema as required by this extension
50
     */
51
    public function augmentDatabase()
52
    {
53
        $exist = DB::tableList();
54
        if (!empty($exist) && array_search('ForumMember', $exist) !== false) {
55
            DB::query("UPDATE \"Member\", \"ForumMember\" " .
56
                "SET \"Member\".\"ClassName\" = 'Member'," .
57
                "\"Member\".\"ForumRank\" = \"ForumMember\".\"ForumRank\"," .
58
                "\"Member\".\"Occupation\" = \"ForumMember\".\"Occupation\"," .
59
                "\"Member\".\"Country\" = \"ForumMember\".\"Country\"," .
60
                "\"Member\".\"Nickname\" = \"ForumMember\".\"Nickname\"," .
61
                "\"Member\".\"FirstNamePublic\" = \"ForumMember\".\"FirstNamePublic\"," .
62
                "\"Member\".\"SurnamePublic\" = \"ForumMember\".\"SurnamePublic\"," .
63
                "\"Member\".\"OccupationPublic\" = \"ForumMember\".\"OccupationPublic\"," .
64
                "\"Member\".\"CountryPublic\" = \"ForumMember\".\"CountryPublic\"," .
65
                "\"Member\".\"EmailPublic\" = \"ForumMember\".\"EmailPublic\"," .
66
                "\"Member\".\"AvatarID\" = \"ForumMember\".\"AvatarID\"," .
67
                "\"Member\".\"LastViewed\" = \"ForumMember\".\"LastViewed\"" .
68
                "WHERE \"Member\".\"ID\" = \"ForumMember\".\"ID\"");
69
            echo("<div style=\"padding:5px; color:white; background-color:blue;\">" . _t('ForumRole.TRANSFERSUCCEEDED', 'The data transfer has succeeded. However, to complete it, you must delete the ForumMember table. To do this, execute the query \"DROP TABLE \'ForumMember\'\".') . "</div>" );
70
        }
71
    }
72
73
    private static $db =  array(
74
        'ForumRank' => 'Varchar',
75
        'Occupation' => 'Varchar',
76
        'Company' => 'Varchar',
77
        'City' => 'Varchar',
78
        'Country' => 'Varchar',
79
        'Nickname' => 'Varchar',
80
        'FirstNamePublic' => 'Boolean',
81
        'SurnamePublic' => 'Boolean',
82
        'OccupationPublic' => 'Boolean',
83
        'CompanyPublic' => 'Boolean',
84
        'CityPublic' => 'Boolean',
85
        'CountryPublic' => 'Boolean',
86
        'EmailPublic' => 'Boolean',
87
        'LastViewed' => 'SS_Datetime',
88
        'Signature' => 'Text',
89
        'ForumStatus' => 'Enum("Normal, Banned, Ghost", "Normal")',
90
        'SuspendedUntil' => 'Date'
91
    );
92
93
    private static $has_one = array(
94
        'Avatar' => 'SilverStripe\\Assets\\Image'
95
    );
96
97
    private static $has_many = array(
98
        'ForumPosts' => 'Post'
99
    );
100
101
    private static $belongs_many_many = array(
102
        'ModeratedForums' => 'Forum'
103
    );
104
105
    private static $defaults = array(
106
        'ForumRank' => 'Community Member'
107
    );
108
109
    private static $searchable_fields = array(
110
        'Nickname' => true
111
    );
112
113
    private static $indexes = array(
114
        'Nickname' => true
115
    );
116
117
    private static $field_labels = array(
118
        'SuspendedUntil' => "Suspend this member from writing on forums until the specified date"
119
    );
120
121
    public function onBeforeDelete()
122
    {
123
        parent::onBeforeDelete();
124
125
        $avatar = $this->owner->Avatar();
126
        if ($avatar && $avatar->exists()) {
127
            $avatar->delete();
128
        }
129
    }
130
131
    public function ForumRank()
132
    {
133
        $moderatedForums = $this->owner->ModeratedForums();
134
        if ($moderatedForums && $moderatedForums->Count() > 0) {
135
            return _t('MODERATOR', 'Forum Moderator');
136
        } else {
137
            return $this->owner->getField('ForumRank');
138
        }
139
    }
140
141
    public function FirstNamePublic()
142
    {
143
        return $this->owner->FirstNamePublic || Permission::check('ADMIN');
144
    }
145
    public function SurnamePublic()
146
    {
147
        return $this->owner->SurnamePublic || Permission::check('ADMIN');
148
    }
149
    public function OccupationPublic()
150
    {
151
        return $this->owner->OccupationPublic || Permission::check('ADMIN');
152
    }
153
    public function CompanyPublic()
154
    {
155
        return $this->owner->CompanyPublic || Permission::check('ADMIN');
156
    }
157
    public function CityPublic()
158
    {
159
        return $this->owner->CityPublic || Permission::check('ADMIN');
160
    }
161
    public function CountryPublic()
162
    {
163
        return $this->owner->CountryPublic || Permission::check('ADMIN');
164
    }
165
    public function EmailPublic()
166
    {
167
        return $this->owner->EmailPublic || Permission::check('ADMIN');
168
    }
169
    /**
170
     * Run the Country code through a converter to get the proper Country Name
171
     */
172
    public function FullCountry()
173
    {
174
        $locale = new Zend_Locale();
175
        $locale->setLocale($this->owner->Country);
176
        return $locale->getRegion();
177
    }
178
    public function NumPosts()
179
    {
180
        if (is_numeric($this->owner->ID)) {
181
            return $this->owner->ForumPosts()->Count();
182
        } else {
183
            return 0;
184
        }
185
    }
186
187
    /**
188
     * Checks if the current user is a moderator of the
189
     * given forum by looking in the moderator ID list.
190
     *
191
     * @param Forum object to check
192
     * @return boolean
193
     */
194
    public function isModeratingForum($forum)
195
    {
196
        $moderatorIds = $forum->Moderators() ? $forum->Moderators()->getIdList() : array();
197
        return in_array($this->owner->ID, $moderatorIds);
198
    }
199
200
    public function Link()
201
    {
202
        return "ForumMemberProfile/show/" . $this->owner->ID;
203
    }
204
205
206
    /**
207
     * Get the fields needed by the forum module
208
     *
209
     * @param bool $showIdentityURL Should a field for an OpenID or an i-name
210
     *                              be shown (always read-only)?
211
     * @return FieldList Returns a FieldList containing all needed fields for
212
     *                  the registration of new users
213
     */
214
    public function getForumFields($showIdentityURL = false, $addmode = false)
215
    {
216
        $gravatarText = (DataObject::get_one("ForumHolder", "\"AllowGravatars\" = 1")) ? '<small>'. _t('ForumRole.CANGRAVATAR', 'If you use Gravatars then leave this blank') .'</small>' : "";
217
218
        //Sets the upload folder to the Configurable one set via the ForumHolder or overridden via Config::inst()->update().
219
        $avatarField = new FileField('Avatar', _t('ForumRole.AVATAR', 'Avatar Image') .' '. $gravatarText);
220
        $avatarField->setFolderName(Config::inst()->get('ForumHolder', 'avatars_folder'));
221
        $avatarField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'gif', 'png'));
222
223
        $personalDetailsFields = new CompositeField(
224
            new HeaderField("PersonalDetails", _t('ForumRole.PERSONAL', 'Personal Details')),
225
            new LiteralField("Blurb", "<p id=\"helpful\">" . _t('ForumRole.TICK', 'Tick the fields to show in public profile') . "</p>"),
226
            new TextField("Nickname", _t('ForumRole.NICKNAME', 'Nickname')),
227
            new CheckableOption("FirstNamePublic", new TextField("FirstName", _t('ForumRole.FIRSTNAME', 'First name'))),
228
            new CheckableOption("SurnamePublic", new TextField("Surname", _t('ForumRole.SURNAME', 'Surname'))),
229
            new CheckableOption("OccupationPublic", new TextField("Occupation", _t('ForumRole.OCCUPATION', 'Occupation')), true),
230
            new CheckableOption('CompanyPublic', new TextField('Company', _t('ForumRole.COMPANY', 'Company')), true),
231
            new CheckableOption('CityPublic', new TextField('City', _t('ForumRole.CITY', 'City')), true),
232
            new CheckableOption("CountryPublic", new ForumCountryDropdownField("Country", _t('ForumRole.COUNTRY', 'Country')), true),
233
            new CheckableOption("EmailPublic", new EmailField("SilverStripe\\Control\\Email\\Email", _t('ForumRole.EMAIL', 'SilverStripe\\Control\\Email\\Email'))),
234
            new ConfirmedPasswordField("Password", _t('ForumRole.PASSWORD', 'Password')),
235
            $avatarField
236
        );
237
        // Don't show 'forum rank' at registration
238
        if (!$addmode) {
239
            $personalDetailsFields->push(
240
                new ReadonlyField("ForumRank", _t('ForumRole.RATING', 'User rating'))
241
            );
242
        }
243
        $personalDetailsFields->setID('PersonalDetailsFields');
244
245
        $fieldset = new FieldList(
246
            $personalDetailsFields
247
        );
248
249
        if ($showIdentityURL) {
250
            $fieldset->insertBefore(
251
                new ReadonlyField('IdentityURL', _t('ForumRole.OPENIDINAME', 'OpenID/i-name')),
252
                'Password'
253
            );
254
            $fieldset->insertAfter(
255
                new LiteralField(
256
                    'PasswordOptionalMessage',
257
                    '<p>' . _t('ForumRole.PASSOPTMESSAGE', 'Since you provided an OpenID respectively an i-name the password is optional. If you enter one, you will be able to log in also with your e-mail address.') . '</p>'
258
                ),
259
                'IdentityURL'
260
            );
261
        }
262
263
        if ($this->owner->IsSuspended()) {
264
            $fieldset->insertAfter(
265
                new LiteralField(
266
                    'SuspensionNote',
267
                    '<p class="message warning suspensionWarning">' . $this->ForumSuspensionMessage() . '</p>'
268
                ),
269
                'Blurb'
270
            );
271
        }
272
273
        $this->owner->extend('updateForumFields', $fieldset);
274
275
        return $fieldset;
276
    }
277
278
    /**
279
     * Get the fields needed by the forum module
280
     *
281
     * @param bool $needPassword Should a password be required?
282
     * @return Validator Returns a Validator for the fields required for the
283
     *                              registration of new users
284
     */
285
    public function getForumValidator($needPassword = true)
286
    {
287
        if ($needPassword) {
288
            $validator = new RequiredFields("Nickname", "SilverStripe\\Control\\Email\\Email", "Password");
289
        } else {
290
            $validator = new RequiredFields("Nickname", "SilverStripe\\Control\\Email\\Email");
291
        }
292
        $this->owner->extend('updateForumValidator', $validator);
293
294
        return $validator;
295
    }
296
297
    public function updateCMSFields(FieldList $fields)
298
    {
299
        $allForums = DataObject::get('Forum');
300
        $fields->removeByName('ModeratedForums');
301
        $fields->addFieldToTab('Root.ModeratedForums', new CheckboxSetField('ModeratedForums', _t('ForumRole.MODERATEDFORUMS', 'Moderated forums'), ($allForums->exists() ? $allForums->map('ID', 'Title') : array())));
302
        $suspend = $fields->dataFieldByName('SuspendedUntil');
303
        $suspend->setConfig('showcalendar', true);
304
        if (Permission::checkMember($this->owner->ID, "ACCESS_FORUM")) {
305
            $avatarField = new FileField('Avatar', _t('ForumRole.UPLOADAVATAR', 'Upload avatar'));
306
            $avatarField->getValidator()->setAllowedExtensions(array('jpg', 'jpeg', 'gif', 'png'));
307
308
            $fields->addFieldToTab('Root.Forum', $avatarField);
309
            $fields->addFieldToTab('Root.Forum', new DropdownField("ForumRank", _t('ForumRole.FORUMRANK', "User rating"), array(
310
                "Community Member" => _t('ForumRole.COMMEMBER'),
311
                "Administrator" => _t('ForumRole.ADMIN', 'Administrator'),
312
                "Moderator" => _t('ForumRole.MOD', 'Moderator')
313
            )));
314
            $fields->addFieldToTab('Root.Forum', $this->owner->dbObject('ForumStatus')->scaffoldFormField());
315
        }
316
    }
317
318
    public function IsSuspended()
319
    {
320
        if ($this->owner->SuspendedUntil) {
321
            return strtotime(DBDatetime::now()->Format('Y-m-d')) < strtotime($this->owner->SuspendedUntil);
322
        } else {
323
            return false;
324
        }
325
    }
326
327
    public function IsBanned()
328
    {
329
        return $this->owner->ForumStatus == 'Banned';
330
    }
331
332
    public function IsGhost()
333
    {
334
        return $this->owner->ForumStatus == 'Ghost' && $this->owner->ID !== Member::currentUserID();
335
    }
336
337
    /**
338
     * Can the current user edit the given member?
339
     *
340
     * @return true if this member can be edited, false otherwise
341
     */
342
    public function canEdit($member = null)
343
    {
344
        if (!$member) {
345
            $member = Member::currentUser();
346
        }
347
348
        if ($this->owner->ID == Member::currentUserID()) {
349
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type documented by SilverStripe\Forum\Extensions\ForumRole::canEdit of type SilverStripe\Forum\Extensions\true.

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...
350
        }
351
352
        if ($member) {
353
            return $member->can('AdminCMS');
354
        }
355
356
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Forum\Extensions\ForumRole::canEdit of type SilverStripe\Forum\Extensions\true.

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...
357
    }
358
359
360
    /**
361
     * Used in preference to the Nickname field on templates
362
     *
363
     * Provides a default for the nickname field (first name, or "Anonymous
364
     * User" if that's not set)
365
     */
366
    public function Nickname()
367
    {
368
        if ($this->owner->Nickname) {
369
            return $this->owner->Nickname;
370
        } elseif ($this->owner->FirstNamePublic && $this->owner->FirstName) {
371
            return $this->owner->FirstName;
372
        } else {
373
            return _t('ForumRole.ANONYMOUS', 'Anonymous user');
374
        }
375
    }
376
377
    /**
378
     * Return the url of the avatar or gravatar of the selected user.
379
     * Checks to see if the current user has an avatar, if they do use it
380
     * otherwise query gravatar.com
381
     *
382
     * @return String
383
     */
384
    public function getFormattedAvatar()
385
    {
386
        $default = "forum/images/forummember_holder.gif";
387
        $currentTheme = Config::inst()->get('SilverStripe\\View\\SSViewer', 'theme');
388
389
        if (file_exists('themes/' . $currentTheme . '_forum/images/forummember_holder.gif')) {
390
            $default = 'themes/' . $currentTheme . '_forum/images/forummember_holder.gif';
391
        }
392
        // if they have uploaded an image
393
        if ($this->owner->AvatarID) {
394
            $avatar = Image::get()->byID($this->owner->AvatarID);
395
            if (!$avatar) {
396
                return $default;
397
            }
398
399
            $resizedAvatar = $avatar->SetWidth(80);
400
            if (!$resizedAvatar) {
401
                return $default;
402
            }
403
404
            return $resizedAvatar->URL;
405
        }
406
407
        //If Gravatar is enabled, allow the selection of the type of default Gravatar.
408
        if ($holder = ForumHolder::get()->filter('AllowGravatars', 1)->first()) {
409
            // If the GravatarType is one of the special types, then set it otherwise use the
410
            //default image from above forummember_holder.gif
411
            if ($holder->GravatarType) {
412
                $default = $holder->GravatarType;
413
            } else {
414
                // we need to get the absolute path for the default forum image
415
                return $default;
416
            }
417
            // ok. no image but can we find a gravatar. Will return the default image as defined above if not.
418
            return "http://www.gravatar.com/avatar/".md5($this->owner->Email)."?default=".urlencode($default)."&amp;size=80";
419
        }
420
421
        return $default;
422
    }
423
424
    /**
425
     * Conditionally includes admin email address (hence we can't simply generate this
426
     * message in templates). We don't need to spam protect the email address as
427
     * the note only shows to logged-in users.
428
     *
429
     * @return String
430
     */
431
    public function ForumSuspensionMessage()
432
    {
433
        $msg = _t('ForumRole.SUSPENSIONNOTE', 'This forum account has been suspended.');
434
        $adminEmail = Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'admin_email');
435
436
        if ($adminEmail) {
437
            $msg .= ' ' . sprintf(
438
                _t('ForumRole.SUSPENSIONEMAILNOTE', 'Please contact %s to resolve this issue.'),
439
                $adminEmail
440
            );
441
        }
442
        return $msg;
443
    }
444
}
445
446
447
448
/**
449
 * ForumRole_Validator
450
 *
451
 * This class is used to validate the new fields added by the
452
 * {@link ForumRole} decorator in the CMS backend.
453
 */
454
class ForumRole_Validator extends Extension
455
{
456
457
    /**
458
     * Client-side validation code
459
     *
460
     * @param string $js The javascript validation code
461
     * @return string Returns the needed javascript code for client-side
462
     *                validation.
463
     */
464
    public function updateJavascript(&$js, &$form)
465
    {
466
467
        $formID = $form->FormName();
468
        $passwordFieldName = $form->dataFieldByName('Password')->id();
469
470
        $passwordConfirmField = $form->dataFieldByName('ConfirmPassword');
471
        if (!$passwordConfirmField) {
472
            return;
473
        }
474
475
        $passwordConfirmFieldName = $passwordConfirmField->id();
476
477
        $passwordcheck = <<<JS
478
Behaviour.register({
479
	"#$formID": {
480
		validatePasswordConfirmation: function() {
481
			var passEl = _CURRENT_FORM.elements['Password'];
482
			var confEl = _CURRENT_FORM.elements['ConfirmPassword'];
483
484
			if(passEl.value == confEl.value) {
485
			  clearErrorMessage(confEl.parentNode);
486
				return true;
487
			} else {
488
				validationError(confEl, "Passwords don't match.", "error");
489
				return false;
490
			}
491
		},
492
		initialize: function() {
493
			var passEl = $('$passwordFieldName');
494
			var confEl = $('$passwordConfirmFieldName');
495
496
			confEl.value = passEl.value;
497
		}
498
	}
499
});
500
JS;
501
        Requirements::customScript(
502
            $passwordcheck,
503
            'func_validatePasswordConfirmation'
504
        );
505
506
        $js .= "\$('$formID').validatePasswordConfirmation();";
507
        return $js;
508
    }
509
}
510