Completed
Push — master ( e42a4c...f187a0 )
by Daniel
03:17
created

Comment_SecurityToken   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 107
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5
Metric Value
wmc 10
lcom 1
cbo 5
dl 0
loc 107
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A getToken() 0 4 1
A memberSalt() 0 5 2
A addToUrl() 0 15 1
A checkRequest() 0 14 2
A generate() 0 9 2
1
<?php
2
3
/**
4
 * Represents a single comment object.
5
 *
6
 * @property string  $Name
7
 * @property string  $Comment
8
 * @property string  $Email
9
 * @property string  $URL
10
 * @property string  $BaseClass
11
 * @property boolean $Moderated
12
 * @property boolean $IsSpam      True if the comment is known as spam
13
 * @property integer $ParentID    ID of the parent page / dataobject
14
 * @property boolean $AllowHtml   If true, treat $Comment as HTML instead of plain text
15
 * @property string  $SecretToken Secret admin token required to provide moderation links between sessions
16
 * @property integer $Depth       Depth of this comment in the nested chain
17
 *
18
 * @method HasManyList ChildComments() List of child comments
19
 * @method Member Author() Member object who created this comment
20
 * @method Comment ParentComment() Parent comment this is a reply to
21
 * @package comments
22
 */
23
class Comment extends DataObject
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
24
{
25
26
    /**
27
     * @var array
28
     */
29
    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...
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
30
        'Name' => 'Varchar(200)',
31
        'Comment' => 'Text',
32
        'Email' => 'Varchar(200)',
33
        'URL' => 'Varchar(255)',
34
        'BaseClass' => 'Varchar(200)',
35
        'Moderated' => 'Boolean(0)',
36
        'IsSpam' => 'Boolean(0)',
37
        'ParentID' => 'Int',
38
        'AllowHtml' => 'Boolean',
39
        'SecretToken' => 'Varchar(255)',
40
        'Depth' => 'Int',
41
    );
42
43
    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...
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
44
        "Author" => "Member",
45
        "ParentComment" => "Comment",
46
    );
47
48
    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...
Unused Code introduced by
The property $has_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
49
        "ChildComments"    => "Comment"
50
    );
51
52
    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...
Unused Code introduced by
The property $default_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
53
54
    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...
Unused Code introduced by
The property $defaults is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
55
        'Moderated' => 0,
56
        'IsSpam' => 0,
57
    );
58
59
    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...
Unused Code introduced by
The property $casting is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
60
        'Title' => 'Varchar',
61
        'ParentTitle' => 'Varchar',
62
        'ParentClassName' => 'Varchar',
63
        'AuthorName' => 'Varchar',
64
        'RSSName' => 'Varchar',
65
        'DeleteLink' => 'Varchar',
66
        'SpamLink' => 'Varchar',
67
        'HamLink' => 'Varchar',
68
        'ApproveLink' => 'Varchar',
69
        'Permalink' => 'Varchar',
70
    );
71
72
    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...
Unused Code introduced by
The property $searchable_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
73
        'Name',
74
        'Email',
75
        'Comment',
76
        'Created',
77
        'BaseClass',
78
    );
79
80
    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...
Unused Code introduced by
The property $summary_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
81
        'Name' => 'Submitted By',
82
        'Email' => 'Email',
83
        'Comment.LimitWordCount' => 'Comment',
84
        'Created' => 'Date Posted',
85
        'ParentTitle' => 'Post',
86
        'IsSpam' => 'Is Spam',
87
    );
88
89
    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...
Unused Code introduced by
The property $field_labels is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
90
        'Author' => 'Author Member',
91
    );
92
93
    public function onBeforeWrite()
94
    {
95
        parent::onBeforeWrite();
96
97
        // Sanitize HTML, because its expected to be passed to the template unescaped later
98
        if ($this->AllowHtml) {
99
            $this->Comment = $this->purifyHtml($this->Comment);
100
        }
101
102
        // Check comment depth
103
        $this->updateDepth();
104
    }
105
106
    public function onBeforeDelete()
107
    {
108
        parent::onBeforeDelete();
109
110
        // Delete all children
111
        foreach ($this->ChildComments() as $comment) {
112
            $comment->delete();
113
        }
114
    }
115
116
    /**
117
     * @return Comment_SecurityToken
118
     */
119
    public function getSecurityToken()
120
    {
121
        return Injector::inst()->createWithArgs('Comment_SecurityToken', array($this));
122
    }
123
124
    /**
125
     * Migrates the old {@link PageComment} objects to {@link Comment}
126
     */
127
    public function requireDefaultRecords()
128
    {
129
        parent::requireDefaultRecords();
130
131
        if (DB::getConn()->hasTable('PageComment')) {
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...
132
            $comments = DB::query('SELECT * FROM "PageComment"');
133
134
            if ($comments) {
135
                while ($pageComment = $comments->nextRecord()) {
136
                    // create a new comment from the older page comment
137
                    $comment = new Comment();
138
                    $comment->update($pageComment);
139
140
                    // set the variables which have changed
141
                    $comment->BaseClass = 'SiteTree';
142
                    $comment->URL = (isset($pageComment['CommenterURL'])) ? $pageComment['CommenterURL'] : '';
143
                    if ((int) $pageComment['NeedsModeration'] == 0) {
144
                        $comment->Moderated = true;
145
                    }
146
147
                    $comment->write();
148
                }
149
            }
150
151
            DB::alteration_message('Migrated PageComment to Comment', 'changed');
152
            DB::getConn()->dontRequireTable('PageComment');
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::dontRequireTable() has been deprecated with message: since version 4.0 Use DB::dont_require_table() 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...
153
        }
154
    }
155
156
    /**
157
     * Return a link to this comment
158
     *
159
     * @param string $action
160
     *
161
     * @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...
162
     */
163
    public function Link($action = '')
164
    {
165
        if ($parent = $this->getParent()) {
166
            return $parent->Link($action) . '#' . $this->Permalink();
167
        }
168
    }
169
170
    /**
171
     * Returns the permalink for this {@link Comment}. Inserted into
172
     * the ID tag of the comment
173
     *
174
     * @return string
175
     */
176
    public function Permalink()
177
    {
178
        $prefix = $this->getOption('comment_permalink_prefix');
179
        return $prefix . $this->ID;
180
    }
181
182
    /**
183
     * Translate the form field labels for the CMS administration
184
     *
185
     * @param boolean $includerelations
186
     *
187
     * @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...
188
     */
189
    public function fieldLabels($includerelations = true)
190
    {
191
        $labels = parent::fieldLabels($includerelations);
192
193
        $labels['Name'] = _t('Comment.NAME', 'Author Name');
194
        $labels['Comment'] = _t('Comment.COMMENT', 'Comment');
195
        $labels['Email'] = _t('Comment.EMAIL', 'Email');
196
        $labels['URL'] = _t('Comment.URL', 'URL');
197
        $labels['IsSpam'] = _t('Comment.ISSPAM', 'Spam?');
198
        $labels['Moderated'] = _t('Comment.MODERATED', 'Moderated?');
199
        $labels['ParentTitle'] = _t('Comment.PARENTTITLE', 'Parent');
200
        $labels['Created'] = _t('Comment.CREATED', 'Date posted');
201
202
        return $labels;
203
    }
204
205
    /**
206
     * Get the commenting option
207
     *
208
     * @param string $key
209
     *
210
     * @return mixed Result if the setting is available, or null otherwise
211
     */
212
    public function getOption($key)
213
    {
214
        // If possible use the current record
215
        $record = $this->getParent();
216
217
        if (!$record && $this->BaseClass) {
218
            // Otherwise a singleton of that record
219
            $record = singleton($this->BaseClass);
220
        } elseif (!$record) {
221
            // Otherwise just use the default options
222
            $record = singleton('CommentsExtension');
223
        }
224
225
        return ($record->hasMethod('getCommentsOption')) ? $record->getCommentsOption($key) : null;
226
    }
227
228
    /**
229
     * Returns the parent {@link DataObject} this comment is attached too
230
     *
231
     * @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...
232
     */
233
    public function getParent()
234
    {
235
        return $this->BaseClass && $this->ParentID
236
            ? DataObject::get_by_id($this->BaseClass, $this->ParentID, true)
237
            : null;
238
    }
239
240
241
    /**
242
     * Returns a string to help identify the parent of the comment
243
     *
244
     * @return string
245
     */
246
    public function getParentTitle()
247
    {
248
        if ($parent = $this->getParent()) {
249
            return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID);
250
        }
251
    }
252
253
    /**
254
     * Comment-parent classnames obviously vary, return the parent classname
255
     *
256
     * @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...
257
     */
258
    public function getParentClassName()
259
    {
260
        return $this->BaseClass;
261
    }
262
263
    public function castingHelper($field)
264
    {
265
        // Safely escape the comment
266
        if ($field === 'EscapedComment') {
267
            return $this->AllowHtml ? 'HTMLText' : 'Text';
268
        }
269
        return parent::castingHelper($field);
270
    }
271
272
    /**
273
     * Content to be safely escaped on the frontend
274
     *
275
     * @return string
276
     */
277
    public function getEscapedComment()
278
    {
279
        return $this->Comment;
280
    }
281
282
    /**
283
     * Return whether this comment is a preview (has not been written to the db)
284
     *
285
     * @return boolean
286
     */
287
    public function isPreview()
288
    {
289
        return !$this->exists();
290
    }
291
292
    /**
293
     * @todo needs to compare to the new {@link Commenting} configuration API
294
     *
295
     * @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...
296
     *
297
     * @return bool
298
     */
299
    public function canCreate($member = null)
300
    {
301
        return false;
302
    }
303
304
    /**
305
     * Checks for association with a page, and {@link SiteTree->ProvidePermission}
306
     * flag being set to true.
307
     *
308
     * @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...
309
     *
310
     * @return Boolean
311
     */
312
    public function canView($member = null)
313
    {
314
        $member = $this->getMember($member);
315
316
        $extended = $this->extendedCan('canView', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>|null, but the function expects a object<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...
317
        if ($extended !== null) {
318
            return $extended;
319
        }
320
321
        if (Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
322
            return true;
323
        }
324
325
        if ($parent = $this->getParent()) {
326
            return $parent->canView($member)
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->getMember($member) on line 314 can also be of type object<DataObject>; however, DataObject::canView() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
327
                && $parent->has_extension('CommentsExtension')
328
                && $parent->CommentsEnabled;
329
        }
330
331
        return false;
332
    }
333
334
    /**
335
     * Checks if the comment can be edited.
336
     *
337
     * @param null|int|Member $member
338
     *
339
     * @return Boolean
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|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...
340
     */
341
    public function canEdit($member = null)
342
    {
343
        $member = $this->getMember($member);
344
345
        if (!$member) {
346
            return false;
347
        }
348
349
        $extended = $this->extendedCan('canEdit', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<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...
350
        if ($extended !== null) {
351
            return $extended;
352
        }
353
354
        if (Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
355
            return true;
356
        }
357
358
        if ($parent = $this->getParent()) {
359
            return $parent->canEdit($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<Member>|null.

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...
360
        }
361
362
        return false;
363
    }
364
365
    /**
366
     * Checks if the comment can be deleted.
367
     *
368
     * @param null|int|Member $member
369
     *
370
     * @return Boolean
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|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...
371
     */
372
    public function canDelete($member = null)
373
    {
374
        $member = $this->getMember($member);
375
376
        if (!$member) {
377
            return false;
378
        }
379
380
        $extended = $this->extendedCan('canDelete', $member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a object<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...
381
        if ($extended !== null) {
382
            return $extended;
383
        }
384
385
        return $this->canEdit($member);
0 ignored issues
show
Documentation introduced by
$member is of type object<DataObject>, but the function expects a null|integer|object<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...
386
    }
387
388
    /**
389
     * Resolves Member object.
390
     *
391
     * @param Member|int|null $member
392
     * @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...
393
     */
394
    protected function getMember($member = null)
395
    {
396
        if (!$member) {
397
            $member = Member::currentUser();
398
        }
399
400
        if (is_numeric($member)) {
401
            $member = DataObject::get_by_id('Member', $member, true);
402
        }
403
404
        return $member;
405
    }
406
407
    /**
408
     * Return the authors name for the comment
409
     *
410
     * @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...
411
     */
412
    public function getAuthorName()
413
    {
414
        if ($this->Name) {
415
            return $this->Name;
416
        } elseif ($author = $this->Author()) {
417
            return $author->getName();
418
        }
419
    }
420
421
    /**
422
     * Generate a secure admin-action link authorised for the specified member
423
     *
424
     * @param string $action An action on CommentingController to link to
425
     * @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...
426
     *
427
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|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...
428
     */
429
    protected function actionLink($action, $member = null)
430
    {
431
        if (!$member) {
432
            $member = Member::currentUser();
433
        }
434
        if (!$member) {
435
            return false;
436
        }
437
438
        $url = Controller::join_links(
439
            Director::baseURL(),
440
            'CommentingController',
441
            $action,
442
            $this->ID
443
        );
444
445
        // Limit access for this user
446
        $token = $this->getSecurityToken();
447
        return $token->addToUrl($url, $member);
0 ignored issues
show
Compatibility introduced by
$member of type object<DataObject> is not a sub-type of object<Member>. 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...
448
    }
449
450
    /**
451
     * Link to delete this comment
452
     *
453
     * @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...
454
     *
455
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|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...
456
     */
457
    public function DeleteLink($member = null)
458
    {
459
        if ($this->canDelete($member)) {
460
            return $this->actionLink('delete', $member);
461
        }
462
    }
463
464
    /**
465
     * Link to mark as spam
466
     *
467
     * @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...
468
     *
469
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|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...
470
     */
471
    public function SpamLink($member = null)
472
    {
473
        if ($this->canEdit($member) && !$this->IsSpam) {
474
            return $this->actionLink('spam', $member);
475
        }
476
    }
477
478
    /**
479
     * Link to mark as not-spam (ham)
480
     *
481
     * @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...
482
     *
483
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|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...
484
     */
485
    public function HamLink($member = null)
486
    {
487
        if ($this->canEdit($member) && $this->IsSpam) {
488
            return $this->actionLink('ham', $member);
489
        }
490
    }
491
492
    /**
493
     * Link to approve this comment
494
     *
495
     * @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...
496
     *
497
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be false|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...
498
     */
499
    public function ApproveLink($member = null)
500
    {
501
        if ($this->canEdit($member) && !$this->Moderated) {
502
            return $this->actionLink('approve', $member);
503
        }
504
    }
505
506
    /**
507
     * Mark this comment as spam
508
     */
509
    public function markSpam()
510
    {
511
        $this->IsSpam = true;
512
        $this->Moderated = true;
513
        $this->write();
514
        $this->extend('afterMarkSpam');
515
    }
516
517
    /**
518
     * Mark this comment as approved
519
     */
520
    public function markApproved()
521
    {
522
        $this->IsSpam = false;
523
        $this->Moderated = true;
524
        $this->write();
525
        $this->extend('afterMarkApproved');
526
    }
527
528
    /**
529
     * Mark this comment as unapproved
530
     */
531
    public function markUnapproved()
532
    {
533
        $this->Moderated = false;
534
        $this->write();
535
        $this->extend('afterMarkUnapproved');
536
    }
537
538
    /**
539
     * @return string
540
     */
541
    public function SpamClass()
542
    {
543
        if ($this->IsSpam) {
544
            return 'spam';
545
        } elseif (!$this->Moderated) {
546
            return 'unmoderated';
547
        } else {
548
            return 'notspam';
549
        }
550
    }
551
552
    /**
553
     * @return string
554
     */
555
    public function getTitle()
556
    {
557
        $title = sprintf(_t('Comment.COMMENTBY', 'Comment by %s', 'Name'), $this->getAuthorName());
558
559
        if ($parent = $this->getParent()) {
560
            if ($parent->Title) {
561
                $title .= sprintf(' %s %s', _t('Comment.ON', 'on'), $parent->Title);
562
            }
563
        }
564
565
        return $title;
566
    }
567
568
    /*
569
     * Modify the default fields shown to the user
570
     */
571
    public function getCMSFields()
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...
572
    {
573
        $commentField = $this->AllowHtml ? 'HtmlEditorField' : 'TextareaField';
574
        $fields = new FieldList(
575
            $this
576
                ->obj('Created')
577
                ->scaffoldFormField($this->fieldLabel('Created'))
578
                ->performReadonlyTransformation(),
579
            TextField::create('Name', $this->fieldLabel('AuthorName')),
580
            $commentField::create('Comment', $this->fieldLabel('Comment')),
581
            EmailField::create('Email', $this->fieldLabel('Email')),
582
            TextField::create('URL', $this->fieldLabel('URL')),
583
            FieldGroup::create(array(
584
                CheckboxField::create('Moderated', $this->fieldLabel('Moderated')),
585
                CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')),
586
            ))
587
                ->setTitle('Options')
588
                ->setDescription(_t(
589
                    'Comment.OPTION_DESCRIPTION',
590
                    'Unmoderated and spam comments will not be displayed until approved'
591
                ))
592
        );
593
594
        // Show member name if given
595
        if (($author = $this->Author()) && $author->exists()) {
596
            $fields->insertAfter(
597
                TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title)
598
                    ->performReadonlyTransformation(),
599
                'Name'
0 ignored issues
show
Documentation introduced by
'Name' is of type string, but the function expects a object<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...
600
            );
601
        }
602
603
        // Show parent comment if given
604
        if (($parent = $this->ParentComment()) && $parent->exists()) {
605
            $fields->push(new HeaderField(
606
                'ParentComment_Title',
607
                _t('Comment.ParentComment_Title', 'This comment is a reply to the below')
608
            ));
609
            // Created date
610
            // FIXME - the method setName in DatetimeField is not chainable, hence
611
            // the lack of chaining here
612
            $createdField = $parent
613
                    ->obj('Created')
614
                    ->scaffoldFormField($parent->fieldLabel('Created'));
615
            $createdField->setName('ParentComment_Created');
616
            $createdField->setValue($parent->Created);
617
            $createdField->performReadonlyTransformation();
618
            $fields->push($createdField);
619
620
            // Name (could be member or string value)
621
            $fields->push(
622
                $parent
623
                    ->obj('AuthorName')
624
                    ->scaffoldFormField($parent->fieldLabel('AuthorName'))
625
                    ->setName('ParentComment_AuthorName')
626
                    ->setValue($parent->getAuthorName())
627
                    ->performReadonlyTransformation()
628
            );
629
630
            // Comment body
631
            $fields->push(
632
                $parent
633
                    ->obj('EscapedComment')
634
                    ->scaffoldFormField($parent->fieldLabel('Comment'))
635
                    ->setName('ParentComment_EscapedComment')
636
                    ->setValue($parent->Comment)
637
                    ->performReadonlyTransformation()
638
            );
639
        }
640
641
        $this->extend('updateCMSFields', $fields);
642
        return $fields;
643
    }
644
645
    /**
646
     * @param  String $dirtyHtml
647
     *
648
     * @return String
649
     */
650
    public function purifyHtml($dirtyHtml)
651
    {
652
        $purifier = $this->getHtmlPurifierService();
653
        return $purifier->purify($dirtyHtml);
654
    }
655
656
    /**
657
     * @return HTMLPurifier (or anything with a "purify()" method)
658
     */
659
    public function getHtmlPurifierService()
660
    {
661
        $config = HTMLPurifier_Config::createDefault();
662
        $allowedElements = $this->getOption('html_allowed_elements');
663
        $config->set('HTML.AllowedElements', $allowedElements);
664
665
        // This injector cannot be set unless the 'p' element is allowed
666
        if (in_array('p', $allowedElements)) {
667
            $config->set('AutoFormat.AutoParagraph', true);
668
        }
669
670
        $config->set('AutoFormat.Linkify', true);
671
        $config->set('URI.DisableExternalResources', true);
672
        $config->set('Cache.SerializerPath', getTempFolder());
673
        return new HTMLPurifier($config);
674
    }
675
676
    /**
677
     * Calculate the Gravatar link from the email address
678
     *
679
     * @return string
680
     */
681
    public function Gravatar()
682
    {
683
        $gravatar = '';
684
        $use_gravatar = $this->getOption('use_gravatar');
685
        if ($use_gravatar) {
686
            $gravatar = 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->Email)));
687
            $gravatarsize = $this->getOption('gravatar_size');
688
            $gravatardefault = $this->getOption('gravatar_default');
689
            $gravatarrating = $this->getOption('gravatar_rating');
690
            $gravatar .= '?s=' . $gravatarsize . '&d=' . $gravatardefault . '&r=' . $gravatarrating;
691
        }
692
693
        return $gravatar;
694
    }
695
696
    /**
697
     * Determine if replies are enabled for this instance
698
     *
699
     * @return boolean
700
     */
701
    public function getRepliesEnabled()
702
    {
703
        // Check reply option
704
        if (!$this->getOption('nested_comments')) {
705
            return false;
706
        }
707
708
        // Check if depth is limited
709
        $maxLevel = $this->getOption('nested_depth');
710
        $notSpam = ($this->SpamClass() == 'notspam');
711
        return $notSpam && (!$maxLevel || $this->Depth < $maxLevel);
712
    }
713
714
    /**
715
     * Returns the list of all replies
716
     *
717
     * @return SS_List
718
     */
719
    public function AllReplies()
720
    {
721
        // No replies if disabled
722
        if (!$this->getRepliesEnabled()) {
723
            return new ArrayList();
724
        }
725
726
        // Get all non-spam comments
727
        $order = $this->getOption('order_replies_by')
728
            ?: $this->getOption('order_comments_by');
729
        $list = $this
730
            ->ChildComments()
731
            ->sort($order);
732
733
        $this->extend('updateAllReplies', $list);
734
        return $list;
735
    }
736
737
    /**
738
     * Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend
739
     *
740
     * @return SS_List
741
     */
742
    public function Replies()
743
    {
744
        // No replies if disabled
745
        if (!$this->getRepliesEnabled()) {
746
            return new ArrayList();
747
        }
748
        $list = $this->AllReplies();
749
750
        // Filter spam comments for non-administrators if configured
751
        $parent = $this->getParent();
752
        $showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments();
753
        if (!$showSpam) {
754
            $list = $list->filter('IsSpam', 0);
755
        }
756
757
        // Filter un-moderated comments for non-administrators if moderation is enabled
758
        $showUnmoderated = $parent && (
759
            ($parent->ModerationRequired === 'None')
760
            || ($this->getOption('frontend_moderation') && $parent->canModerateComments())
761
        );
762
        if (!$showUnmoderated) {
763
            $list = $list->filter('Moderated', 1);
764
        }
765
766
        $this->extend('updateReplies', $list);
767
        return $list;
768
    }
769
770
    /**
771
     * Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend
772
     *
773
     * @return PaginatedList
774
     */
775 View Code Duplication
    public function PagedReplies()
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...
776
    {
777
        $list = $this->Replies();
778
779
        // Add pagination
780
        $list = new PaginatedList($list, Controller::curr()->getRequest());
0 ignored issues
show
Documentation introduced by
\Controller::curr()->getRequest() is of type object<SS_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...
781
        $list->setPaginationGetVar('repliesstart'.$this->ID);
782
        $list->setPageLength($this->getOption('comments_per_page'));
783
784
        $this->extend('updatePagedReplies', $list);
785
        return $list;
786
    }
787
788
    /**
789
     * Generate a reply form for this comment
790
     *
791
     * @return Form
792
     */
793
    public function ReplyForm()
794
    {
795
        // Ensure replies are enabled
796
        if (!$this->getRepliesEnabled()) {
797
            return null;
798
        }
799
800
        // Check parent is available
801
        $parent = $this->getParent();
802
        if (!$parent || !$parent->exists()) {
803
            return null;
804
        }
805
806
        // Build reply controller
807
        $controller = CommentingController::create();
808
        $controller->setOwnerRecord($parent);
809
        $controller->setBaseClass($parent->ClassName);
810
        $controller->setOwnerController(Controller::curr());
811
812
        return $controller->ReplyForm($this);
813
    }
814
815
    /**
816
     * Refresh of this comment in the hierarchy
817
     */
818
    public function updateDepth()
819
    {
820
        $parent = $this->ParentComment();
821
        if ($parent && $parent->exists()) {
822
            $parent->updateDepth();
823
            $this->Depth = $parent->Depth + 1;
824
        } else {
825
            $this->Depth = 1;
826
        }
827
    }
828
}
829
830
831
/**
832
 * Provides the ability to generate cryptographically secure tokens for comment moderation
833
 */
834
class Comment_SecurityToken
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
835
{
836
837
    private $secret = null;
838
839
    /**
840
     * @param Comment $comment Comment to generate this token for
841
     */
842
    public function __construct($comment)
843
    {
844
        if (!$comment->SecretToken) {
845
            $comment->SecretToken = $this->generate();
846
            $comment->write();
847
        }
848
        $this->secret = $comment->SecretToken;
849
    }
850
851
    /**
852
     * Generate the token for the given salt and current secret
853
     *
854
     * @param string $salt
855
     *
856
     * @return string
857
     */
858
    protected function getToken($salt)
859
    {
860
        return hash_pbkdf2('sha256', $this->secret, $salt, 1000, 30);
861
    }
862
863
    /**
864
     * Get the member-specific salt.
865
     *
866
     * The reason for making the salt specific to a user is that it cannot be "passed in" via a
867
     * querystring, requiring the same user to be present at both the link generation and the
868
     * controller action.
869
     *
870
     * @param string $salt   Single use salt
871
     * @param Member $member Member object
872
     *
873
     * @return string Generated salt specific to this member
874
     */
875
    protected function memberSalt($salt, $member)
876
    {
877
        // Fallback to salting with ID in case the member has not one set
878
        return $salt . ($member->Salt ?: $member->ID);
879
    }
880
881
    /**
882
     * @param string $url    Comment action URL
883
     * @param Member $member Member to restrict access to this action to
884
     *
885
     * @return string
886
     */
887
    public function addToUrl($url, $member)
888
    {
889
        $salt = $this->generate(15); // New random salt; Will be passed into url
890
        // Generate salt specific to this member
891
        $memberSalt = $this->memberSalt($salt, $member);
892
        $token = $this->getToken($memberSalt);
893
        return Controller::join_links(
894
            $url,
895
            sprintf(
896
                '?t=%s&s=%s',
897
                urlencode($token),
898
                urlencode($salt)
899
            )
900
        );
901
    }
902
903
    /**
904
     * @param SS_HTTPRequest $request
905
     *
906
     * @return boolean
907
     */
908
    public function checkRequest($request)
909
    {
910
        $member = Member::currentUser();
911
        if (!$member) {
912
            return false;
913
        }
914
915
        $salt = $request->getVar('s');
916
        $memberSalt = $this->memberSalt($salt, $member);
0 ignored issues
show
Compatibility introduced by
$member of type object<DataObject> is not a sub-type of object<Member>. 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...
917
        $token = $this->getToken($memberSalt);
918
919
        // Ensure tokens match
920
        return $token === $request->getVar('t');
921
    }
922
923
924
    /**
925
     * Generates new random key
926
     *
927
     * @param integer $length
0 ignored issues
show
Documentation introduced by
Should the type for parameter $length not be integer|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...
928
     *
929
     * @return string
930
     */
931
    protected function generate($length = null)
932
    {
933
        $generator = new RandomGenerator();
934
        $result = $generator->randomToken('sha256');
935
        if ($length !== null) {
936
            return substr($result, 0, $length);
937
        }
938
        return $result;
939
    }
940
}
941