Completed
Push — master ( b1bf62...c2fee6 )
by Damian
01:51
created

code/controllers/CommentingController.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @package comments
5
 */
6
7
class CommentingController extends Controller {
8
9
	private static $allowed_actions = array(
10
		'delete',
11
		'spam',
12
		'ham',
13
		'approve',
14
		'rss',
15
		'CommentsForm',
16
		'reply',
17
		'doPostComment',
18
		'doPreviewComment'
19
	);
20
21
	private static $url_handlers = array(
22
		'reply/$ParentCommentID//$ID/$OtherID' => 'reply',
23
	);
24
25
	/**
26
	 * Fields required for this form
27
	 *
28
	 * @var array
29
	 * @config
30
	 */
31
	private static $required_fields = array(
32
		'Name',
33
		'Email',
34
		'Comment'
35
	);
36
37
	/**
38
	 * Base class this commenting form is for
39
	 *
40
	 * @var string
41
	 */
42
	private $baseClass = "";
43
44
	/**
45
	 * The record this commenting form is for
46
	 *
47
	 * @var DataObject
48
	 */
49
	private $ownerRecord = null;
50
51
	/**
52
	 * Parent controller record
53
	 *
54
	 * @var Controller
55
	 */
56
	private $ownerController = null;
57
58
	/**
59
	 * Backup url to return to
60
	 *
61
	 * @var string
62
	 */
63
	protected $fallbackReturnURL = null;
64
65
	/**
66
	 * Set the base class to use
67
	 *
68
	 * @param string $class
69
	 */
70
	public function setBaseClass($class) {
71
		$this->baseClass = $class;
72
	}
73
74
	/**
75
	 * Get the base class used
76
	 *
77
	 * @return string
78
	 */
79
	public function getBaseClass() {
80
		return $this->baseClass;
81
	}
82
83
	/**
84
	 * Set the record this controller is working on
85
	 *
86
	 * @param DataObject $record
87
	 */
88
	public function setOwnerRecord($record) {
89
		$this->ownerRecord = $record;
90
	}
91
92
	/**
93
	 * Get the record
94
	 *
95
	 * @return DataObject
96
	 */
97
	public function getOwnerRecord() {
98
		return $this->ownerRecord;
99
	}
100
101
	/**
102
	 * Set the parent controller
103
	 *
104
	 * @param Controller $controller
105
	 */
106
	public function setOwnerController($controller) {
107
		$this->ownerController = $controller;
108
	}
109
110
	/**
111
	 * Get the parent controller
112
	 *
113
	 * @return Controller
114
	 */
115
	public function getOwnerController() {
116
		return $this->ownerController;
117
	}
118
119
	/**
120
	 * Get the commenting option for the current state
121
	 *
122
	 * @param string $key
123
	 * @return mixed Result if the setting is available, or null otherwise
124
	 */
125
	public function getOption($key) {
126
		// If possible use the current record
127
		if($record = $this->getOwnerRecord()) {
128
			return $record->getCommentsOption($key);
129
		}
130
131
		// Otherwise a singleton of that record
132
		if($class = $this->getBaseClass()) {
133
			return singleton($class)->getCommentsOption($key);
134
		}
135
136
		// Otherwise just use the default options
137
		return singleton('CommentsExtension')->getCommentsOption($key);
138
	}
139
140
	/**
141
	 * Workaround for generating the link to this controller
142
	 *
143
	 * @return string
144
	 */
145
	public function Link($action = '', $id = '', $other = '') {
146
		return Controller::join_links(Director::baseURL(), __CLASS__ , $action, $id, $other);
147
	}
148
149
	/**
150
	 * Outputs the RSS feed of comments
151
	 *
152
	 * @return HTMLText
153
	 */
154
	public function rss() {
155
		return $this->getFeed($this->request)->outputToBrowser();
156
	}
157
158
	/**
159
	 * Return an RSSFeed of comments for a given set of comments or all
160
	 * comments on the website.
161
	 *
162
	 * To maintain backwards compatibility with 2.4 this supports mapping
163
	 * of PageComment/rss?pageid= as well as the new RSS format for comments
164
	 * of CommentingController/rss/{classname}/{id}
165
	 *
166
	 * @param SS_HTTPRequest
167
	 *
168
	 * @return RSSFeed
169
	 */
170
	public function getFeed(SS_HTTPRequest $request) {
171
		$link = $this->Link('rss');
172
		$class = $request->param('ID');
173
		$id = $request->param('OtherID');
174
175
		// Support old pageid param
176
		if(!$id && !$class && ($id = $request->getVar('pageid'))) {
177
			$class = 'SiteTree';
178
		}
179
180
		$comments = Comment::get()->filter(array(
181
			'Moderated' => 1,
182
			'IsSpam' => 0,
183
		));
184
185
		// Check if class filter
186
		if($class) {
187
			if(!is_subclass_of($class, 'DataObject') || !$class::has_extension('CommentsExtension')) {
188
				return $this->httpError(404);
189
			}
190
			$this->setBaseClass($class);
191
			$comments = $comments->filter('BaseClass', $class);
192
			$link = Controller::join_links($link, $class);
193
194
			// Check if id filter
195
			if($id) {
196
				$comments = $comments->filter('ParentID', $id);
197
				$link = Controller::join_links($link, $id);
198
				$this->setOwnerRecord(DataObject::get_by_id($class, $id));
199
			}
200
		}
201
202
		$title = _t('CommentingController.RSSTITLE', "Comments RSS Feed");
203
204
		$comments = new PaginatedList($comments, $request);
205
		$comments->setPageLength($this->getOption('comments_per_page'));
206
207
		return new RSSFeed(
208
			$comments,
209
			$link,
210
			$title,
211
			$link,
212
			'Title', 'EscapedComment', 'AuthorName'
213
		);
214
	}
215
216
	/**
217
	 * Deletes a given {@link Comment} via the URL.
218
	 */
219 View Code Duplication
	public function delete() {
220
		$comment = $this->getComment();
221
		if(!$comment) return $this->httpError(404);
222
		if(!$comment->canDelete()) {
223
			return Security::permissionFailure($this, 'You do not have permission to delete this comment');
224
		}
225
		if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
226
227
		$comment->delete();
228
229
		return $this->request->isAjax()
230
			? true
231
			: $this->redirectBack();
232
	}
233
234
	/**
235
	 * Marks a given {@link Comment} as spam. Removes the comment from display
236
	 */
237 View Code Duplication
	public function spam() {
238
		$comment = $this->getComment();
239
		if(!$comment) return $this->httpError(404);
240
		if(!$comment->canEdit()) {
241
			return Security::permissionFailure($this, 'You do not have permission to edit this comment');
242
		}
243
		if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
244
245
		$comment->markSpam();
246
        return $this->renderChangedCommentState($comment);
247
	}
248
249
	/**
250
	 * Marks a given {@link Comment} as ham (not spam).
251
	 */
252 View Code Duplication
	public function ham() {
253
		$comment = $this->getComment();
254
		if(!$comment) return $this->httpError(404);
255
		if(!$comment->canEdit()) {
256
			return Security::permissionFailure($this, 'You do not have permission to edit this comment');
257
		}
258
		if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
259
260
		$comment->markApproved();
261
        return $this->renderChangedCommentState($comment);
262
	}
263
264
	/**
265
	 * Marks a given {@link Comment} as approved.
266
	 */
267 View Code Duplication
	public function approve() {
268
		$comment = $this->getComment();
269
		if(!$comment) return $this->httpError(404);
270
		if(!$comment->canEdit()) {
271
			return Security::permissionFailure($this, 'You do not have permission to approve this comment');
272
		}
273
		if(!$comment->getSecurityToken()->checkRequest($this->request)) return $this->httpError(400);
274
275
		$comment->markApproved();
276
        return $this->renderChangedCommentState($comment);
277
	}
278
279
    /**
280
     * Redirect back to referer if available, ensuring that only site URLs
281
     * are allowed to avoid phishing.  If it's an AJAX request render the
282
     * comment in it's new state
283
     */
284
    private function renderChangedCommentState($comment) {
285
        $referer = $this->request->getHeader('Referer');
286
287
        // Render comment using AJAX
288
        if ($this->request->isAjax()) {
289
            return $comment->renderWith('CommentsInterface_singlecomment');
290
        } else {
291
            // Redirect to either the comment or start of the page
292
            if (empty($referer)) {
293
                return $this->redirectBack();
294
            } else {
295
                // Redirect to the comment, but check for phishing
296
                $url = $referer . '#comment-' . $comment->ID;
297
                // absolute redirection URLs not located on this site may cause phishing
298
                if(Director::is_site_url($url)) {
299
                    return $this->redirect($url);
300
                } else {
301
                    return false;
302
                }
303
            }
304
        }
305
    }
306
307
	/**
308
	 * Returns the comment referenced in the URL (by ID). Permission checking
309
	 * should be done in the callee.
310
	 *
311
	 * @return Comment|false
312
	 */
313
	public function getComment() {
314
		$id = isset($this->urlParams['ID']) ? $this->urlParams['ID'] : false;
315
316
		if($id) {
317
			$comment = DataObject::get_by_id('Comment', $id);
318
319
			if($comment) {
320
				$this->fallbackReturnURL = $comment->Link();
321
				return $comment;
322
			}
323
		}
324
325
		return false;
326
	}
327
328
	/**
329
	 * Create a reply form for a specified comment
330
	 *
331
	 * @param Comment $comment
332
	 */
333
	public function ReplyForm($comment) {
334
		// Enables multiple forms with different names to use the same handler
335
		$form = $this->CommentsForm();
336
		$form->setName('ReplyForm_'.$comment->ID);
337
		$form->addExtraClass('reply-form');
338
339
		// Load parent into reply form
340
		$form->loadDataFrom(array(
341
			'ParentCommentID' => $comment->ID
342
		));
343
344
		// Customise action
345
		$form->setFormAction($this->Link('reply', $comment->ID));
346
347
		$this->extend('updateReplyForm', $form);
348
		return $form;
349
	}
350
351
352
	/**
353
	 * Request handler for reply form.
354
	 * This method will disambiguate multiple reply forms in the same method
355
	 *
356
	 * @param SS_HTTPRequest $request
357
	 */
358
	public function reply(SS_HTTPRequest $request) {
359
		// Extract parent comment from reply and build this way
360
		if($parentID = $request->param('ParentCommentID')) {
361
			$comment = DataObject::get_by_id('Comment', $parentID, true);
362
			if($comment) {
363
				return $this->ReplyForm($comment);
364
			}
365
		}
366
		return $this->httpError(404);
367
	}
368
369
	/**
370
	 * Post a comment form
371
	 *
372
	 * @return Form
373
	 */
374
	public function CommentsForm() {
375
		$usePreview = $this->getOption('use_preview');
376
377
		$nameRequired = _t('CommentInterface.YOURNAME_MESSAGE_REQUIRED', 'Please enter your name');
378
		$emailRequired = _t('CommentInterface.EMAILADDRESS_MESSAGE_REQUIRED', 'Please enter your email address');
379
		$emailInvalid = _t('CommentInterface.EMAILADDRESS_MESSAGE_EMAIL', 'Please enter a valid email address');
380
		$urlInvalid = _t('CommentInterface.COMMENT_MESSAGE_URL', 'Please enter a valid URL');
381
		$commentRequired = _t('CommentInterface.COMMENT_MESSAGE_REQUIRED', 'Please enter your comment');
382
383
		$fields = new FieldList(
384
			$dataFields = new CompositeField(
385
				// Name
386
				TextField::create("Name", _t('CommentInterface.YOURNAME', 'Your name'))
387
					->setCustomValidationMessage($nameRequired)
388
					->setAttribute('data-msg-required', $nameRequired),
389
390
				// Email
391
				EmailField::create(
392
					"Email",
393
					_t('CommentingController.EMAILADDRESS', "Your email address (will not be published)")
394
				)
395
					->setCustomValidationMessage($emailRequired)
396
					->setAttribute('data-msg-required', $emailRequired)
397
					->setAttribute('data-msg-email', $emailInvalid)
398
					->setAttribute('data-rule-email', true),
399
400
				// Url
401
				TextField::create("URL", _t('CommentingController.WEBSITEURL', "Your website URL"))
402
					->setAttribute('data-msg-url', $urlInvalid)
403
					->setAttribute('data-rule-url', true),
404
405
				// Comment
406
				TextareaField::create("Comment", _t('CommentingController.COMMENTS', "Comments"))
407
					->setCustomValidationMessage($commentRequired)
408
					->setAttribute('data-msg-required', $commentRequired)
409
			),
410
			HiddenField::create("ParentID"),
411
			HiddenField::create("ReturnURL"),
412
			HiddenField::create("ParentCommentID"),
413
			HiddenField::create("BaseClass")
414
		);
415
416
		// Preview formatted comment. Makes most sense when shortcodes or
417
		// limited HTML is allowed. Populated by JS/Ajax.
418
		if($usePreview) {
419
			$fields->insertAfter(
420
				ReadonlyField::create('PreviewComment', _t('CommentInterface.PREVIEWLABEL', 'Preview'))
421
					->setAttribute('style', 'display: none'), // enable through JS
422
				'Comment'
423
			);
424
		}
425
426
		$dataFields->addExtraClass('data-fields');
427
428
		// save actions
429
		$actions = new FieldList(
430
			new FormAction("doPostComment", _t('CommentInterface.POST', 'Post'))
431
		);
432
		if($usePreview) {
433
			$actions->push(
434
				FormAction::create('doPreviewComment', _t('CommentInterface.PREVIEW', 'Preview'))
435
					->addExtraClass('action-minor')
436
					->setAttribute('style', 'display: none') // enable through JS
437
			);
438
		}
439
440
		// required fields for server side
441
		$required = new RequiredFields($this->config()->required_fields);
442
443
		// create the comment form
444
		$form = new Form($this, 'CommentsForm', $fields, $actions, $required);
445
446
		// if the record exists load the extra required data
447
		if($record = $this->getOwnerRecord()) {
448
449
			// Load member data
450
			$member = Member::currentUser();
451
			if(($record->CommentsRequireLogin || $record->PostingRequiredPermission) && $member) {
452
				$fields = $form->Fields();
453
454
				$fields->removeByName('Name');
455
				$fields->removeByName('Email');
456
				$fields->insertBefore(new ReadonlyField("NameView", _t('CommentInterface.YOURNAME', 'Your name'), $member->getName()), 'URL');
457
				$fields->push(new HiddenField("Name", "", $member->getName()));
458
				$fields->push(new HiddenField("Email", "", $member->Email));
459
			}
460
461
			// we do not want to read a new URL when the form has already been submitted
462
			// which in here, it hasn't been.
463
			$form->loadDataFrom(array(
464
				'ParentID'		=> $record->ID,
465
				'ReturnURL'		=> $this->request->getURL(),
466
				'BaseClass'		=> $this->getBaseClass()
467
			));
468
		}
469
470
		// Set it so the user gets redirected back down to the form upon form fail
471
		$form->setRedirectToFormOnValidationError(true);
472
473
		// load any data from the cookies
474
		if($data = Cookie::get('CommentsForm_UserData')) {
475
			$data = Convert::json2array($data);
476
477
			$form->loadDataFrom(array(
478
				"Name"		=> isset($data['Name']) ? $data['Name'] : '',
479
				"URL"		=> isset($data['URL']) ? $data['URL'] : '',
480
				"Email"		=> isset($data['Email']) ? $data['Email'] : ''
481
			));
482
			// allow previous value to fill if comment not stored in cookie (i.e. validation error)
483
			$prevComment = Cookie::get('CommentsForm_Comment');
484
			if($prevComment && $prevComment != ''){
0 ignored issues
show
Bug Best Practice introduced by
The expression $prevComment of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
485
				$form->loadDataFrom(array("Comment" => $prevComment));
486
			}
487
		}
488
489
		if(!empty($member)) {
490
			$form->loadDataFrom($member);
491
		}
492
493
		// hook to allow further extensions to alter the comments form
494
		$this->extend('alterCommentForm', $form);
495
496
		return $form;
497
	}
498
499
	/**
500
	 * Process which creates a {@link Comment} once a user submits a comment from this form.
501
	 *
502
	 * @param array $data
503
	 * @param Form $form
504
	 */
505
	public function doPostComment($data, $form) {
506
		// Load class and parent from data
507
		if(isset($data['BaseClass'])) {
508
			$this->setBaseClass($data['BaseClass']);
509
		}
510
		if(isset($data['ParentID']) && ($class = $this->getBaseClass())) {
511
			$this->setOwnerRecord($class::get()->byID($data['ParentID']));
512
		}
513
		if(!$this->getOwnerRecord()) return $this->httpError(404);
514
515
		// cache users data
516
		Cookie::set("CommentsForm_UserData", Convert::raw2json($data));
517
		Cookie::set("CommentsForm_Comment", $data['Comment']);
518
519
		// extend hook to allow extensions. Also see onAfterPostComment
520
		$this->extend('onBeforePostComment', $form);
521
522
		// If commenting can only be done by logged in users, make sure the user is logged in
523
		if(!$this->getOwnerRecord()->canPostComment()) {
524
			return Security::permissionFailure(
525
				$this,
526
				_t(
527
					'CommentingController.PERMISSIONFAILURE',
528
					"You're not able to post comments to this page. Please ensure you are logged in and have an "
529
					. "appropriate permission level."
530
				)
531
			);
532
		}
533
534
		if($member = Member::currentUser()) {
535
			$form->Fields()->push(new HiddenField("AuthorID", "Author ID", $member->ID));
536
		}
537
538
		// What kind of moderation is required?
539
		switch($this->getOwnerRecord()->ModerationRequired) {
540
			case 'Required':
541
				$requireModeration = true;
542
				break;
543
			case 'NonMembersOnly':
544
				$requireModeration = empty($member);
545
				break;
546
			case 'None':
547
			default:
548
				$requireModeration = false;
549
				break;
550
		}
551
552
		$comment = new Comment();
553
		$form->saveInto($comment);
554
555
		$comment->AllowHtml = $this->getOption('html_allowed');
556
		$comment->Moderated = !$requireModeration;
557
558
		// Save into DB, or call pre-save hooks to give accurate preview
559
		$usePreview = $this->getOption('use_preview');
560
		$isPreview = $usePreview && !empty($data['IsPreview']);
561
		if($isPreview) {
562
			$comment->extend('onBeforeWrite');
563
		} else {
564
			$comment->write();
565
566
			// extend hook to allow extensions. Also see onBeforePostComment
567
			$this->extend('onAfterPostComment', $comment);
568
		}
569
570
		// we want to show a notification if comments are moderated
571
		if ($requireModeration && !$comment->IsSpam) {
572
			Session::set('CommentsModerated', 1);
573
		}
574
575
		// clear the users comment since it passed validation
576
		Cookie::set('CommentsForm_Comment', false);
577
578
		// Find parent link
579
		if(!empty($data['ReturnURL'])) {
580
			$url = $data['ReturnURL'];
581
		} elseif($parent = $comment->getParent()) {
582
			$url = $parent->Link();
583
		} else {
584
			return $this->redirectBack();
585
		}
586
587
		// Given a redirect page exists, attempt to link to the correct anchor
588
		if($comment->IsSpam) {
589
			// Link to the form with the error message contained
590
			$hash = $form->FormName();
591
		} else if(!$comment->Moderated) {
592
			// Display the "awaiting moderation" text
593
			$holder = $this->getOption('comments_holder_id');
594
			$hash = "{$holder}_PostCommentForm_error";
595
		} else {
596
			// Link to the moderated, non-spam comment
597
			$hash = $comment->Permalink();
598
		}
599
600
		return $this->redirect(Controller::join_links($url, "#{$hash}"));
601
	}
602
603
	public function doPreviewComment($data, $form) {
604
		$data['IsPreview'] = 1;
605
606
		return $this->doPostComment($data, $form);
607
	}
608
609
	public function redirectBack() {
610
		// Don't cache the redirect back ever
611
		HTTP::set_cache_age(0);
612
613
		$url = null;
614
615
		// In edge-cases, this will be called outside of a handleRequest() context; in that case,
616
		// redirect to the homepage - don't break into the global state at this stage because we'll
617
		// be calling from a test context or something else where the global state is inappropraite
618
		if($this->request) {
619
			if($this->request->requestVar('BackURL')) {
620
				$url = $this->request->requestVar('BackURL');
621
			} else if($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
622
				$url = $this->request->getHeader('X-Backurl');
623
			} else if($this->request->getHeader('Referer')) {
624
				$url = $this->request->getHeader('Referer');
625
			}
626
		}
627
628
		if(!$url) $url = $this->fallbackReturnURL;
629
		if(!$url) $url = Director::baseURL();
630
631
		// absolute redirection URLs not located on this site may cause phishing
632
		if(Director::is_site_url($url)) {
633
			return $this->redirect($url);
634
		} else {
635
			return false;
636
		}
637
638
	}
639
}
640