Completed
Pull Request — master (#173)
by Gordon
01:45
created

Comment::getJavaScriptEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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 {
24
25
	/**
26
	 * @var array
27
	 */
28
	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...
29
		'Name' => 'Varchar(200)',
30
		'Comment' => 'Text',
31
		'Email' => 'Varchar(200)',
32
		'URL' => 'Varchar(255)',
33
		'BaseClass' => 'Varchar(200)',
34
		'Moderated' => 'Boolean(0)',
35
		'IsSpam' => 'Boolean(0)',
36
		'ParentID' => 'Int',
37
		'AllowHtml' => 'Boolean',
38
		'SecretToken' => 'Varchar(255)',
39
		'Depth' => 'Int',
40
	);
41
42
	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...
43
		"Author" => "Member",
44
		"ParentComment" => "Comment",
45
	);
46
47
	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...
48
		"ChildComments"	=> "Comment"
49
	);
50
51
	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...
52
53
	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...
54
		'Moderated' => 0,
55
		'IsSpam' => 0,
56
	);
57
58
	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...
59
		'Title' => 'Varchar',
60
		'ParentTitle' => 'Varchar',
61
		'ParentClassName' => 'Varchar',
62
		'AuthorName' => 'Varchar',
63
		'RSSName' => 'Varchar',
64
		'DeleteLink' => 'Varchar',
65
		'SpamLink' => 'Varchar',
66
		'HamLink' => 'Varchar',
67
		'ApproveLink' => 'Varchar',
68
		'Permalink' => 'Varchar',
69
	);
70
71
	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...
72
		'Name',
73
		'Email',
74
		'Comment',
75
		'Created',
76
		'BaseClass',
77
	);
78
79
	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...
80
		'Name' => 'Submitted By',
81
		'Email' => 'Email',
82
		'Comment.LimitWordCount' => 'Comment',
83
		'Created' => 'Date Posted',
84
		'ParentTitle' => 'Post',
85
		'IsSpam' => 'Is Spam',
86
	);
87
88
	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...
89
		'Author' => 'Author Member',
90
	);
91
92
	public function onBeforeWrite() {
93
		parent::onBeforeWrite();
94
95
		// Sanitize HTML, because its expected to be passed to the template unescaped later
96
		if($this->AllowHtml) {
97
			$this->Comment = $this->purifyHtml($this->Comment);
98
		}
99
100
		// Check comment depth
101
		$this->updateDepth();
102
	}
103
104
	public function onBeforeDelete() {
105
		parent::onBeforeDelete();
106
107
		// Delete all children
108
		foreach($this->ChildComments() as $comment) {
109
			$comment->delete();
110
		}
111
	}
112
113
	/**
114
	 * @return Comment_SecurityToken
115
	 */
116
	public function getSecurityToken() {
117
		return Injector::inst()->createWithArgs('Comment_SecurityToken', array($this));
118
	}
119
120
	/**
121
	 * Migrates the old {@link PageComment} objects to {@link Comment}
122
	 */
123
	public function requireDefaultRecords() {
124
		parent::requireDefaultRecords();
125
126
		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...
127
			$comments = DB::query('SELECT * FROM "PageComment"');
128
129
			if($comments) {
130
				while($pageComment = $comments->nextRecord()) {
131
					// create a new comment from the older page comment
132
					$comment = new Comment();
133
					$comment->update($pageComment);
134
135
					// set the variables which have changed
136
					$comment->BaseClass = 'SiteTree';
137
					$comment->URL = (isset($pageComment['CommenterURL'])) ? $pageComment['CommenterURL'] : '';
138
					if((int) $pageComment['NeedsModeration'] == 0) $comment->Moderated = true;
139
140
					$comment->write();
141
				}
142
			}
143
144
			DB::alteration_message('Migrated PageComment to Comment', 'changed');
145
			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...
146
		}
147
	}
148
149
	/**
150
	 * Return a link to this comment
151
	 *
152
	 * @param string $action
153
	 *
154
	 * @return string link to this comment.
155
	 */
156
	public function Link($action = '') {
157
		if($parent = $this->getParent()) {
158
			return $parent->Link($action) . '#' . $this->Permalink();
159
		}
160
	}
161
162
	/**
163
	 * Returns the permalink for this {@link Comment}. Inserted into
164
	 * the ID tag of the comment
165
	 *
166
	 * @return string
167
	 */
168
	public function Permalink() {
169
		$prefix = $this->getOption('comment_permalink_prefix');
170
		return $prefix . $this->ID;
171
	}
172
173
	/**
174
	 * Translate the form field labels for the CMS administration
175
	 *
176
	 * @param boolean $includerelations
177
	 *
178
	 * @return array
179
	 */
180
	public function fieldLabels($includerelations = true) {
181
		$labels = parent::fieldLabels($includerelations);
182
183
		$labels['Name'] = _t('Comment.NAME', 'Author Name');
184
		$labels['Comment'] = _t('Comment.COMMENT', 'Comment');
185
		$labels['Email'] = _t('Comment.EMAIL', 'Email');
186
		$labels['URL'] = _t('Comment.URL', 'URL');
187
		$labels['IsSpam'] = _t('Comment.ISSPAM', 'Spam?');
188
		$labels['Moderated'] = _t('Comment.MODERATED', 'Moderated?');
189
		$labels['ParentTitle'] = _t('Comment.PARENTTITLE', 'Parent');
190
		$labels['Created'] = _t('Comment.CREATED', 'Date posted');
191
192
		return $labels;
193
	}
194
195
	/**
196
	 * Get the commenting option
197
	 *
198
	 * @param string $key
199
	 *
200
	 * @return mixed Result if the setting is available, or null otherwise
201
	 */
202
	public function getOption($key) {
203
		// If possible use the current record
204
		$record = $this->getParent();
205
206
		if(!$record && $this->BaseClass) {
207
			// Otherwise a singleton of that record
208
			$record = singleton($this->BaseClass);
209
		}
210
		else if(!$record) {
211
			// Otherwise just use the default options
212
			$record = singleton('CommentsExtension');
213
		}
214
215
		return ($record->hasMethod('getCommentsOption')) ? $record->getCommentsOption($key) : null;
216
	}
217
218
	/**
219
	 * Returns the parent {@link DataObject} this comment is attached too
220
	 *
221
	 * @return DataObject
222
	 */
223
	public function getParent() {
224
		return $this->BaseClass && $this->ParentID
225
			? DataObject::get_by_id($this->BaseClass, $this->ParentID, true)
226
			: null;
227
	}
228
229
230
	/**
231
	 * Returns a string to help identify the parent of the comment
232
	 *
233
	 * @return string
234
	 */
235
	public function getParentTitle() {
236
		if($parent = $this->getParent()) {
237
			return $parent->Title ?: ($parent->ClassName . ' #' . $parent->ID);
238
		}
239
	}
240
241
	/**
242
	 * Comment-parent classnames obviously vary, return the parent classname
243
	 *
244
	 * @return string
245
	 */
246
	public function getParentClassName() {
247
		return $this->BaseClass;
248
	}
249
250
	public function castingHelper($field) {
251
		// Safely escape the comment
252
		if($field === 'EscapedComment') {
253
			return $this->AllowHtml ? 'HTMLText' : 'Text';
254
		}
255
		return parent::castingHelper($field);
256
	}
257
258
	/**
259
	 * Content to be safely escaped on the frontend
260
	 *
261
	 * @return string
262
	 */
263
	public function getEscapedComment() {
264
		return $this->Comment;
265
	}
266
267
	/**
268
	 * Return whether this comment is a preview (has not been written to the db)
269
	 *
270
	 * @return boolean
271
	 */
272
	public function isPreview() {
273
		return !$this->exists();
274
	}
275
276
	/**
277
	 * @todo needs to compare to the new {@link Commenting} configuration API
278
	 *
279
	 * @param Member $member
280
	 *
281
	 * @return bool
282
	 */
283
	public function canCreate($member = null) {
284
		return false;
285
	}
286
287
	/**
288
	 * Checks for association with a page, and {@link SiteTree->ProvidePermission}
289
	 * flag being set to true.
290
	 *
291
	 * @param Member $member
292
	 *
293
	 * @return Boolean
294
	 */
295
	public function canView($member = null) {
296
		$member = $this->getMember($member);
297
298
		$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...
299
		if($extended !== null) {
300
			return $extended;
301
		}
302
303
		if(Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
304
			return true;
305
		}
306
307
		if($parent = $this->getParent()) {
308
			return $parent->canView($member)
0 ignored issues
show
Bug introduced by
It seems like $member defined by $this->getMember($member) on line 296 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...
309
				&& $parent->has_extension('CommentsExtension')
310
				&& $parent->CommentsEnabled;
311
		}
312
313
		return false;
314
	}
315
316
	/**
317
	 * Checks if the comment can be edited.
318
	 *
319
	 * @param null|int|Member $member
320
	 *
321
	 * @return Boolean
322
	 */
323
	public function canEdit($member = null) {
324
		$member = $this->getMember($member);
325
326
		if(!$member) {
327
			return false;
328
		}
329
330
		$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...
331
		if($extended !== null) {
332
			return $extended;
333
		}
334
335
		if(Permission::checkMember($member, 'CMS_ACCESS_CommentAdmin')) {
336
			return true;
337
		}
338
339
		if($parent = $this->getParent()) {
340
			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...
341
		}
342
343
		return false;
344
	}
345
346
	/**
347
	 * Checks if the comment can be deleted.
348
	 *
349
	 * @param null|int|Member $member
350
	 *
351
	 * @return Boolean
352
	 */
353
	public function canDelete($member = null) {
354
		$member = $this->getMember($member);
355
356
		if(!$member) {
357
			return false;
358
		}
359
360
		$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...
361
		if($extended !== null) {
362
			return $extended;
363
		}
364
365
		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...
366
	}
367
368
	/**
369
	 * Resolves Member object.
370
	 *
371
	 * @param Member|int|null $member
372
	 * @return Member|null
373
	 */
374
	protected function getMember($member = null) {
375
		if(!$member) {
376
			$member = Member::currentUser();
377
		}
378
379
		if(is_numeric($member)) {
380
			$member = DataObject::get_by_id('Member', $member, true);
381
		}
382
383
		return $member;
384
	}
385
386
	/**
387
	 * Return the authors name for the comment
388
	 *
389
	 * @return string
390
	 */
391
	public function getAuthorName() {
392
		if($this->Name) {
393
			return $this->Name;
394
		} else if($author = $this->Author()) {
395
			return $author->getName();
396
		}
397
	}
398
399
	/**
400
	 * Generate a secure admin-action link authorised for the specified member
401
	 *
402
	 * @param string $action An action on CommentingController to link to
403
	 * @param Member $member The member authorised to invoke this action
404
	 *
405
	 * @return string
406
	 */
407
	protected function actionLink($action, $member = null) {
408
		if(!$member) $member = Member::currentUser();
409
		if(!$member) return false;
410
411
		$url = Controller::join_links(
412
			Director::baseURL(),
413
			'CommentingController',
414
			$action,
415
			$this->ID
416
		);
417
418
		// Limit access for this user
419
		$token = $this->getSecurityToken();
420
		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...
421
	}
422
423
	/**
424
	 * Link to delete this comment
425
	 *
426
	 * @param Member $member
427
	 *
428
	 * @return string
429
	 */
430
	public function DeleteLink($member = null) {
431
		if($this->canDelete($member)) {
432
			return $this->actionLink('delete', $member);
433
		}
434
	}
435
436
	/**
437
	 * Link to mark as spam
438
	 *
439
	 * @param Member $member
440
	 *
441
	 * @return string
442
	 */
443
	public function SpamLink($member = null) {
444
		if($this->canEdit($member) && !$this->IsSpam) {
445
			return $this->actionLink('spam', $member);
446
		}
447
	}
448
449
	/**
450
	 * Link to mark as not-spam (ham)
451
	 *
452
	 * @param Member $member
453
	 *
454
	 * @return string
455
	 */
456
	public function HamLink($member = null) {
457
		if($this->canEdit($member) && $this->IsSpam) {
458
			return $this->actionLink('ham', $member);
459
		}
460
	}
461
462
	/**
463
	 * Link to approve this comment
464
	 *
465
	 * @param Member $member
466
	 *
467
	 * @return string
468
	 */
469
	public function ApproveLink($member = null) {
470
		if($this->canEdit($member) && !$this->Moderated) {
471
			return $this->actionLink('approve', $member);
472
		}
473
	}
474
475
	/**
476
	 * Mark this comment as spam
477
	 */
478
	public function markSpam() {
479
		$this->IsSpam = true;
480
		$this->Moderated = true;
481
		$this->write();
482
		$this->extend('afterMarkSpam');
483
	}
484
485
	/**
486
	 * Mark this comment as approved
487
	 */
488
	public function markApproved() {
489
		$this->IsSpam = false;
490
		$this->Moderated = true;
491
		$this->write();
492
		$this->extend('afterMarkApproved');
493
	}
494
495
	/**
496
	 * Mark this comment as unapproved
497
	 */
498
	public function markUnapproved() {
499
		$this->Moderated = false;
500
		$this->write();
501
		$this->extend('afterMarkUnapproved');
502
	}
503
504
	/**
505
	 * @return string
506
	 */
507
	public function SpamClass() {
508
		if($this->IsSpam) {
509
			return 'spam';
510
		} else if(!$this->Moderated) {
511
			return 'unmoderated';
512
		} else {
513
			return 'notspam';
514
		}
515
	}
516
517
	/**
518
	 * @return string
519
	 */
520
	public function getTitle() {
521
		$title = sprintf(_t('Comment.COMMENTBY', 'Comment by %s', 'Name'), $this->getAuthorName());
522
523
		if($parent = $this->getParent()) {
524
			if($parent->Title) {
525
				$title .= sprintf(' %s %s', _t('Comment.ON', 'on'), $parent->Title);
526
			}
527
		}
528
529
		return $title;
530
	}
531
532
	/*
533
	 * Modify the default fields shown to the user
534
	 */
535
	public function getCMSFields() {
536
		$commentField = $this->AllowHtml ? 'HtmlEditorField' : 'TextareaField';
537
		$fields = new FieldList(
538
			$this
539
				->obj('Created')
540
				->scaffoldFormField($this->fieldLabel('Created'))
541
				->performReadonlyTransformation(),
542
			TextField::create('Name', $this->fieldLabel('AuthorName')),
543
			$commentField::create('Comment', $this->fieldLabel('Comment')),
544
			EmailField::create('Email', $this->fieldLabel('Email')),
545
			TextField::create('URL', $this->fieldLabel('URL')),
546
			FieldGroup::create(array(
547
				CheckboxField::create('Moderated', $this->fieldLabel('Moderated')),
548
				CheckboxField::create('IsSpam', $this->fieldLabel('IsSpam')),
549
			))
550
				->setTitle('Options')
551
				->setDescription(_t(
552
					'Comment.OPTION_DESCRIPTION',
553
					'Unmoderated and spam comments will not be displayed until approved'
554
				))
555
		);
556
557
		// Show member name if given
558
		if(($author = $this->Author()) && $author->exists()) {
559
			$fields->insertAfter(
560
				TextField::create('AuthorMember', $this->fieldLabel('Author'), $author->Title)
561
					->performReadonlyTransformation(),
562
				'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...
563
			);
564
		}
565
566
		// Show parent comment if given
567
		if(($parent = $this->ParentComment()) && $parent->exists()) {
568
			$fields->push(new HeaderField(
569
				'ParentComment_Title',
570
				_t('Comment.ParentComment_Title', 'This comment is a reply to the below')
571
			));
572
			// Created date
573
            // FIXME - the method setName in DatetimeField is not chainable, hence
574
            // the lack of chaining here
575
            $createdField = $parent
576
                    ->obj('Created')
577
                    ->scaffoldFormField($parent->fieldLabel('Created'));
578
            $createdField->setName('ParentComment_Created');
579
            $createdField->setValue($parent->Created);
580
            $createdField->performReadonlyTransformation();
581
            $fields->push($createdField);
582
583
			// Name (could be member or string value)
584
			$fields->push(
585
				$parent
586
					->obj('AuthorName')
587
					->scaffoldFormField($parent->fieldLabel('AuthorName'))
588
					->setName('ParentComment_AuthorName')
589
					->setValue($parent->getAuthorName())
590
					->performReadonlyTransformation()
591
			);
592
593
			// Comment body
594
			$fields->push(
595
				$parent
596
					->obj('EscapedComment')
597
					->scaffoldFormField($parent->fieldLabel('Comment'))
598
					->setName('ParentComment_EscapedComment')
599
					->setValue($parent->Comment)
600
					->performReadonlyTransformation()
601
			);
602
		}
603
604
		$this->extend('updateCMSFields', $fields);
605
		return $fields;
606
	}
607
608
	/**
609
	 * @param  String $dirtyHtml
610
	 *
611
	 * @return String
612
	 */
613
	public function purifyHtml($dirtyHtml) {
614
		$purifier = $this->getHtmlPurifierService();
615
		return $purifier->purify($dirtyHtml);
616
	}
617
618
	/**
619
	 * @return HTMLPurifier (or anything with a "purify()" method)
620
	 */
621
	public function getHtmlPurifierService() {
622
		$config = HTMLPurifier_Config::createDefault();
623
        $allowedElements = $this->getOption('html_allowed_elements');
624
        $config->set('HTML.AllowedElements', $allowedElements);
625
626
        // This injector cannot be set unless the 'p' element is allowed
627
        if (in_array('p', $allowedElements)) {
628
            $config->set('AutoFormat.AutoParagraph', true);
629
        }
630
631
		$config->set('AutoFormat.Linkify', true);
632
		$config->set('URI.DisableExternalResources', true);
633
		$config->set('Cache.SerializerPath', getTempFolder());
634
		return new HTMLPurifier($config);
635
	}
636
637
	/**
638
	 * Calculate the Gravatar link from the email address
639
	 *
640
	 * @return string
641
	 */
642
	public function Gravatar() {
643
		$gravatar = '';
644
		$use_gravatar = $this->getOption('use_gravatar');
645
		if($use_gravatar) {
646
			$gravatar = 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->Email)));
647
			$gravatarsize = $this->getOption('gravatar_size');
648
			$gravatardefault = $this->getOption('gravatar_default');
649
			$gravatarrating = $this->getOption('gravatar_rating');
650
			$gravatar .= '?s=' . $gravatarsize . '&d=' . $gravatardefault . '&r=' . $gravatarrating;
651
		}
652
653
		return $gravatar;
654
	}
655
656
	/**
657
	 * Determine if replies are enabled for this instance
658
	 *
659
	 * @return boolean
660
	 */
661
	public function getRepliesEnabled() {
662
		// Check reply option
663
		if(!$this->getOption('nested_comments')) {
664
			return false;
665
		}
666
667
		// Check if depth is limited
668
		$maxLevel = $this->getOption('nested_depth');
669
		return !$maxLevel || $this->Depth < $maxLevel;
670
	}
671
672
	/**
673
	 * Returns the list of all replies
674
	 *
675
	 * @return SS_List
676
	 */
677
	public function AllReplies() {
678
		// No replies if disabled
679
		if(!$this->getRepliesEnabled()) {
680
			return new ArrayList();
681
		}
682
683
		// Get all non-spam comments
684
		$order = $this->getOption('order_replies_by')
685
			?: $this->getOption('order_comments_by');
686
		$list = $this
687
			->ChildComments()
688
			->sort($order);
689
690
		$this->extend('updateAllReplies', $list);
691
		return $list;
692
	}
693
694
    /**
695
    * Determine if JavaScript is enabled, used in templates
696
    * @return boolean true iff JavaScript enabled with include_js = true in config
697
    */
698
    public function getJavaScriptEnabled() {
699
        return $this->getOption('include_js');
700
    }
701
702
    public function ShowReplyToForm() {
703
        $controller = Controller::curr();
704
        $request = $controller->getRequest();
705
        $replyTo = $request->getVar('replyTo');
706
        $result = $replyTo == $this->ID;
707
        return $result;
708
    }
709
710
	/**
711
	 * Returns the list of replies, with spam and unmoderated items excluded, for use in the frontend
712
	 *
713
	 * @return SS_List
714
	 */
715
	public function Replies() {
716
		// No replies if disabled
717
		if(!$this->getRepliesEnabled()) {
718
			return new ArrayList();
719
		}
720
		$list = $this->AllReplies();
721
722
		// Filter spam comments for non-administrators if configured
723
		$parent = $this->getParent();
724
		$showSpam = $this->getOption('frontend_spam') && $parent && $parent->canModerateComments();
725
		if(!$showSpam) {
726
			$list = $list->filter('IsSpam', 0);
727
		}
728
729
		// Filter un-moderated comments for non-administrators if moderation is enabled
730
		$showUnmoderated = $parent && (
731
			($parent->ModerationRequired === 'None')
732
			|| ($this->getOption('frontend_moderation') && $parent->canModerateComments())
733
		);
734
		if (!$showUnmoderated) {
735
		    $list = $list->filter('Moderated', 1);
736
		}
737
738
		$this->extend('updateReplies', $list);
739
		return $list;
740
	}
741
742
	/**
743
	 * Returns the list of replies paged, with spam and unmoderated items excluded, for use in the frontend
744
	 *
745
	 * @return PaginatedList
746
	 */
747 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...
748
		$list = $this->Replies();
749
750
		// Add pagination
751
		$list = new PaginatedList($list, Controller::curr()->getRequest());
752
		$list->setPaginationGetVar('repliesstart'.$this->ID);
753
		$list->setPageLength($this->getOption('comments_per_page'));
754
755
		$this->extend('updatePagedReplies', $list);
756
		return $list;
757
	}
758
759
	/**
760
	 * Generate a reply form for this comment
761
	 *
762
	 * @return Form
763
	 */
764
	public function ReplyForm() {
765
		// Ensure replies are enabled
766
		if(!$this->getRepliesEnabled()) {
767
			return null;
768
		}
769
770
		// Check parent is available
771
		$parent = $this->getParent();
772
		if(!$parent || !$parent->exists()) {
773
			return null;
774
		}
775
776
		// Build reply controller
777
		$controller = CommentingController::create();
778
		$controller->setOwnerRecord($parent);
779
		$controller->setBaseClass($parent->ClassName);
780
		$controller->setOwnerController(Controller::curr());
781
782
		return $controller->ReplyForm($this);
783
	}
784
785
	/**
786
	 * Refresh of this comment in the hierarchy
787
	 */
788
	public function updateDepth() {
789
		$parent = $this->ParentComment();
790
		if($parent && $parent->exists()) {
791
			$parent->updateDepth();
792
			$this->Depth = $parent->Depth + 1;
793
		} else {
794
			$this->Depth = 1;
795
		}
796
	}
797
}
798
799
800
/**
801
 * Provides the ability to generate cryptographically secure tokens for comment moderation
802
 */
803
class Comment_SecurityToken {
804
805
	private $secret = null;
806
807
	/**
808
	 * @param Comment $comment Comment to generate this token for
809
	 */
810
	public function __construct($comment) {
811
		if(!$comment->SecretToken) {
812
			$comment->SecretToken = $this->generate();
813
			$comment->write();
814
		}
815
		$this->secret = $comment->SecretToken;
816
	}
817
818
	/**
819
	 * Generate the token for the given salt and current secret
820
	 *
821
	 * @param string $salt
822
	 *
823
	 * @return string
824
	 */
825
	protected function getToken($salt) {
826
		return hash_pbkdf2('sha256', $this->secret, $salt, 1000, 30);
827
	}
828
829
	/**
830
	 * Get the member-specific salt.
831
	 *
832
	 * The reason for making the salt specific to a user is that it cannot be "passed in" via a
833
	 * querystring, requiring the same user to be present at both the link generation and the
834
	 * controller action.
835
	 *
836
	 * @param string $salt   Single use salt
837
	 * @param Member $member Member object
838
	 *
839
	 * @return string Generated salt specific to this member
840
	 */
841
	protected function memberSalt($salt, $member) {
842
		// Fallback to salting with ID in case the member has not one set
843
		return $salt . ($member->Salt ?: $member->ID);
844
	}
845
846
	/**
847
	 * @param string $url    Comment action URL
848
	 * @param Member $member Member to restrict access to this action to
849
	 *
850
	 * @return string
851
	 */
852
	public function addToUrl($url, $member) {
853
		$salt = $this->generate(15); // New random salt; Will be passed into url
854
		// Generate salt specific to this member
855
		$memberSalt = $this->memberSalt($salt, $member);
856
		$token = $this->getToken($memberSalt);
857
		return Controller::join_links(
858
			$url,
859
			sprintf(
860
				'?t=%s&s=%s',
861
				urlencode($token),
862
				urlencode($salt)
863
			)
864
		);
865
	}
866
867
	/**
868
	 * @param SS_HTTPRequest $request
869
	 *
870
	 * @return boolean
871
	 */
872
	public function checkRequest($request) {
873
		$member = Member::currentUser();
874
		if(!$member) return false;
875
876
		$salt = $request->getVar('s');
877
		$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...
878
		$token = $this->getToken($memberSalt);
879
880
		// Ensure tokens match
881
		return $token === $request->getVar('t');
882
	}
883
884
885
	/**
886
	 * Generates new random key
887
	 *
888
	 * @param integer $length
889
	 *
890
	 * @return string
891
	 */
892
	protected function generate($length = null) {
893
		$generator = new RandomGenerator();
894
		$result = $generator->randomToken('sha256');
895
		if($length !== null) return substr($result, 0, $length);
896
		return $result;
897
	}
898
}
899