Issues (124)

src/Controllers/CommentingController.php (2 issues)

1
<?php
2
3
namespace SilverStripe\Comments\Controllers;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Comments\Extensions\CommentsExtension;
7
use SilverStripe\Comments\Forms\CommentForm;
8
use SilverStripe\Comments\Model\Comment;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\HTTP;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\HTTPResponse;
14
use SilverStripe\Control\HTTPResponse_Exception;
15
use SilverStripe\Control\RSS\RSSFeed;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Forms\Form;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\FieldType\DBHTMLText;
20
use SilverStripe\ORM\PaginatedList;
21
use SilverStripe\Security\Security;
22
23
/**
24
 * @package comments
25
 */
26
class CommentingController extends Controller
27
{
28
    /**
29
     * {@inheritDoc}
30
     */
31
    private static $allowed_actions = [
32
        'delete',
33
        'spam',
34
        'ham',
35
        'approve',
36
        'rss',
37
        'CommentsForm',
38
        'reply',
39
        'doPostComment',
40
        'doPreviewComment',
41
    ];
42
43
    /**
44
     * {@inheritDoc}
45
     */
46
    private static $url_handlers = [
47
        'reply/$ParentCommentID//$ID/$OtherID' => 'reply',
48
    ];
49
50
    /**
51
     * Fields required for this form
52
     *
53
     * @var array
54
     * @config
55
     */
56
    private static $required_fields = [
57
        'Name',
58
        'Email',
59
        'Comment',
60
    ];
61
62
    /**
63
     * Parent class this commenting form is for
64
     *
65
     * @var string
66
     */
67
    private $parentClass = '';
68
69
    /**
70
     * The record this commenting form is for
71
     *
72
     * @var DataObject
73
     */
74
    private $ownerRecord;
75
76
    /**
77
     * Parent controller record
78
     *
79
     * @var Controller
80
     */
81
    private $ownerController;
82
83
    /**
84
     * Backup url to return to
85
     *
86
     * @var string
87
     */
88
    protected $fallbackReturnURL;
89
90
    /**
91
     * Set the parent class name to use
92
     *
93
     * @param string $class
94
     */
95
    public function setParentClass($class)
96
    {
97
        $this->parentClass = $this->encodeClassName($class);
98
    }
99
100
    /**
101
     * Get the parent class name used
102
     *
103
     * @return string
104
     */
105
    public function getParentClass()
106
    {
107
        return $this->decodeClassName($this->parentClass);
108
    }
109
110
    /**
111
     * Encode a fully qualified class name to a URL-safe version
112
     *
113
     * @param string $input
114
     * @return string
115
     */
116
    public function encodeClassName($input)
117
    {
118
        return str_replace('\\', '-', $input);
119
    }
120
121
    /**
122
     * Decode an "encoded" fully qualified class name back to its original
123
     *
124
     * @param string $input
125
     * @return string
126
     */
127
    public function decodeClassName($input)
128
    {
129
        return str_replace('-', '\\', $input);
130
    }
131
132
    /**
133
     * Set the record this controller is working on
134
     *
135
     * @param DataObject $record
136
     */
137
    public function setOwnerRecord($record)
138
    {
139
        $this->ownerRecord = $record;
140
    }
141
142
    /**
143
     * Get the record
144
     *
145
     * @return DataObject
146
     */
147
    public function getOwnerRecord()
148
    {
149
        return $this->ownerRecord;
150
    }
151
152
    /**
153
     * Set the parent controller
154
     *
155
     * @param Controller $controller
156
     */
157
    public function setOwnerController($controller)
158
    {
159
        $this->ownerController = $controller;
160
    }
161
162
    /**
163
     * Get the parent controller
164
     *
165
     * @return Controller
166
     */
167
    public function getOwnerController()
168
    {
169
        return $this->ownerController;
170
    }
171
172
    /**
173
     * Get the commenting option for the current state
174
     *
175
     * @param string $key
176
     * @return mixed Result if the setting is available, or null otherwise
177
     */
178
    public function getOption($key)
179
    {
180
        // If possible use the current record
181
        if ($record = $this->getOwnerRecord()) {
182
            /** @var DataObject|CommentsExtension $record */
183
            return $record->getCommentsOption($key);
184
        }
185
186
        // Otherwise a singleton of that record
187
        if ($class = $this->getParentClass()) {
188
            return singleton($class)->getCommentsOption($key);
189
        }
190
191
        // Otherwise just use the default options
192
        return singleton(CommentsExtension::class)->getCommentsOption($key);
193
    }
194
195
    /**
196
     * Returns all the commenting options for the current instance.
197
     *
198
     * @return array
199
     */
200
    public function getOptions()
201
    {
202
        if ($record = $this->getOwnerRecord()) {
203
            /** @var DataObject|CommentsExtension $record */
204
            return $record->getCommentsOptions();
205
        }
206
207
        // Otherwise a singleton of that record
208
        if ($class = $this->getParentClass()) {
209
            return singleton($class)->getCommentsOptions();
210
        }
211
212
        // Otherwise just use the default options
213
        return singleton(CommentsExtension::class)->getCommentsOptions();
214
    }
215
216
    /**
217
     * Workaround for generating the link to this controller
218
     *
219
     * @param  string $action
220
     * @param  int    $id
221
     * @param  string $other
222
     * @return string
223
     */
224
    public function Link($action = '', $id = '', $other = '')
225
    {
226
        return Controller::join_links(Director::baseURL(), 'comments', $action, $id, $other);
227
    }
228
229
    /**
230
     * Outputs the RSS feed of comments
231
     *
232
     * @return DBHTMLText
233
     */
234
    public function rss()
235
    {
236
        return $this->getFeed($this->request)->outputToBrowser();
237
    }
238
239
    /**
240
     * Return an RSSFeed of comments for a given set of comments or all
241
     * comments on the website.
242
     *
243
     * @param HTTPRequest
244
     *
245
     * @return RSSFeed
246
     */
247
    public function getFeed(HTTPRequest $request)
248
    {
249
        $link = $this->Link('rss');
250
        $class = $this->decodeClassName($request->param('ID'));
251
        $id = $request->param('OtherID');
252
253
        // Support old pageid param
254
        if (!$id && !$class && ($id = $request->getVar('pageid'))) {
255
            $class = SiteTree::class;
256
        }
257
258
        $comments = Comment::get()->filter([
259
            'Moderated' => 1,
260
            'IsSpam' => 0,
261
        ]);
262
263
        // Check if class filter
264
        if ($class) {
265
            if (!is_subclass_of($class, DataObject::class) || !$class::has_extension(CommentsExtension::class)) {
266
                return $this->httpError(404);
267
            }
268
            $this->setParentClass($class);
269
            $comments = $comments->filter('ParentClass', $class);
270
            $link = Controller::join_links($link, $this->encodeClassName($class));
271
272
            // Check if id filter
273
            if ($id) {
274
                $comments = $comments->filter('ParentID', $id);
275
                $link = Controller::join_links($link, $id);
276
                $this->setOwnerRecord(DataObject::get_by_id($class, $id));
277
            }
278
        }
279
280
        $title = _t(__CLASS__ . '.RSSTITLE', "Comments RSS Feed");
281
        $comments = PaginatedList::create($comments, $request);
282
        $comments->setPageLength($this->getOption('comments_per_page'));
283
284
        return RSSFeed::create(
285
            $comments,
286
            $link,
287
            $title,
288
            $link,
289
            'Title',
290
            'EscapedComment',
291
            'AuthorName'
292
        );
293
    }
294
295
    /**
296
     * Deletes a given {@link Comment} via the URL.
297
     */
298
    public function delete()
299
    {
300
        $comment = $this->getComment();
301
        if (!$comment) {
302
            return $this->httpError(404);
303
        }
304
        if (!$comment->canDelete()) {
305
            return Security::permissionFailure($this, 'You do not have permission to delete this comment');
306
        }
307
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
308
            return $this->httpError(400);
309
        }
310
311
        $comment->delete();
312
313
        return $this->request->isAjax()
314
            ? true
315
            : $this->redirectBack();
316
    }
317
318
    /**
319
     * Marks a given {@link Comment} as spam. Removes the comment from display
320
     */
321
    public function spam()
322
    {
323
        $comment = $this->getComment();
324
        if (!$comment) {
325
            return $this->httpError(404);
326
        }
327
        if (!$comment->canEdit()) {
328
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
329
        }
330
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
331
            return $this->httpError(400);
332
        }
333
334
        $comment->markSpam();
335
        return $this->renderChangedCommentState($comment);
336
    }
337
338
    /**
339
     * Marks a given {@link Comment} as ham (not spam).
340
     */
341
    public function ham()
342
    {
343
        $comment = $this->getComment();
344
        if (!$comment) {
345
            return $this->httpError(404);
346
        }
347
        if (!$comment->canEdit()) {
348
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
349
        }
350
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
351
            return $this->httpError(400);
352
        }
353
354
        $comment->markApproved();
355
        return $this->renderChangedCommentState($comment);
356
    }
357
358
    /**
359
     * Marks a given {@link Comment} as approved.
360
     */
361
    public function approve()
362
    {
363
        $comment = $this->getComment();
364
        if (!$comment) {
365
            return $this->httpError(404);
366
        }
367
        if (!$comment->canEdit()) {
368
            return Security::permissionFailure($this, 'You do not have permission to approve this comment');
369
        }
370
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
371
            return $this->httpError(400);
372
        }
373
        $comment->markApproved();
374
        return $this->renderChangedCommentState($comment);
375
    }
376
377
    /**
378
     * Redirect back to referer if available, ensuring that only site URLs
379
     * are allowed to avoid phishing.  If it's an AJAX request render the
380
     * comment in it's new state
381
     *
382
     * @param Comment $comment
383
     * @return DBHTMLText|HTTPResponse|false
384
     */
385
    private function renderChangedCommentState($comment)
386
    {
387
        $referer = $this->request->getHeader('Referer');
388
389
        // Render comment using AJAX
390
        if ($this->request->isAjax()) {
391
            return $comment->renderWith('Includes/CommentsInterface_singlecomment');
392
        }
393
394
        // Redirect to either the comment or start of the page
395
        if (empty($referer)) {
396
            return $this->redirectBack();
397
        }
398
399
        // Redirect to the comment, but check for phishing
400
        $url = $referer . '#comment-' . $comment->ID;
401
        // absolute redirection URLs not located on this site may cause phishing
402
        if (Director::is_site_url($url)) {
403
            return $this->redirect($url);
404
        }
405
406
        return false;
407
    }
408
409
    /**
410
     * Returns the comment referenced in the URL (by ID). Permission checking
411
     * should be done in the callee.
412
     *
413
     * @return Comment|false
414
     */
415
    public function getComment()
416
    {
417
        $id = isset($this->urlParams['ID']) ? $this->urlParams['ID'] : false;
418
419
        if ($id) {
420
            /** @var Comment $comment */
421
            $comment = Comment::get()->byId($id);
422
            if ($comment) {
0 ignored issues
show
$comment is of type SilverStripe\Comments\Model\Comment, thus it always evaluated to true.
Loading history...
423
                $this->fallbackReturnURL = $comment->Link();
424
                return $comment;
425
            }
426
        }
427
428
        return false;
429
    }
430
431
    /**
432
     * Create a reply form for a specified comment
433
     *
434
     * @param  Comment $comment
435
     * @return Form
436
     */
437
    public function ReplyForm($comment)
438
    {
439
        // Enables multiple forms with different names to use the same handler
440
        $form = $this->CommentsForm();
441
        $form->setName('ReplyForm_' . $comment->ID);
442
        $form->setHTMLID(null);
443
        $form->addExtraClass('reply-form');
444
445
        // Load parent into reply form
446
        $form->loadDataFrom([
447
            'ParentCommentID' => $comment->ID
448
        ]);
449
450
        // Customise action
451
        $form->setFormAction($this->Link('reply', $comment->ID));
452
453
        $this->extend('updateReplyForm', $form);
454
455
        return $form;
456
    }
457
458
459
    /**
460
     * Request handler for reply form.
461
     *
462
     * This method will disambiguate multiple reply forms in the same method
463
     *
464
     * @param  HTTPRequest $request
465
     * @throws HTTPResponse_Exception
466
     */
467
    public function reply(HTTPRequest $request)
468
    {
469
        // Extract parent comment from reply and build this way
470
        if ($parentID = $request->param('ParentCommentID')) {
471
            /** @var Comment $comment */
472
            $comment = DataObject::get_by_id(Comment::class, $parentID, true);
473
            if ($comment) {
0 ignored issues
show
$comment is of type SilverStripe\Comments\Model\Comment, thus it always evaluated to true.
Loading history...
474
                return $this->ReplyForm($comment);
475
            }
476
        }
477
        return $this->httpError(404);
478
    }
479
480
    /**
481
     * Post a comment form
482
     *
483
     * @return Form
484
     */
485
    public function CommentsForm()
486
    {
487
        $form = Injector::inst()->create(CommentForm::class, __FUNCTION__, $this);
488
489
        // hook to allow further extensions to alter the comments form
490
        $this->extend('alterCommentForm', $form);
491
492
        return $form;
493
    }
494
495
496
    /**
497
     * @return HTTPResponse|false
498
     */
499
    public function redirectBack()
500
    {
501
        // Don't cache the redirect back ever
502
        HTTP::set_cache_age(0);
503
504
        $url = null;
505
506
        // In edge-cases, this will be called outside of a handleRequest() context; in that case,
507
        // redirect to the homepage - don't break into the global state at this stage because we'll
508
        // be calling from a test context or something else where the global state is inappropraite
509
        if ($this->request) {
510
            if ($this->request->requestVar('BackURL')) {
511
                $url = $this->request->requestVar('BackURL');
512
            } elseif ($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
513
                $url = $this->request->getHeader('X-Backurl');
514
            } elseif ($this->request->getHeader('Referer')) {
515
                $url = $this->request->getHeader('Referer');
516
            }
517
        }
518
519
        if (!$url) {
520
            $url = $this->fallbackReturnURL;
521
        }
522
        if (!$url) {
523
            $url = Director::baseURL();
524
        }
525
526
        // absolute redirection URLs not located on this site may cause phishing
527
        if (Director::is_site_url($url)) {
528
            return $this->redirect($url);
529
        }
530
531
        return false;
532
    }
533
}
534