CommentingController   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 157
c 2
b 0
f 0
dl 0
loc 506
rs 3.36
wmc 63

23 Methods

Rating   Name   Duplication   Size   Complexity  
A encodeClassName() 0 3 1
A getParentClass() 0 3 1
A rss() 0 3 1
A spam() 0 15 4
A Link() 0 3 1
A getOwnerRecord() 0 3 1
A setParentClass() 0 3 1
A ham() 0 15 4
A delete() 0 18 5
A setOwnerController() 0 3 1
A setOwnerRecord() 0 3 1
A getOwnerController() 0 3 1
A decodeClassName() 0 3 1
A approve() 0 14 4
A getComment() 0 14 4
B redirectBack() 0 33 9
A CommentsForm() 0 8 1
A renderChangedCommentState() 0 22 4
A getOption() 0 15 3
B getFeed() 0 45 8
A getOptions() 0 14 3
A ReplyForm() 0 19 1
A reply() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like CommentingController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CommentingController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Comments\Controllers;
4
5
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by Robbie Averill
The type SilverStripe\CMS\Model\SiteTree was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 = [
0 ignored issues
show
introduced by Robbie Averill
The private property $allowed_actions is not used, and could be removed.
Loading history...
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 = [
0 ignored issues
show
introduced by Robbie Averill
The private property $url_handlers is not used, and could be removed.
Loading history...
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 = [
0 ignored issues
show
introduced by Robbie Averill
The private property $required_fields is not used, and could be removed.
Loading history...
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
introduced by Damian Mooyman
$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);
0 ignored issues
show
Bug introduced by Robbie Averill
$parentID of type string is incompatible with the type boolean|integer expected by parameter $idOrCache of SilverStripe\ORM\DataObject::get_by_id(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

472
            $comment = DataObject::get_by_id(Comment::class, /** @scrutinizer ignore-type */ $parentID, true);
Loading history...
473
            if ($comment) {
0 ignored issues
show
introduced by Damian Mooyman
$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);
0 ignored issues
show
Deprecated Code introduced by Damian Mooyman
The function SilverStripe\Control\HTTP::set_cache_age() has been deprecated: 4.2.0:5.0.0 Use HTTPCacheControlMiddleware::singleton()->setMaxAge($age) instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

502
        /** @scrutinizer ignore-deprecated */ HTTP::set_cache_age(0);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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