Passed
Push — master ( 161f5f...780ea2 )
by Robbie
02:43 queued 10s
created

src/Extensions/CommentsExtension.php (1 issue)

1
<?php
2
3
namespace SilverStripe\Comments\Extensions;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Comments\Admin\CommentsGridField;
7
use SilverStripe\Comments\Admin\CommentsGridFieldConfig;
8
use SilverStripe\Comments\Controllers\CommentingController;
9
use SilverStripe\Comments\Model\Comment;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Forms\CheckboxField;
14
use SilverStripe\Forms\DropdownField;
15
use SilverStripe\Forms\FieldGroup;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\Tab;
18
use SilverStripe\Forms\TabSet;
19
use SilverStripe\ORM\DataExtension;
20
use SilverStripe\ORM\DataList;
21
use SilverStripe\ORM\PaginatedList;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Security\Permission;
24
use SilverStripe\Security\Security;
25
use SilverStripe\View\Requirements;
26
27
/**
28
 * Extension to {@link DataObject} to enable tracking comments.
29
 *
30
 * @package comments
31
 */
32
class CommentsExtension extends DataExtension
33
{
34
    /**
35
     * Default configuration values
36
     *
37
     * enabled:                     Allows commenting to be disabled even if the extension is present
38
     * enabled_cms:                 Allows commenting to be enabled or disabled via the CMS
39
     * require_login:               Boolean, whether a user needs to login (required for required_permission)
40
     * require_login_cms:           Allows require_login to be set via the CMS
41
     * required_permission:         Permission (or array of permissions) required to comment
42
     * include_js:                  Enhance operation by ajax behaviour on moderation links (required for use_preview)
43
     * use_gravatar:                Set to true to show gravatar icons
44
     * gravatar_default:            Theme for 'not found' gravatar {@see http://gravatar.com/site/implement/images}
45
     * gravatar_rating:             Gravatar rating (same as the standard default)
46
     * show_comments_when_disabled: Show older comments when commenting has been disabled.
47
     * order_comments_by:           Default sort order.
48
     * order_replies_by:            Sort order for replies.
49
     * comments_holder_id:          ID for the comments holder
50
     * comment_permalink_prefix:    ID prefix for each comment
51
     * require_moderation:          Require moderation for all comments
52
     * require_moderation_cms:      Ignore other comment moderation config settings and set via CMS
53
     * frontend_moderation:         Display unmoderated comments in the frontend, if the user can moderate them.
54
     * frontend_spam:               Display spam comments in the frontend, if the user can moderate them.
55
     * html_allowed:                Allow for sanitized HTML in comments
56
     * use_preview:                 Preview formatted comment (when allowing HTML)
57
     * nested_comments:             Enable nested comments
58
     * nested_depth:                Max depth of nested comments in levels (where root is 1 depth) 0 means no limit.
59
     *
60
     * @var array
61
     *
62
     * @config
63
     */
64
    private static $comments = [
65
        'enabled' => true,
66
        'enabled_cms' => false,
67
        'require_login' => false,
68
        'require_login_cms' => false,
69
        'required_permission' => false,
70
        'include_js' => true,
71
        'use_gravatar' => false,
72
        'gravatar_size' => 80,
73
        'gravatar_default' => 'identicon',
74
        'gravatar_rating' => 'g',
75
        'show_comments_when_disabled' => false,
76
        'order_comments_by' => '"Created" DESC',
77
        'order_replies_by' => false,
78
        'comments_per_page' => 10,
79
        'comments_holder_id' => 'comments-holder',
80
        'comment_permalink_prefix' => 'comment-',
81
        'require_moderation' => false,
82
        'require_moderation_nonmembers' => false,
83
        'require_moderation_cms' => false,
84
        'frontend_moderation' => false,
85
        'frontend_spam' => false,
86
        'html_allowed' => false,
87
        'html_allowed_elements' => ['a', 'img', 'i', 'b'],
88
        'use_preview' => false,
89
        'nested_comments' => false,
90
        'nested_depth' => 2,
91
    ];
92
93
    /**
94
     * @var array
95
     */
96
    private static $db = [
97
        'ProvideComments' => 'Boolean',
98
        'ModerationRequired' => 'Enum(\'None,Required,NonMembersOnly\',\'None\')',
99
        'CommentsRequireLogin' => 'Boolean',
100
    ];
101
102
    /**
103
     * {@inheritDoc}
104
     */
105
    private static $has_many = [
106
        'Commments' => Comment::class . '.Parent'
107
    ];
108
109
    /**
110
     * CMS configurable options should default to the config values, but respect
111
     * default values specified by the object
112
     */
113
    public function populateDefaults()
114
    {
115
        $defaults = $this->owner->config()->get('defaults');
116
117
        // Set if comments should be enabled by default
118
        if (isset($defaults['ProvideComments'])) {
119
            $this->owner->ProvideComments = $defaults['ProvideComments'];
120
        } else {
121
            $this->owner->ProvideComments = $this->owner->getCommentsOption('enabled') ? 1 : 0;
122
        }
123
124
        // If moderation options should be configurable via the CMS then
125
        if (isset($defaults['ModerationRequired'])) {
126
            $this->owner->ModerationRequired = $defaults['ModerationRequired'];
127
        } elseif ($this->owner->getCommentsOption('require_moderation')) {
128
            $this->owner->ModerationRequired = 'Required';
129
        } elseif ($this->owner->getCommentsOption('require_moderation_nonmembers')) {
130
            $this->owner->ModerationRequired = 'NonMembersOnly';
131
        } else {
132
            $this->owner->ModerationRequired = 'None';
133
        }
134
135
        // Set login required
136
        if (isset($defaults['CommentsRequireLogin'])) {
137
            $this->owner->CommentsRequireLogin = $defaults['CommentsRequireLogin'];
138
        } else {
139
            $this->owner->CommentsRequireLogin = $this->owner->getCommentsOption('require_login') ? 1 : 0;
140
        }
141
    }
142
143
144
    /**
145
     * If this extension is applied to a {@link SiteTree} record then
146
     * append a Provide Comments checkbox to allow authors to trigger
147
     * whether or not to display comments
148
     *
149
     * @todo Allow customization of other {@link Commenting} configuration
150
     *
151
     * @param FieldList $fields
152
     */
153
    public function updateSettingsFields(FieldList $fields)
154
    {
155
        $options = FieldGroup::create()->setTitle(_t(__CLASS__ . '.COMMENTOPTIONS', 'Comments'));
156
157
        // Check if enabled setting should be cms configurable
158
        if ($this->owner->getCommentsOption('enabled_cms')) {
159
            $options->push(CheckboxField::create('ProvideComments', _t(
160
                'SilverStripe\\Comments\\Model\\Comment.ALLOWCOMMENTS',
161
                'Allow comments'
162
            )));
163
        }
164
165
        // Check if we should require users to login to comment
166
        if ($this->owner->getCommentsOption('require_login_cms')) {
167
            $options->push(
168
                CheckboxField::create(
169
                    'CommentsRequireLogin',
170
                    _t('Comments.COMMENTSREQUIRELOGIN', 'Require login to comment')
171
                )
172
            );
173
        }
174
175
        if ($options->FieldList()->count()) {
176
            if ($fields->hasTabSet()) {
177
                $fields->addFieldsToTab('Root.Settings', $options);
178
            } else {
179
                $fields->push($options);
180
            }
181
        }
182
183
        // Check if moderation should be enabled via cms configurable
184
        if ($this->owner->getCommentsOption('require_moderation_cms')) {
185
            $moderationField = DropdownField::create(
186
                'ModerationRequired',
187
                _t(
188
                    __CLASS__ . '.COMMENTMODERATION',
189
                    'Comment Moderation'
190
                ),
191
                [
192
                    'None' => _t(__CLASS__ . '.MODERATIONREQUIRED_NONE', 'No moderation required'),
193
                    'Required' => _t(__CLASS__ . '.MODERATIONREQUIRED_REQUIRED', 'Moderate all comments'),
194
                    'NonMembersOnly' => _t(
195
                        __CLASS__ . '.MODERATIONREQUIRED_NONMEMBERSONLY',
196
                        'Only moderate non-members'
197
                    ),
198
                ]
199
            );
200
            if ($fields->hasTabSet()) {
201
                $fields->addFieldToTab('Root.Settings', $moderationField);
202
            } else {
203
                $fields->push($moderationField);
204
            }
205
        }
206
    }
207
208
    /**
209
     * Get comment moderation rules for this parent
210
     *
211
     * None:           No moderation required
212
     * Required:       All comments
213
     * NonMembersOnly: Only anonymous users
214
     *
215
     * @return string
216
     */
217
    public function getModerationRequired()
218
    {
219
        if ($this->owner->getCommentsOption('require_moderation_cms')) {
220
            return $this->owner->getField('ModerationRequired');
221
        }
222
223
        if ($this->owner->getCommentsOption('require_moderation')) {
224
            return 'Required';
225
        }
226
227
        if ($this->owner->getCommentsOption('require_moderation_nonmembers')) {
228
            return 'NonMembersOnly';
229
        }
230
231
        return 'None';
232
    }
233
234
    /**
235
     * Determine if users must be logged in to post comments
236
     *
237
     * @return boolean
238
     */
239
    public function getCommentsRequireLogin()
240
    {
241
        if ($this->owner->getCommentsOption('require_login_cms')) {
242
            return (bool) $this->owner->getField('CommentsRequireLogin');
243
        }
244
        return (bool) $this->owner->getCommentsOption('require_login');
245
    }
246
247
    /**
248
     * Returns the RelationList of all comments against this object. Can be used as a data source
249
     * for a gridfield with write access.
250
     *
251
     * @return DataList
252
     */
253
    public function AllComments()
254
    {
255
        $order = $this->owner->getCommentsOption('order_comments_by');
256
        $comments = Comment::get()
257
            ->filter('ParentID', $this->owner->ID)
258
            ->sort($order);
259
        $this->owner->extend('updateAllComments', $comments);
260
        return $comments;
261
    }
262
263
    /**
264
     * Returns all comments against this object, with with spam and unmoderated items excluded, for use in the frontend
265
     *
266
     * @return DataList
267
     */
268
    public function AllVisibleComments()
269
    {
270
        $list = $this->AllComments();
271
272
        // Filter spam comments for non-administrators if configured
273
        $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments();
274
275
        if (!$showSpam) {
276
            $list = $list->filter('IsSpam', 0);
277
        }
278
279
        // Filter un-moderated comments for non-administrators if moderation is enabled
280
        $showUnmoderated = ($this->owner->ModerationRequired === 'None')
281
            || ($this->owner->getCommentsOption('frontend_moderation') && $this->owner->canModerateComments());
282
        if (!$showUnmoderated) {
283
            $list = $list->filter('Moderated', 1);
284
        }
285
286
        $this->owner->extend('updateAllVisibleComments', $list);
287
        return $list;
288
    }
289
290
    /**
291
     * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend
292
     *
293
     * @return DataList
294
     */
295
    public function Comments()
296
    {
297
        $list = $this->AllVisibleComments();
298
299
        // If nesting comments, only show root level
300
        if ($this->owner->getCommentsOption('nested_comments')) {
301
            $list = $list->filter('ParentCommentID', 0);
302
        }
303
304
        $this->owner->extend('updateComments', $list);
305
        return $list;
306
    }
307
308
    /**
309
     * Returns a paged list of the root level comments, with spam and unmoderated items excluded,
310
     * for use in the frontend
311
     *
312
     * @return PaginatedList
313
     */
314
    public function PagedComments()
315
    {
316
        $list = $this->Comments();
317
318
        // Add pagination
319
        $list = PaginatedList::create($list, Controller::curr()->getRequest());
320
        $list->setPaginationGetVar('commentsstart' . $this->owner->ID);
321
        $list->setPageLength($this->owner->getCommentsOption('comments_per_page'));
322
323
        $this->owner->extend('updatePagedComments', $list);
324
        return $list;
325
    }
326
327
    /**
328
     * Determine if comments are enabled for this instance
329
     *
330
     * @return boolean
331
     */
332
    public function getCommentsEnabled()
333
    {
334
        // Don't display comments form for pseudo-pages (such as the login form)
335
        if (!$this->owner->exists()) {
336
            return false;
337
        }
338
339
        // Determine which flag should be used to determine if this is enabled
340
        if ($this->owner->getCommentsOption('enabled_cms')) {
341
            return (bool) $this->owner->ProvideComments;
342
        }
343
344
        return (bool) $this->owner->getCommentsOption('enabled');
345
    }
346
347
    /**
348
     * Get the HTML ID for the comment holder in the template
349
     *
350
     * @return string
351
     */
352
    public function getCommentHolderID()
353
    {
354
        return $this->owner->getCommentsOption('comments_holder_id');
355
    }
356
357
    /**
358
     * Permission codes required in order to post (or empty if none required)
359
     *
360
     * @return string|array Permission or list of permissions, if required
361
     */
362
    public function getPostingRequiredPermission()
363
    {
364
        return $this->owner->getCommentsOption('required_permission');
365
    }
366
367
    /**
368
     * Determine if a user can post comments on this item
369
     *
370
     * @param Member $member Member to check
371
     *
372
     * @return boolean
373
     */
374
    public function canPostComment($member = null)
375
    {
376
        // Deny if not enabled for this object
377
        if (!$this->owner->CommentsEnabled) {
378
            return false;
379
        }
380
381
        if (!$this->owner->canView($member)) {
382
            // deny if current user cannot view the underlying record.
383
            return false;
384
        }
385
386
        // Check if member is required
387
        $requireLogin = $this->owner->CommentsRequireLogin;
388
        if (!$requireLogin) {
389
            return true;
390
        }
391
392
        // Check member is logged in
393
        $member = $member ?: Security::getCurrentUser();
394
        if (!$member) {
395
            return false;
396
        }
397
398
        // If member required check permissions
399
        $requiredPermission = $this->owner->PostingRequiredPermission;
400
        if ($requiredPermission && !Permission::checkMember($member, $requiredPermission)) {
401
            return false;
402
        }
403
404
        return true;
405
    }
406
407
    /**
408
     * Determine if this member can moderate comments in the CMS
409
     *
410
     * @param Member $member
411
     *
412
     * @return boolean
413
     */
414
    public function canModerateComments($member = null)
415
    {
416
        // Deny if not enabled for this object
417
        if (!$this->owner->CommentsEnabled) {
418
            return false;
419
        }
420
421
        // Fallback to can-edit
422
        return $this->owner->canEdit($member);
423
    }
424
425
    /**
426
     * Gets the RSS link to all comments
427
     *
428
     * @return string
429
     */
430
    public function getCommentRSSLink()
431
    {
432
        return Director::absoluteURL('comments/rss');
433
    }
434
435
    /**
436
     * Get the RSS link to all comments on this page
437
     *
438
     * @return string
439
     */
440
    public function getCommentRSSLinkPage()
441
    {
442
        return Controller::join_links(
443
            $this->getCommentRSSLink(),
444
            str_replace('\\', '-', get_class($this->owner)),
445
            $this->owner->ID
446
        );
447
    }
448
449
    /**
450
     * Comments interface for the front end. Includes the CommentAddForm and the composition
451
     * of the comments display.
452
     *
453
     * To customize the html see templates/CommentInterface.ss or extend this function with
454
     * your own extension.
455
     *
456
     * @todo Cleanup the passing of all this configuration based functionality
457
     *
458
     * @see  docs/en/Extending
459
     */
460
    public function CommentsForm()
461
    {
462
        // Check if enabled
463
        $enabled = $this->getCommentsEnabled();
464
        if ($enabled && $this->owner->getCommentsOption('include_js')) {
465
            Requirements::javascript('//code.jquery.com/jquery-3.3.1.min.js');
466
            Requirements::javascript('silverstripe/comments:thirdparty/jquery-validate/jquery.validate.min.js');
467
            Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js');
468
            Requirements::add_i18n_javascript('silverstripe/comments:javascript/lang');
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Requir...::add_i18n_javascript() has been deprecated. ( Ignorable by Annotation )

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

468
            /** @scrutinizer ignore-deprecated */ Requirements::add_i18n_javascript('silverstripe/comments:javascript/lang');
Loading history...
469
            Requirements::javascript('silverstripe/comments:javascript/CommentsInterface.js');
470
        }
471
472
        $controller = CommentingController::create();
473
        $controller->setOwnerRecord($this->owner);
474
        $controller->setParentClass($this->owner->getClassName());
475
        $controller->setOwnerController(Controller::curr());
476
477
        $session = Controller::curr()->getRequest()->getSession();
478
        $moderatedSubmitted = $session->get('CommentsModerated');
479
        $session->clear('CommentsModerated');
480
481
        $form = ($enabled) ? $controller->CommentsForm() : false;
482
483
        // a little bit all over the show but to ensure a slightly easier upgrade for users
484
        // return back the same variables as previously done in comments
485
        return $this
486
            ->owner
487
            ->customise([
488
                'AddCommentForm' => $form,
489
                'ModeratedSubmitted' => $moderatedSubmitted,
490
            ])
491
            ->renderWith('CommentsInterface');
492
    }
493
494
    /**
495
     * Returns whether this extension instance is attached to a {@link SiteTree} object
496
     *
497
     * @return bool
498
     */
499
    public function attachedToSiteTree()
500
    {
501
        $class = $this->owner->baseClass();
502
503
        return (is_subclass_of($class, SiteTree::class)) || ($class == SiteTree::class);
504
    }
505
506
    /**
507
     * Get the commenting option for this object.
508
     *
509
     * This can be overridden in any instance or extension to customise the
510
     * option available.
511
     *
512
     * @param string $key
513
     *
514
     * @return mixed Result if the setting is available, or null otherwise
515
     */
516
    public function getCommentsOption($key)
517
    {
518
        $settings = $this->getCommentsOptions();
519
        $value = null;
520
521
        if (isset($settings[$key])) {
522
            $value = $settings[$key];
523
        }
524
525
        // To allow other extensions to customise this option
526
        if ($this->owner) {
527
            $this->owner->extend('updateCommentsOption', $key, $value);
528
        }
529
530
        return $value;
531
    }
532
533
    /**
534
     * @return array
535
     */
536
    public function getCommentsOptions()
537
    {
538
        if ($this->owner) {
539
            $settings = $this->owner->config()->get('comments');
540
        } else {
541
            $settings = Config::inst()->get(__CLASS__, 'comments');
542
        }
543
544
        return $settings;
545
    }
546
547
    /**
548
     * Add moderation functions to the current fieldlist
549
     *
550
     * @param FieldList $fields
551
     */
552
    protected function updateModerationFields(FieldList $fields)
553
    {
554
        Requirements::css('silverstripe/comments:css/cms.css');
555
556
        $newComments = $this->owner->AllComments()->filter('Moderated', 0);
557
558
        $newGrid = CommentsGridField::create(
559
            'NewComments',
560
            _t('CommentsAdmin.NewComments', 'New'),
561
            $newComments,
562
            CommentsGridFieldConfig::create()
563
        );
564
565
        $approvedComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 0);
566
567
        $approvedGrid = new CommentsGridField(
568
            'ApprovedComments',
569
            _t('CommentsAdmin.Comments', 'Approved'),
570
            $approvedComments,
571
            CommentsGridFieldConfig::create()
572
        );
573
574
        $spamComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 1);
575
576
        $spamGrid = CommentsGridField::create(
577
            'SpamComments',
578
            _t('CommentsAdmin.SpamComments', 'Spam'),
579
            $spamComments,
580
            CommentsGridFieldConfig::create()
581
        );
582
583
        $newCount = '(' . count($newComments) . ')';
584
        $approvedCount = '(' . count($approvedComments) . ')';
585
        $spamCount = '(' . count($spamComments) . ')';
586
587
        if ($fields->hasTabSet()) {
588
            $tabs = TabSet::create(
589
                'Comments',
590
                Tab::create(
591
                    'CommentsNewCommentsTab',
592
                    _t('SilverStripe\\Comments\\Admin\\CommentAdmin.NewComments', 'New') . ' ' . $newCount,
593
                    $newGrid
594
                ),
595
                Tab::create(
596
                    'CommentsCommentsTab',
597
                    _t('SilverStripe\\Comments\\Admin\\CommentAdmin.Comments', 'Approved') . ' ' . $approvedCount,
598
                    $approvedGrid
599
                ),
600
                Tab::create(
601
                    'CommentsSpamCommentsTab',
602
                    _t('SilverStripe\\Comments\\Admin\\CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount,
603
                    $spamGrid
604
                )
605
            );
606
            $tabs->setTitle(_t(__CLASS__ . '.COMMENTSTABSET', 'Comments'));
607
608
            $fields->addFieldToTab('Root', $tabs);
609
        } else {
610
            $fields->push($newGrid);
611
            $fields->push($approvedGrid);
612
            $fields->push($spamGrid);
613
        }
614
    }
615
616
    public function updateCMSFields(FieldList $fields)
617
    {
618
        // Disable moderation if not permitted
619
        if ($this->owner->canModerateComments()) {
620
            $this->updateModerationFields($fields);
621
        }
622
623
        // If this isn't a page we should merge the settings into the CMS fields
624
        if (!$this->attachedToSiteTree()) {
625
            $this->updateSettingsFields($fields);
626
        }
627
    }
628
}
629