Completed
Push — master ( 82c4a1...4bf0a8 )
by Robbie
10:38
created

src/Controllers/CommentingController.php (2 issues)

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
namespace SilverStripe\Comments\Controllers;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Comments\Extensions\CommentsExtension;
7
use SilverStripe\Comments\Model\Comment;
8
use SilverStripe\Control\Director;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Email\Email;
11
use SilverStripe\Control\HTTP;
12
use SilverStripe\Control\HTTPRequest;
13
use SilverStripe\Control\RSS\RSSFeed;
14
use SilverStripe\Control\Session;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\ORM\PaginatedList;
17
use SilverStripe\Security\Member;
18
use SilverStripe\Security\Security;
19
use SilverStripe\Core\Injector\Injector;
20
use SilverStripe\Comments\Forms\CommentForm;
21
22
/**
23
 * @package comments
24
 */
25
class CommentingController extends Controller
26
{
27
    /**
28
     * {@inheritDoc}
29
     */
30
    private static $allowed_actions = array(
31
        'delete',
32
        'spam',
33
        'ham',
34
        'approve',
35
        'rss',
36
        'CommentsForm',
37
        'reply',
38
        'doPostComment',
39
        'doPreviewComment'
40
    );
41
42
    /**
43
     * {@inheritDoc}
44
     */
45
    private static $url_handlers = array(
46
        'reply/$ParentCommentID//$ID/$OtherID' => 'reply',
47
    );
48
49
    /**
50
     * Fields required for this form
51
     *
52
     * @var array
53
     * @config
54
     */
55
    private static $required_fields = array(
56
        'Name',
57
        'Email',
58
        'Comment'
59
    );
60
61
    /**
62
     * Parent class this commenting form is for
63
     *
64
     * @var string
65
     */
66
    private $parentClass = '';
67
68
    /**
69
     * The record this commenting form is for
70
     *
71
     * @var DataObject
72
     */
73
    private $ownerRecord = null;
74
75
    /**
76
     * Parent controller record
77
     *
78
     * @var Controller
79
     */
80
    private $ownerController = null;
81
82
    /**
83
     * Backup url to return to
84
     *
85
     * @var string
86
     */
87
    protected $fallbackReturnURL = null;
88
89
    /**
90
     * Set the parent class name to use
91
     *
92
     * @param string $class
93
     */
94
    public function setParentClass($class)
95
    {
96
        $this->parentClass = $this->encodeClassName($class);
97
    }
98
99
    /**
100
     * Get the parent class name used
101
     *
102
     * @return string
103
     */
104
    public function getParentClass()
105
    {
106
        return $this->decodeClassName($this->parentClass);
107
    }
108
109
    /**
110
     * Encode a fully qualified class name to a URL-safe version
111
     *
112
     * @param string $input
113
     * @return string
114
     */
115
    public function encodeClassName($input)
116
    {
117
        return str_replace('\\', '-', $input);
118
    }
119
120
    /**
121
     * Decode an "encoded" fully qualified class name back to its original
122
     *
123
     * @param string $input
124
     * @return string
125
     */
126
    public function decodeClassName($input)
127
    {
128
        return str_replace('-', '\\', $input);
129
    }
130
131
    /**
132
     * Set the record this controller is working on
133
     *
134
     * @param DataObject $record
135
     */
136
    public function setOwnerRecord($record)
137
    {
138
        $this->ownerRecord = $record;
139
    }
140
141
    /**
142
     * Get the record
143
     *
144
     * @return DataObject
145
     */
146
    public function getOwnerRecord()
147
    {
148
        return $this->ownerRecord;
149
    }
150
151
    /**
152
     * Set the parent controller
153
     *
154
     * @param Controller $controller
155
     */
156
    public function setOwnerController($controller)
157
    {
158
        $this->ownerController = $controller;
159
    }
160
161
    /**
162
     * Get the parent controller
163
     *
164
     * @return Controller
165
     */
166
    public function getOwnerController()
167
    {
168
        return $this->ownerController;
169
    }
170
171
    /**
172
     * Get the commenting option for the current state
173
     *
174
     * @param string $key
175
     * @return mixed Result if the setting is available, or null otherwise
176
     */
177 View Code Duplication
    public function getOption($key)
178
    {
179
        // If possible use the current record
180
        if ($record = $this->getOwnerRecord()) {
181
            return $record->getCommentsOption($key);
182
        }
183
184
        // Otherwise a singleton of that record
185
        if ($class = $this->getParentClass()) {
186
            return singleton($class)->getCommentsOption($key);
187
        }
188
189
        // Otherwise just use the default options
190
        return singleton(CommentsExtension::class)->getCommentsOption($key);
191
    }
192
193
    /**
194
     * Returns all the commenting options for the current instance.
195
     *
196
     * @return array
197
     */
198 View Code Duplication
    public function getOptions()
199
    {
200
        if ($record = $this->getOwnerRecord()) {
201
            return $record->getCommentsOptions();
202
        }
203
204
        // Otherwise a singleton of that record
205
        if ($class = $this->getParentClass()) {
206
            return singleton($class)->getCommentsOptions();
207
        }
208
209
        // Otherwise just use the default options
210
        return singleton(CommentsExtension::class)->getCommentsOptions();
211
    }
212
213
    /**
214
     * Workaround for generating the link to this controller
215
     *
216
     * @param  string $action
217
     * @param  int    $id
0 ignored issues
show
Should the type for parameter $id not be string|integer?

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...
218
     * @param  string $other
219
     * @return string
220
     */
221
    public function Link($action = '', $id = '', $other = '')
222
    {
223
        return Controller::join_links(Director::baseURL(), 'comments', $action, $id, $other);
224
    }
225
226
    /**
227
     * Outputs the RSS feed of comments
228
     *
229
     * @return HTMLText
230
     */
231
    public function rss()
232
    {
233
        return $this->getFeed($this->request)->outputToBrowser();
234
    }
235
236
    /**
237
     * Return an RSSFeed of comments for a given set of comments or all
238
     * comments on the website.
239
     *
240
     * @param HTTPRequest
241
     *
242
     * @return RSSFeed
243
     */
244
    public function getFeed(HTTPRequest $request)
245
    {
246
        $link = $this->Link('rss');
247
        $class = $this->decodeClassName($request->param('ID'));
248
        $id = $request->param('OtherID');
249
250
        // Support old pageid param
251
        if (!$id && !$class && ($id = $request->getVar('pageid'))) {
252
            $class = SiteTree::class;
253
        }
254
255
        $comments = Comment::get()->filter(array(
256
            'Moderated' => 1,
257
            'IsSpam' => 0,
258
        ));
259
260
        // Check if class filter
261
        if ($class) {
262
            if (!is_subclass_of($class, DataObject::class) || !$class::has_extension(CommentsExtension::class)) {
263
                return $this->httpError(404);
264
            }
265
            $this->setParentClass($class);
266
            $comments = $comments->filter('ParentClass', $class);
267
            $link = Controller::join_links($link, $this->encodeClassName($class));
268
269
            // Check if id filter
270
            if ($id) {
271
                $comments = $comments->filter('ParentID', $id);
272
                $link = Controller::join_links($link, $id);
273
                $this->setOwnerRecord(DataObject::get_by_id($class, $id));
274
            }
275
        }
276
277
        $title = _t('SilverStripe\\Comments\\Controllers\\CommentingController.RSSTITLE', "Comments RSS Feed");
278
        $comments = new PaginatedList($comments, $request);
279
        $comments->setPageLength($this->getOption('comments_per_page'));
280
281
        return new RSSFeed(
282
            $comments,
283
            $link,
284
            $title,
285
            $link,
286
            'Title',
287
            'EscapedComment',
288
            'AuthorName'
289
        );
290
    }
291
292
    /**
293
     * Deletes a given {@link Comment} via the URL.
294
     */
295 View Code Duplication
    public function delete()
296
    {
297
        $comment = $this->getComment();
298
        if (!$comment) {
299
            return $this->httpError(404);
300
        }
301
        if (!$comment->canDelete()) {
302
            return Security::permissionFailure($this, 'You do not have permission to delete this comment');
303
        }
304
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
305
            return $this->httpError(400);
306
        }
307
308
        $comment->delete();
309
310
        return $this->request->isAjax()
311
            ? true
312
            : $this->redirectBack();
313
    }
314
315
    /**
316
     * Marks a given {@link Comment} as spam. Removes the comment from display
317
     */
318 View Code Duplication
    public function spam()
319
    {
320
        $comment = $this->getComment();
321
        if (!$comment) {
322
            return $this->httpError(404);
323
        }
324
        if (!$comment->canEdit()) {
325
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
326
        }
327
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
328
            return $this->httpError(400);
329
        }
330
331
        $comment->markSpam();
332
        return $this->renderChangedCommentState($comment);
333
    }
334
335
    /**
336
     * Marks a given {@link Comment} as ham (not spam).
337
     */
338 View Code Duplication
    public function ham()
339
    {
340
        $comment = $this->getComment();
341
        if (!$comment) {
342
            return $this->httpError(404);
343
        }
344
        if (!$comment->canEdit()) {
345
            return Security::permissionFailure($this, 'You do not have permission to edit this comment');
346
        }
347
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
348
            return $this->httpError(400);
349
        }
350
351
        $comment->markApproved();
352
        return $this->renderChangedCommentState($comment);
353
    }
354
355
    /**
356
     * Marks a given {@link Comment} as approved.
357
     */
358 View Code Duplication
    public function approve()
359
    {
360
        $comment = $this->getComment();
361
        if (!$comment) {
362
            return $this->httpError(404);
363
        }
364
        if (!$comment->canEdit()) {
365
            return Security::permissionFailure($this, 'You do not have permission to approve this comment');
366
        }
367
        if (!$comment->getSecurityToken()->checkRequest($this->request)) {
368
            return $this->httpError(400);
369
        }
370
        $comment->markApproved();
371
        return $this->renderChangedCommentState($comment);
372
    }
373
374
    /**
375
     * Redirect back to referer if available, ensuring that only site URLs
376
     * are allowed to avoid phishing.  If it's an AJAX request render the
377
     * comment in it's new state
378
     */
379
    private function renderChangedCommentState($comment)
380
    {
381
        $referer = $this->request->getHeader('Referer');
382
383
        // Render comment using AJAX
384
        if ($this->request->isAjax()) {
385
            return $comment->renderWith('Includes/CommentsInterface_singlecomment');
386
        } else {
387
            // Redirect to either the comment or start of the page
388
            if (empty($referer)) {
389
                return $this->redirectBack();
390
            } else {
391
                // Redirect to the comment, but check for phishing
392
                $url = $referer . '#comment-' . $comment->ID;
393
                // absolute redirection URLs not located on this site may cause phishing
394
                if (Director::is_site_url($url)) {
395
                    return $this->redirect($url);
396
                } else {
397
                    return false;
398
                }
399
            }
400
        }
401
    }
402
403
    /**
404
     * Returns the comment referenced in the URL (by ID). Permission checking
405
     * should be done in the callee.
406
     *
407
     * @return Comment|false
408
     */
409
    public function getComment()
410
    {
411
        $id = isset($this->urlParams['ID']) ? $this->urlParams['ID'] : false;
412
413
        if ($id) {
414
            $comment = Comment::get()->byId($id);
415
            if ($comment) {
416
                $this->fallbackReturnURL = $comment->Link();
417
                return $comment;
418
            }
419
        }
420
421
        return false;
422
    }
423
424
    /**
425
     * Create a reply form for a specified comment
426
     *
427
     * @param  Comment $comment
428
     * @return Form
429
     */
430
    public function ReplyForm($comment)
431
    {
432
        // Enables multiple forms with different names to use the same handler
433
        $form = $this->CommentsForm();
434
        $form->setName('ReplyForm_' . $comment->ID);
435
        $form->addExtraClass('reply-form');
436
437
        // Load parent into reply form
438
        $form->loadDataFrom(array(
439
            'ParentCommentID' => $comment->ID
440
        ));
441
442
        // Customise action
443
        $form->setFormAction($this->Link('reply', $comment->ID));
444
445
        $this->extend('updateReplyForm', $form);
446
447
        return $form;
448
    }
449
450
451
    /**
452
     * Request handler for reply form.
453
     *
454
     * This method will disambiguate multiple reply forms in the same method
455
     *
456
     * @param  HTTPRequest $request
457
     * @throws HTTPResponse_Exception
458
     */
459
    public function reply(HTTPRequest $request)
460
    {
461
        // Extract parent comment from reply and build this way
462
        if ($parentID = $request->param('ParentCommentID')) {
463
            $comment = DataObject::get_by_id(Comment::class, $parentID, true);
464
            if ($comment) {
465
                return $this->ReplyForm($comment);
466
            }
467
        }
468
        return $this->httpError(404);
469
    }
470
471
    /**
472
     * Post a comment form
473
     *
474
     * @return Form
475
     */
476
    public function CommentsForm()
477
    {
478
        return Injector::inst()->create(CommentForm::class, __FUNCTION__, $this);
479
    }
480
481
482
    /**
483
     * @return HTTPResponse|false
0 ignored issues
show
Should the return type not be \SilverStripe\Control\HTTPResponse|null|false?

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 redirectBack()
486
    {
487
        // Don't cache the redirect back ever
488
        HTTP::set_cache_age(0);
489
490
        $url = null;
491
492
        // In edge-cases, this will be called outside of a handleRequest() context; in that case,
493
        // redirect to the homepage - don't break into the global state at this stage because we'll
494
        // be calling from a test context or something else where the global state is inappropraite
495
        if ($this->request) {
496
            if ($this->request->requestVar('BackURL')) {
497
                $url = $this->request->requestVar('BackURL');
498
            } elseif ($this->request->isAjax() && $this->request->getHeader('X-Backurl')) {
499
                $url = $this->request->getHeader('X-Backurl');
500
            } elseif ($this->request->getHeader('Referer')) {
501
                $url = $this->request->getHeader('Referer');
502
            }
503
        }
504
505
        if (!$url) {
506
            $url = $this->fallbackReturnURL;
507
        }
508
        if (!$url) {
509
            $url = Director::baseURL();
510
        }
511
512
        // absolute redirection URLs not located on this site may cause phishing
513
        if (Director::is_site_url($url)) {
514
            return $this->redirect($url);
515
        } else {
516
            return false;
517
        }
518
    }
519
}
520