Completed
Push — master ( 97461f...d504d4 )
by Robbie
06:21
created

Comment   F

Complexity

Total Complexity 116

Size/Duplication

Total Lines 868
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 16

Importance

Changes 0
Metric Value
wmc 116
lcom 2
cbo 16
dl 0
loc 868
rs 1.732
c 0
b 0
f 0

42 Methods

Rating   Name   Duplication   Size   Complexity  
A onBeforeWrite() 0 12 2
A onBeforeDelete() 0 9 2
A getSecurityToken() 0 4 1
A Link() 0 6 2
A Permalink() 0 5 1
A fieldLabels() 0 15 1
A getOption() 0 17 6
A getParent() 0 6 3
A getParentTitle() 0 6 3
A getParentClassName() 0 4 1
A castingHelper() 0 8 3
A getEscapedComment() 0 4 1
A isPreview() 0 4 1
A canCreate() 0 4 1
B canView() 0 21 6
A canEdit() 0 23 5
A canDelete() 0 15 3
A getMember() 0 12 3
A getAuthorName() 0 8 3
A getAuthorEmail() 0 8 3
A actionLink() 0 28 3
A DeleteLink() 0 6 2
A SpamLink() 0 6 3
A HamLink() 0 6 3
A ApproveLink() 0 6 3
A markSpam() 0 7 1
A markApproved() 0 7 1
A markUnapproved() 0 6 1
A SpamClass() 0 10 3
A getTitle() 0 12 3
B getCMSFields() 0 73 6
A purifyHtml() 0 8 2
A getHtmlPurifierService() 0 22 4
A Gravatar() 0 19 2
A getRepliesEnabled() 0 12 4
A canPostComment() 0 6 3
A AllReplies() 0 17 3
B Replies() 0 27 9
A PagedReplies() 0 12 1
A ReplyForm() 0 21 4
A getDate() 0 4 1
A updateDepth() 0 10 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace SilverStripe\Comments\Model;
4
5
use HTMLPurifier;
6
use HTMLPurifier_Config;
7
use SilverStripe\Comments\Controllers\CommentingController;
8
use SilverStripe\Comments\Extensions\CommentsExtension;
9
use SilverStripe\Comments\Model\Comment\SecurityToken;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\TempFolder;
14
use SilverStripe\Forms\CheckboxField;
15
use SilverStripe\Forms\EmailField;
16
use SilverStripe\Forms\FieldGroup;
17
use SilverStripe\Forms\FieldList;
18
use SilverStripe\Forms\Form;
19
use SilverStripe\Forms\HeaderField;
20
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
21
use SilverStripe\Forms\TextareaField;
22
use SilverStripe\Forms\TextField;
23
use SilverStripe\ORM\ArrayList;
24
use SilverStripe\ORM\DataObject;
25
use SilverStripe\ORM\FieldType\DBDatetime;
26
use SilverStripe\ORM\PaginatedList;
27
use SilverStripe\ORM\SS_List;
28
use SilverStripe\Security\Member;
29
use SilverStripe\Security\Permission;
30
31
/**
32
 * Represents a single comment object.
33
 *
34
 * @property string  $Name
35
 * @property string  $Comment
36
 * @property string  $Email
37
 * @property string  $URL
38
 * @property string  $BaseClass
39
 * @property boolean $Moderated
40
 * @property boolean $IsSpam      True if the comment is known as spam
41
 * @property integer $ParentID    ID of the parent page / dataobject
42
 * @property boolean $AllowHtml   If true, treat $Comment as HTML instead of plain text
43
 * @property string  $SecretToken Secret admin token required to provide moderation links between sessions
44
 * @property integer $Depth       Depth of this comment in the nested chain
45
 *
46
 * @method HasManyList ChildComments() List of child comments
47
 * @method Member Author() Member object who created this comment
48
 * @method Comment ParentComment() Parent comment this is a reply to
49
 * @package comments
50
 */
51
class Comment extends DataObject
52
{
53
    /**
54
     * {@inheritDoc}
55
     */
56
    private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
        'Name' => 'Varchar(200)',
58
        'Comment' => 'Text',
59
        'Email' => 'Varchar(200)',
60
        'URL' => 'Varchar(255)',
61
        'Moderated' => 'Boolean(0)',
62
        'IsSpam' => 'Boolean(0)',
63
        'AllowHtml' => 'Boolean',
64
        'SecretToken' => 'Varchar(255)',
65
        'Depth' => 'Int'
66
    );
67
68
    /**
69
     * {@inheritDoc}
70
     */
71
    private static $has_one = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
72
        'Author' => Member::class,
73
        'ParentComment' => self::class,
74
        'Parent' => DataObject::class
75
    );
76
77
    /**
78
     * {@inheritDoc}
79
     */
80
    private static $has_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
81
        'ChildComments' => self::class
82
    );
83
84
    /**
85
     * {@inheritDoc}
86
     */
87
    private static $default_sort = '"Created" DESC';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
88
89
    /**
90
     * {@inheritDoc}
91
     */
92
    private static $defaults = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
93
        'Moderated' => 0,
94
        'IsSpam' => 0,
95
    );
96
97
    /**
98
     * {@inheritDoc}
99
     */
100
    private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
101
        'Title' => 'Varchar',
102
        'ParentTitle' => 'Varchar',
103
        'ParentClassName' => 'Varchar',
104
        'AuthorName' => 'Varchar',
105
        'RSSName' => 'Varchar',
106
        'DeleteLink' => 'Varchar',
107
        'Date' => 'Datetime',
108
        'SpamLink' => 'Varchar',
109
        'HamLink' => 'Varchar',
110
        'ApproveLink' => 'Varchar',
111
        'Permalink' => 'Varchar'
112
    );
113
114
    /**
115
     * {@inheritDoc}
116
     */
117
    private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
118
        'Name',
119
        'Email',
120
        'Comment',
121
        'Created'
122
    );
123
124
    /**
125
     * {@inheritDoc}
126
     */
127
    private static $summary_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
128
        'getAuthorName' => 'Submitted By',
129
        'getAuthorEmail' => 'Email',
130
        'Comment.LimitWordCount' => 'Comment',
131
        'Created' => 'Date Posted',
132
        'Parent.Title' => 'Post',
133
        'IsSpam' => 'Is Spam'
134
    );
135
136
    /**
137
     * {@inheritDoc}
138
     */
139
    private static $field_labels = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
140
        'Author' => 'Author Member'
141
    );
142
143
    /**
144
     * {@inheritDoc}
145
     */
146
    private static $table_name = 'Comment';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
147
148
    /**
149
     * {@inheritDoc}
150
     */
151
    public function onBeforeWrite()
152
    {
153
        parent::onBeforeWrite();
154
155
        // Sanitize HTML, because its expected to be passed to the template unescaped later
156
        if ($this->AllowHtml) {
157
            $this->Comment = $this->purifyHtml($this->Comment);
158
        }
159
160
        // Check comment depth
161
        $this->updateDepth();
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167
    public function onBeforeDelete()
168
    {
169
        parent::onBeforeDelete();
170
171
        // Delete all children
172
        foreach ($this->ChildComments() as $comment) {
173
            $comment->delete();
174
        }
175
    }
176
177
    /**
178
     * @return Comment_SecurityToken
179
     */
180
    public function getSecurityToken()
181
    {
182
        return Injector::inst()->createWithArgs(SecurityToken::class, array($this));
183
    }
184
185
    /**
186
     * Return a link to this comment
187
     *
188
     * @param string $action
189
     *
190
     * @return string link to this comment.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
191
     */
192
    public function Link($action = '')
193
    {
194
        if ($parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
195
            return $parent->Link($action) . '#' . $this->Permalink();
196
        }
197
    }
198
199
    /**
200
     * Returns the permalink for this {@link Comment}. Inserted into
201
     * the ID tag of the comment
202
     *
203
     * @return string
204
     */
205
    public function Permalink()
206
    {
207
        $prefix = $this->getOption('comment_permalink_prefix');
208
        return $prefix . $this->ID;
209
    }
210
211
    /**
212
     * Translate the form field labels for the CMS administration
213
     *
214
     * @param boolean $includerelations
215
     *
216
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be array|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
217
     */
218
    public function fieldLabels($includerelations = true)
219
    {
220
        $labels = parent::fieldLabels($includerelations);
221
222
        $labels['Name'] = _t(__CLASS__ . '.NAME', 'Author name');
223
        $labels['Comment'] = _t(__CLASS__ . '.COMMENT', 'Comment');
224
        $labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email');
225
        $labels['URL'] = _t(__CLASS__ . '.URL', 'URL');
226
        $labels['IsSpam'] = _t(__CLASS__ . '.ISSPAM', 'Spam?');
227
        $labels['Moderated'] = _t(__CLASS__ . '.MODERATED', 'Moderated?');
228
        $labels['ParentTitle'] = _t(__CLASS__ . '.PARENTTITLE', 'Parent');
229
        $labels['Created'] = _t(__CLASS__ . '.CREATED', 'Date posted');
230
231
        return $labels;
232
    }
233
234
    /**
235
     * Get the commenting option
236
     *
237
     * @param string $key
238
     *
239
     * @return mixed Result if the setting is available, or null otherwise
240
     */
241
    public function getOption($key)
242
    {
243
        // If possible use the current record
244
        $record = $this->Parent();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
245
246
        if (!$record && $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
247
            // Otherwise a singleton of that record
248
            $record = singleton($this->Parent()->dataClass());
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
249
        } elseif (!$record) {
250
            // Otherwise just use the default options
251
            $record = singleton(CommentsExtension::class);
252
        }
253
254
        return ($record instanceof CommentsExtension || $record->hasExtension(CommentsExtension::class))
255
            ? $record->getCommentsOption($key)
256
            : null;
257
    }
258
259
    /**
260
     * Returns the parent {@link DataObject} this comment is attached too
261
     *
262
     * @deprecated 4.0.0 Use $this->Parent() instead
263
     * @return DataObject
0 ignored issues
show
Documentation introduced by
Should the return type not be DataObject|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
264
     */
265
    public function getParent()
266
    {
267
        return $this->BaseClass && $this->ParentID
268
            ? DataObject::get_by_id($this->BaseClass, $this->ParentID, true)
269
            : null;
270
    }
271
272
273
    /**
274
     * Returns a string to help identify the parent of the comment
275
     *
276
     * @return string
277
     */
278
    public function getParentTitle()
279
    {
280
        if ($parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
281
            return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID);
282
        }
283
    }
284
285
    /**
286
     * Comment-parent classnames obviously vary, return the parent classname
287
     *
288
     * @return string
289
     */
290
    public function getParentClassName()
291
    {
292
        return $this->Parent()->getClassName();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
293
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298
    public function castingHelper($field)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
299
    {
300
        // Safely escape the comment
301
        if (in_array($field, ['EscapedComment', 'Comment'], true)) {
302
            return $this->AllowHtml ? 'HTMLText' : 'Text';
303
        }
304
        return parent::castingHelper($field);
305
    }
306
307
    /**
308
     * Content to be safely escaped on the frontend
309
     *
310
     * @return string
311
     */
312
    public function getEscapedComment()
313
    {
314
        return $this->Comment;
315
    }
316
317
    /**
318
     * Return whether this comment is a preview (has not been written to the db)
319
     *
320
     * @return boolean
321
     */
322
    public function isPreview()
323
    {
324
        return !$this->exists();
325
    }
326
327
    /**
328
     * @todo needs to compare to the new {@link Commenting} configuration API
329
     *
330
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
331
     * @param array  $context
332
     * @return bool
333
     */
334
    public function canCreate($member = null, $context = [])
335
    {
336
        return false;
337
    }
338
339
    /**
340
     * Checks for association with a page, and {@link SiteTree->ProvidePermission}
341
     * flag being set to true.
342
     *
343
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
344
     * @return Boolean
345
     */
346
    public function canView($member = null)
347
    {
348
        $member = $this->getMember($member);
349
350
        $extended = $this->extendedCan('canView', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<SilverStripe\Security\Member>|integer.

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...
351
        if ($extended !== null) {
352
            return $extended;
353
        }
354
355
        if (Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
356
            return true;
357
        }
358
359
        if ($parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
360
            return $parent->canView($member)
361
                && $parent->hasExtension(CommentsExtension::class)
362
                && $parent->CommentsEnabled;
363
        }
364
365
        return false;
366
    }
367
368
    /**
369
     * Checks if the comment can be edited.
370
     *
371
     * @param null|int|Member $member
372
     * @return Boolean
373
     */
374
    public function canEdit($member = null)
375
    {
376
        $member = $this->getMember($member);
377
378
        if (!$member) {
379
            return false;
380
        }
381
382
        $extended = $this->extendedCan('canEdit', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>, but the function expects a object<SilverStripe\Security\Member>|integer.

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...
383
        if ($extended !== null) {
384
            return $extended;
385
        }
386
387
        if (Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
388
            return true;
389
        }
390
391
        if ($parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
392
            return $parent->canEdit($member);
393
        }
394
395
        return false;
396
    }
397
398
    /**
399
     * Checks if the comment can be deleted.
400
     *
401
     * @param null|int|Member $member
402
     * @return Boolean
403
     */
404
    public function canDelete($member = null)
405
    {
406
        $member = $this->getMember($member);
407
408
        if (!$member) {
409
            return false;
410
        }
411
412
        $extended = $this->extendedCan('canDelete', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>, but the function expects a object<SilverStripe\Security\Member>|integer.

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...
413
        if ($extended !== null) {
414
            return $extended;
415
        }
416
417
        return $this->canEdit($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<SilverStripe\ORM\DataObject>, but the function expects a null|integer|object<SilverStripe\Security\Member>.

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...
418
    }
419
420
    /**
421
     * Resolves Member object.
422
     *
423
     * @param Member|int|null $member
424
     * @return Member|null
0 ignored issues
show
Documentation introduced by
Should the return type not be DataObject|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
425
     */
426
    protected function getMember($member = null)
427
    {
428
        if (!$member) {
429
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\Security\Member::currentUser() has been deprecated with message: 5.0.0 use Security::getCurrentUser()

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...
430
        }
431
432
        if (is_numeric($member)) {
433
            $member = DataObject::get_by_id(Member::class, $member, true);
434
        }
435
436
        return $member;
437
    }
438
439
    /**
440
     * Return the authors name for the comment
441
     *
442
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
443
     */
444
    public function getAuthorName()
445
    {
446
        if ($this->Name) {
447
            return $this->Name;
448
        } elseif ($author = $this->Author()) {
449
            return $author->getName();
450
        }
451
    }
452
453
    /**
454
     * Return the comment authors email address
455
     *
456
     * @return string
457
     */
458
    public function getAuthorEmail()
459
    {
460
        if ($this->Email) {
461
            return $this->Email;
462
        } elseif ($author = $this->Author()) {
463
            return $author->Email;
464
        }
465
    }
466
467
    /**
468
     * Generate a secure admin-action link authorised for the specified member
469
     *
470
     * @param string $action An action on CommentingController to link to
471
     * @param Member $member The member authorised to invoke this action
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
472
     *
473
     * @return string
474
     */
475
    protected function actionLink($action, $member = null)
476
    {
477
        if (!$member) {
478
            $member = Member::currentUser();
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\Security\Member::currentUser() has been deprecated with message: 5.0.0 use Security::getCurrentUser()

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...
479
        }
480
        if (!$member) {
481
            return false;
482
        }
483
484
        /**
485
         * @todo: How do we handle "DataObject" instances that don't have a Link to reject/spam/delete?? This may
486
         * we have to make CMS a hard dependency instead.
487
         */
488
        // if (!$this->Parent()->hasMethod('Link')) {
489
        //     return false;
490
        // }
491
492
        $url = Controller::join_links(
493
            Director::baseURL(),
494
            'comments',
495
            $action,
496
            $this->ID
497
        );
498
499
        // Limit access for this user
500
        $token = $this->getSecurityToken();
501
        return $token->addToUrl($url, $member);
502
    }
503
504
    /**
505
     * Link to delete this comment
506
     *
507
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
508
     *
509
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
510
     */
511
    public function DeleteLink($member = null)
512
    {
513
        if ($this->canDelete($member)) {
514
            return $this->actionLink('delete', $member);
515
        }
516
    }
517
518
    /**
519
     * Link to mark as spam
520
     *
521
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
522
     *
523
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
524
     */
525
    public function SpamLink($member = null)
526
    {
527
        if ($this->canEdit($member) && !$this->IsSpam) {
528
            return $this->actionLink('spam', $member);
529
        }
530
    }
531
532
    /**
533
     * Link to mark as not-spam (ham)
534
     *
535
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
536
     *
537
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
538
     */
539
    public function HamLink($member = null)
540
    {
541
        if ($this->canEdit($member) && $this->IsSpam) {
542
            return $this->actionLink('ham', $member);
543
        }
544
    }
545
546
    /**
547
     * Link to approve this comment
548
     *
549
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
550
     *
551
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
552
     */
553
    public function ApproveLink($member = null)
554
    {
555
        if ($this->canEdit($member) && !$this->Moderated) {
556
            return $this->actionLink('approve', $member);
557
        }
558
    }
559
560
    /**
561
     * Mark this comment as spam
562
     */
563
    public function markSpam()
564
    {
565
        $this->IsSpam = true;
566
        $this->Moderated = true;
567
        $this->write();
568
        $this->extend('afterMarkSpam');
569
    }
570
571
    /**
572
     * Mark this comment as approved
573
     */
574
    public function markApproved()
575
    {
576
        $this->IsSpam = false;
577
        $this->Moderated = true;
578
        $this->write();
579
        $this->extend('afterMarkApproved');
580
    }
581
582
    /**
583
     * Mark this comment as unapproved
584
     */
585
    public function markUnapproved()
586
    {
587
        $this->Moderated = false;
588
        $this->write();
589
        $this->extend('afterMarkUnapproved');
590
    }
591
592
    /**
593
     * @return string
594
     */
595
    public function SpamClass()
596
    {
597
        if ($this->IsSpam) {
598
            return 'spam';
599
        } elseif (!$this->Moderated) {
600
            return 'unmoderated';
601
        } else {
602
            return 'notspam';
603
        }
604
    }
605
606
    /**
607
     * @return string
608
     */
609
    public function getTitle()
610
    {
611
        $title = sprintf(_t(__CLASS__ . '.COMMENTBY', 'Comment by %s', 'Name'), $this->getAuthorName());
612
613
        if ($parent = $this->Parent()) {
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
614
            if ($parent->Title) {
615
                $title .= sprintf(' %s %s', _t(__CLASS__ . '.ON', 'on'), $parent->Title);
616
            }
617
        }
618
619
        return $title;
620
    }
621
622
    /*
623
     * Modify the default fields shown to the user
624
     */
625
    public function getCMSFields()
626
    {
627
        $commentField = $this->AllowHtml ? HTMLEditorField::class : TextareaField::class;
628
        $fields = new FieldList(
629
            $this
630
                ->obj('Created')
631
                ->scaffoldFormField($this->fieldLabel('Created'))
632
                ->performReadonlyTransformation(),
633
            TextField::create('Name', $this->fieldLabel('Name')),
634
            $commentField::create('Comment', $this->fieldLabel('Comment')),
635
            EmailField::create('Email', $this->fieldLabel('Email')),
636
            TextField::create('URL', $this->fieldLabel('URL')),
637
            FieldGroup::create(array(
638
                CheckboxField::create('Moderated', $this->fieldLabel('Moderated')),
639
                CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')),
640
            ))
641
                ->setTitle(_t(__CLASS__ . '.OPTIONS', 'Options'))
642
                ->setDescription(_t(
643
                    __CLASS__ . '.OPTION_DESCRIPTION',
644
                    'Unmoderated and spam comments will not be displayed until approved'
645
                ))
646
        );
647
648
        // Show member name if given
649
        if (($author = $this->Author()) && $author->exists()) {
650
            $fields->insertAfter(
651
                TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title)
652
                    ->performReadonlyTransformation(),
653
                'Name'
0 ignored issues
show
Documentation introduced by
'Name' is of type string, but the function expects a object<SilverStripe\Forms\FormField>.

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...
654
            );
655
        }
656
657
        // Show parent comment if given
658
        if (($parent = $this->ParentComment()) && $parent->exists()) {
659
            $fields->push(new HeaderField(
660
                'ParentComment_Title',
661
                _t(__CLASS__ . '.ParentComment_Title', 'This comment is a reply to the below')
662
            ));
663
            // Created date
664
            // FIXME - the method setName in DatetimeField is not chainable, hence
665
            // the lack of chaining here
666
            $createdField = $parent
667
                ->obj('Created')
668
                ->scaffoldFormField($parent->fieldLabel('Created'));
669
            $createdField->setName('ParentComment_Created');
670
            $createdField->setValue($parent->Created);
671
            $createdField->performReadonlyTransformation();
672
            $fields->push($createdField);
673
674
            // Name (could be member or string value)
675
            $fields->push(
676
                $parent
677
                    ->obj('AuthorName')
678
                    ->scaffoldFormField($parent->fieldLabel('AuthorName'))
679
                    ->setName('ParentComment_AuthorName')
680
                    ->setValue($parent->getAuthorName())
681
                    ->performReadonlyTransformation()
682
            );
683
684
            // Comment body
685
            $fields->push(
686
                $parent
687
                    ->obj('EscapedComment')
688
                    ->scaffoldFormField($parent->fieldLabel(self::class))
689
                    ->setName('ParentComment_EscapedComment')
690
                    ->setValue($parent->Comment)
691
                    ->performReadonlyTransformation()
692
            );
693
        }
694
695
        $this->extend('updateCMSFields', $fields);
696
        return $fields;
697
    }
698
699
    /**
700
     * @param  string $dirtyHtml
701
     *
702
     * @return string
703
     */
704
    public function purifyHtml($dirtyHtml)
705
    {
706
        if ($service = $this->getHtmlPurifierService()) {
707
            return $service->purify($dirtyHtml);
708
        }
709
710
        return $dirtyHtml;
711
    }
712
713
    /**
714
     * @return HTMLPurifier (or anything with a "purify()" method)
0 ignored issues
show
Documentation introduced by
Should the return type not be HTMLPurifier|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
715
     */
716
    public function getHtmlPurifierService()
717
    {
718
        if (!class_exists(HTMLPurifier_Config::class)) {
719
            return null;
720
        }
721
722
        $config = HTMLPurifier_Config::createDefault();
723
        $allowedElements = (array) $this->getOption('html_allowed_elements');
724
        if (!empty($allowedElements)) {
725
            $config->set('HTML.AllowedElements', $allowedElements);
726
        }
727
728
        // This injector cannot be set unless the 'p' element is allowed
729
        if (in_array('p', $allowedElements)) {
730
            $config->set('AutoFormat.AutoParagraph', true);
731
        }
732
733
        $config->set('AutoFormat.Linkify', true);
734
        $config->set('URI.DisableExternalResources', true);
735
        $config->set('Cache.SerializerPath', TempFolder::getTempFolder(BASE_PATH));
736
        return new HTMLPurifier($config);
737
    }
738
739
    /**
740
     * Calculate the Gravatar link from the email address
741
     *
742
     * @return string
743
     */
744
    public function Gravatar()
745
    {
746
        $gravatar = '';
747
        $use_gravatar = $this->getOption('use_gravatar');
748
749
        if ($use_gravatar) {
750
            $gravatar = 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($this->Email)));
751
            $gravatarsize = $this->getOption('gravatar_size');
752
            $gravatardefault = $this->getOption('gravatar_default');
753
            $gravatarrating = $this->getOption('gravatar_rating');
754
            $gravatar .= '?' . http_build_query(array(
755
                's' => $gravatarsize,
756
                'd' => $gravatardefault,
757
                'r' => $gravatarrating,
758
            ));
759
        }
760
761
        return $gravatar;
762
    }
763
764
    /**
765
     * Determine if replies are enabled for this instance
766
     *
767
     * @return boolean
768
     */
769
    public function getRepliesEnabled()
770
    {
771
        // Check reply option
772
        if (!$this->getOption('nested_comments')) {
773
            return false;
774
        }
775
776
        // Check if depth is limited
777
        $maxLevel = $this->getOption('nested_depth');
778
        $notSpam = ($this->SpamClass() == 'notspam');
779
        return $notSpam && (!$maxLevel || $this->Depth < $maxLevel);
780
    }
781
782
    /**
783
     * Proxy for checking whether the has permission to comment on the comment parent.
784
     *
785
     * @param Member $member Member to check
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
786
     *
787
     * @return boolean
788
     */
789
    public function canPostComment($member = null)
790
    {
791
        return $this->Parent()
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
792
            && $this->Parent()->exists()
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
793
            && $this->Parent()->canPostComment($member);
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
794
    }
795
796
    /**
797
     * Returns the list of all replies
798
     *
799
     * @return SS_List
800
     */
801
    public function AllReplies()
802
    {
803
        // No replies if disabled
804
        if (!$this->getRepliesEnabled()) {
805
            return new ArrayList();
806
        }
807
808
        // Get all non-spam comments
809
        $order = $this->getOption('order_replies_by')
810
            ?: $this->getOption('order_comments_by');
811
        $list = $this
812
            ->ChildComments()
813
            ->sort($order);
814
815
        $this->extend('updateAllReplies', $list);
816
        return $list;
817
    }
818
819
    /**
820
     * Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend
821
     *
822
     * @return SS_List
823
     */
824
    public function Replies()
825
    {
826
        // No replies if disabled
827
        if (!$this->getRepliesEnabled()) {
828
            return new ArrayList();
829
        }
830
        $list = $this->AllReplies();
831
832
        // Filter spam comments for non-administrators if configured
833
        $parent = $this->Parent();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
834
        $showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments();
835
        if (!$showSpam) {
836
            $list = $list->filter('IsSpam', 0);
837
        }
838
839
        // Filter un-moderated comments for non-administrators if moderation is enabled
840
        $showUnmoderated = $parent && (
841
            ($parent->ModerationRequired === 'None')
842
            || ($this->getOption('frontend_moderation') && $parent->canModerateComments())
843
        );
844
        if (!$showUnmoderated) {
845
            $list = $list->filter('Moderated', 1);
846
        }
847
848
        $this->extend('updateReplies', $list);
849
        return $list;
850
    }
851
852
    /**
853
     * Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend
854
     *
855
     * @return PaginatedList
856
     */
857
    public function PagedReplies()
858
    {
859
        $list = $this->Replies();
860
861
        // Add pagination
862
        $list = new PaginatedList($list, Controller::curr()->getRequest());
0 ignored issues
show
Documentation introduced by
\SilverStripe\Control\Co...r::curr()->getRequest() is of type object<SilverStripe\Control\HTTPRequest>, but the function expects a array.

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...
863
        $list->setPaginationGetVar('repliesstart' . $this->ID);
864
        $list->setPageLength($this->getOption('comments_per_page'));
865
866
        $this->extend('updatePagedReplies', $list);
867
        return $list;
868
    }
869
870
    /**
871
     * Generate a reply form for this comment
872
     *
873
     * @return Form
0 ignored issues
show
Documentation introduced by
Should the return type not be Form|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
874
     */
875
    public function ReplyForm()
876
    {
877
        // Ensure replies are enabled
878
        if (!$this->getRepliesEnabled()) {
879
            return null;
880
        }
881
882
        // Check parent is available
883
        $parent = $this->Parent();
0 ignored issues
show
Bug introduced by
The method Parent() does not exist on SilverStripe\Comments\Model\Comment. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
884
        if (!$parent || !$parent->exists()) {
885
            return null;
886
        }
887
888
        // Build reply controller
889
        $controller = CommentingController::create();
890
        $controller->setOwnerRecord($parent);
891
        $controller->setParentClass($parent->ClassName);
892
        $controller->setOwnerController(Controller::curr());
893
894
        return $controller->ReplyForm($this);
895
    }
896
897
    /**
898
     * @return string
899
     */
900
    public function getDate()
901
    {
902
        return $this->Created;
903
    }
904
905
    /**
906
     * Refresh of this comment in the hierarchy
907
     */
908
    public function updateDepth()
909
    {
910
        $parent = $this->ParentComment();
911
        if ($parent && $parent->exists()) {
912
            $parent->updateDepth();
913
            $this->Depth = $parent->Depth + 1;
914
        } else {
915
            $this->Depth = 1;
916
        }
917
    }
918
}
919