Completed
Push — master ( fb3cc1...4e2d22 )
by Will
13s
created

src/Extensions/CommentsExtension.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\Extensions;
4
5
use SilverStripe\Comments\Admin\CommentsGridField;
6
use SilverStripe\Comments\Admin\CommentsGridFieldConfig;
7
use SilverStripe\Comments\Controllers\CommentingController;
8
use SilverStripe\Comments\Model\Comment;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\Session;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\Manifest\ModuleLoader;
14
use SilverStripe\Dev\Deprecation;
15
use SilverStripe\Forms\CheckboxField;
16
use SilverStripe\Forms\DropdownField;
17
use SilverStripe\Forms\FieldGroup;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\Forms\Tab;
20
use SilverStripe\Forms\TabSet;
21
use SilverStripe\ORM\DataExtension;
22
use SilverStripe\ORM\PaginatedList;
23
use SilverStripe\Security\Member;
24
use SilverStripe\Security\Permission;
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 = array(
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' => array('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 = array(
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' => 'SilverStripe\\Comments\\Model\\Comment.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()->defaults;
116
117
        // Set if comments should be enabled by default
118 View Code Duplication
        if (isset($defaults['ProvideComments'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
        if (isset($defaults['CommentsRequireLogin'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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('CommentsExtension.COMMENTOPTIONS', 'Comments'));
156
157
        // Check if enabled setting should be cms configurable
158
        if ($this->owner->getCommentsOption('enabled_cms')) {
159
            $options->push(new CheckboxField('ProvideComments', _t('Comment.ALLOWCOMMENTS', 'Allow Comments')));
160
        }
161
162
        // Check if we should require users to login to comment
163
        if ($this->owner->getCommentsOption('require_login_cms')) {
164
            $options->push(
165
                new CheckboxField(
166
                    'CommentsRequireLogin',
167
                    _t('Comments.COMMENTSREQUIRELOGIN', 'Require login to comment')
168
                )
169
            );
170
        }
171
172
        if ($options->FieldList()->count()) {
173
            if ($fields->hasTabSet()) {
174
                $fields->addFieldsToTab('Root.Settings', $options);
175
            } else {
176
                $fields->push($options);
177
            }
178
        }
179
180
        // Check if moderation should be enabled via cms configurable
181
        if ($this->owner->getCommentsOption('require_moderation_cms')) {
182
            $moderationField = new DropdownField('ModerationRequired', _t('CommentsExtension.COMMENTMODERATION', 'Comment Moderation'), array(
183
                'None' => _t('CommentsExtension.MODERATIONREQUIRED_NONE', 'No moderation required'),
184
                'Required' => _t('CommentsExtension.MODERATIONREQUIRED_REQUIRED', 'Moderate all comments'),
185
                'NonMembersOnly' => _t(
186
                    'CommentsExtension.MODERATIONREQUIRED_NONMEMBERSONLY',
187
                    'Only moderate non-members'
188
                ),
189
            ));
190
            if ($fields->hasTabSet()) {
191
                $fields->addFieldsToTab('Root.Settings', $moderationField);
192
            } else {
193
                $fields->push($moderationField);
194
            }
195
        }
196
    }
197
198
    /**
199
     * Get comment moderation rules for this parent
200
     *
201
     * None:           No moderation required
202
     * Required:       All comments
203
     * NonMembersOnly: Only anonymous users
204
     *
205
     * @return string
206
     */
207
    public function getModerationRequired()
208
    {
209
        if ($this->owner->getCommentsOption('require_moderation_cms')) {
210
            return $this->owner->getField('ModerationRequired');
211
        } elseif ($this->owner->getCommentsOption('require_moderation')) {
212
            return 'Required';
213
        } elseif ($this->owner->getCommentsOption('require_moderation_nonmembers')) {
214
            return 'NonMembersOnly';
215
        } else {
216
            return 'None';
217
        }
218
    }
219
220
    /**
221
     * Determine if users must be logged in to post comments
222
     *
223
     * @return boolean
224
     */
225
    public function getCommentsRequireLogin()
226
    {
227
        if ($this->owner->getCommentsOption('require_login_cms')) {
228
            return (bool) $this->owner->getField('CommentsRequireLogin');
229
        } else {
230
            return (bool) $this->owner->getCommentsOption('require_login');
231
        }
232
    }
233
234
    /**
235
     * Returns the RelationList of all comments against this object. Can be used as a data source
236
     * for a gridfield with write access.
237
     *
238
     * @return DataList
239
     */
240 View Code Duplication
    public function AllComments()
241
    {
242
        $order = $this->owner->getCommentsOption('order_comments_by');
243
        $comments = Comment::get()
244
            ->filter('ParentID', $this->owner->ID)
245
            ->sort($order);
246
        $this->owner->extend('updateAllComments', $comments);
247
        return $comments;
248
    }
249
250
    /**
251
     * Returns all comments against this object, with with spam and unmoderated items excluded, for use in the frontend
252
     *
253
     * @return DataList
254
     */
255
    public function AllVisibleComments()
256
    {
257
        $list = $this->AllComments();
258
259
        // Filter spam comments for non-administrators if configured
260
        $showSpam = $this->owner->getCommentsOption('frontend_spam') && $this->owner->canModerateComments();
261
        if (!$showSpam) {
262
            $list = $list->filter('IsSpam', 0);
263
        }
264
265
        // Filter un-moderated comments for non-administrators if moderation is enabled
266
        $showUnmoderated = ($this->owner->ModerationRequired === 'None')
267
            || ($this->owner->getCommentsOption('frontend_moderation') && $this->owner->canModerateComments());
268
        if (!$showUnmoderated) {
269
            $list = $list->filter('Moderated', 1);
270
        }
271
272
        $this->owner->extend('updateAllVisibleComments', $list);
273
        return $list;
274
    }
275
276
    /**
277
     * Returns the root level comments, with spam and unmoderated items excluded, for use in the frontend
278
     *
279
     * @return DataList
280
     */
281 View Code Duplication
    public function Comments()
282
    {
283
        $list = $this->AllVisibleComments();
284
285
        // If nesting comments, only show root level
286
        if ($this->owner->getCommentsOption('nested_comments')) {
287
            $list = $list->filter('ParentCommentID', 0);
288
        }
289
290
        $this->owner->extend('updateComments', $list);
291
        return $list;
292
    }
293
294
    /**
295
     * Returns a paged list of the root level comments, with spam and unmoderated items excluded,
296
     * for use in the frontend
297
     *
298
     * @return PaginatedList
299
     */
300 View Code Duplication
    public function PagedComments()
301
    {
302
        $list = $this->Comments();
303
304
        // Add pagination
305
        $list = new PaginatedList($list, Controller::curr()->getRequest());
306
        $list->setPaginationGetVar('commentsstart' . $this->owner->ID);
307
        $list->setPageLength($this->owner->getCommentsOption('comments_per_page'));
308
309
        $this->owner->extend('updatePagedComments', $list);
310
        return $list;
311
    }
312
313
    /**
314
     * Determine if comments are enabled for this instance
315
     *
316
     * @return boolean
317
     */
318
    public function getCommentsEnabled()
319
    {
320
        // Don't display comments form for pseudo-pages (such as the login form)
321
        if (!$this->owner->exists()) {
322
            return false;
323
        }
324
325
        // Determine which flag should be used to determine if this is enabled
326
        if ($this->owner->getCommentsOption('enabled_cms')) {
327
            return $this->owner->ProvideComments;
328
        } else {
329
            return $this->owner->getCommentsOption('enabled');
330
        }
331
    }
332
333
    /**
334
     * Get the HTML ID for the comment holder in the template
335
     *
336
     * @return string
337
     */
338
    public function getCommentHolderID()
339
    {
340
        return $this->owner->getCommentsOption('comments_holder_id');
341
    }
342
343
    /**
344
     * Permission codes required in order to post (or empty if none required)
345
     *
346
     * @return string|array Permission or list of permissions, if required
347
     */
348
    public function getPostingRequiredPermission()
349
    {
350
        return $this->owner->getCommentsOption('required_permission');
351
    }
352
353
    /**
354
     * Determine if a user can post comments on this item
355
     *
356
     * @param Member $member Member to check
357
     *
358
     * @return boolean
359
     */
360
    public function canPostComment($member = null)
361
    {
362
        // Deny if not enabled for this object
363
        if (!$this->owner->CommentsEnabled) {
364
            return false;
365
        }
366
367
        // Check if member is required
368
        $requireLogin = $this->owner->CommentsRequireLogin;
369
        if (!$requireLogin) {
370
            return true;
371
        }
372
373
        // Check member is logged in
374
        $member = $member ?: Member::currentUser();
375
        if (!$member) {
376
            return false;
377
        }
378
379
        // If member required check permissions
380
        $requiredPermission = $this->owner->PostingRequiredPermission;
381
        if ($requiredPermission && !Permission::checkMember($member, $requiredPermission)) {
382
            return false;
383
        }
384
385
        return true;
386
    }
387
388
    /**
389
     * Determine if this member can moderate comments in the CMS
390
     *
391
     * @param Member $member
392
     *
393
     * @return boolean
394
     */
395
    public function canModerateComments($member = null)
396
    {
397
        // Deny if not enabled for this object
398
        if (!$this->owner->CommentsEnabled) {
399
            return false;
400
        }
401
402
        // Fallback to can-edit
403
        return $this->owner->canEdit($member);
404
    }
405
406
    /**
407
     * Gets the RSS link to all comments
408
     *
409
     * @return string
410
     */
411
    public function getCommentRSSLink()
412
    {
413
        return Director::absoluteURL('comments/rss');
414
    }
415
416
    /**
417
     * Get the RSS link to all comments on this page
418
     *
419
     * @return string
420
     */
421
    public function getCommentRSSLinkPage()
422
    {
423
        return Controller::join_links(
424
            $this->getCommentRSSLink(),
425
            str_replace('\\', '-', $this->ownerBaseClass),
426
            $this->owner->ID
427
        );
428
    }
429
430
    /**
431
     * Comments interface for the front end. Includes the CommentAddForm and the composition
432
     * of the comments display.
433
     *
434
     * To customize the html see templates/CommentInterface.ss or extend this function with
435
     * your own extension.
436
     *
437
     * @todo Cleanup the passing of all this configuration based functionality
438
     *
439
     * @see  docs/en/Extending
440
     */
441
    public function CommentsForm()
442
    {
443
        // Check if enabled
444
        $enabled = $this->getCommentsEnabled();
445
        if ($enabled && $this->owner->getCommentsOption('include_js')) {
446
            $adminThirdPartyDir = ModuleLoader::getModule('silverstripe/admin')->getRelativePath() . '/thirdparty';
447
            Requirements::javascript($adminThirdPartyDir . '/jquery/jquery.js');
448
            Requirements::javascript($adminThirdPartyDir . '/jquery-entwine/dist/jquery.entwine-dist.js');
449
            Requirements::javascript($adminThirdPartyDir . '/jquery-form/jquery.form.js');
450
            Requirements::javascript(COMMENTS_THIRDPARTY . '/jquery-validate/jquery.validate.min.js');
451
            Requirements::add_i18n_javascript('comments/javascript/lang');
452
            Requirements::javascript('comments/javascript/CommentsInterface.js');
453
        }
454
455
        $controller = CommentingController::create();
456
        $controller->setOwnerRecord($this->owner);
457
        $controller->setParentClass($this->owner->getClassName());
458
        $controller->setOwnerController(Controller::curr());
459
460
        $session = Controller::curr()->getRequest()->getSession();
461
        $moderatedSubmitted = $session->get('CommentsModerated');
462
        $session->clear('CommentsModerated');
463
464
        $form = ($enabled) ? $controller->CommentsForm() : false;
465
466
        // a little bit all over the show but to ensure a slightly easier upgrade for users
467
        // return back the same variables as previously done in comments
468
        return $this
469
            ->owner
470
            ->customise(array(
471
                'AddCommentForm' => $form,
472
                'ModeratedSubmitted' => $moderatedSubmitted,
473
            ))
474
            ->renderWith('CommentsInterface');
475
    }
476
477
    /**
478
     * Returns whether this extension instance is attached to a {@link SiteTree} object
479
     *
480
     * @return bool
481
     */
482
    public function attachedToSiteTree()
483
    {
484
        $class = $this->ownerBaseClass;
485
486
        return (is_subclass_of($class, SiteTree::class)) || ($class == SiteTree::class);
487
    }
488
489
    /**
490
     * Get the commenting option for this object
491
     *
492
     * This can be overridden in any instance or extension to customise the option available
493
     *
494
     * @param string $key
495
     *
496
     * @return mixed Result if the setting is available, or null otherwise
497
     */
498
    public function getCommentsOption($key)
499
    {
500
        $settings = $this->owner // In case singleton is called on the extension directly
501
            ? $this->owner->config()->comments
502
            : Config::inst()->get(__CLASS__, 'comments');
503
        $value = null;
504
        if (isset($settings[$key])) {
505
            $value = $settings[$key];
506
        }
507
508
        // To allow other extensions to customise this option
509
        if ($this->owner) {
510
            $this->owner->extend('updateCommentsOption', $key, $value);
511
        }
512
        return $value;
513
    }
514
515
    /**
516
     * Add moderation functions to the current fieldlist
517
     *
518
     * @param FieldList $fields
519
     */
520
    protected function updateModerationFields(FieldList $fields)
521
    {
522
        Requirements::css(COMMENTS_DIR . '/css/cms.css');
523
524
        $newComments = $this->owner->AllComments()->filter('Moderated', 0);
525
526
        $newGrid = new CommentsGridField(
527
            'NewComments',
528
            _t('CommentsAdmin.NewComments', 'New'),
529
            $newComments,
530
            CommentsGridFieldConfig::create()
531
        );
532
533
        $approvedComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 0);
534
535
        $approvedGrid = new CommentsGridField(
536
            'ApprovedComments',
537
            _t('CommentsAdmin.Comments', 'Approved'),
538
            $approvedComments,
539
            CommentsGridFieldConfig::create()
540
        );
541
542
        $spamComments = $this->owner->AllComments()->filter('Moderated', 1)->filter('IsSpam', 1);
543
544
        $spamGrid = new CommentsGridField(
545
            'SpamComments',
546
            _t('CommentsAdmin.SpamComments', 'Spam'),
547
            $spamComments,
548
            CommentsGridFieldConfig::create()
549
        );
550
551
        $newCount = '(' . count($newComments) . ')';
552
        $approvedCount = '(' . count($approvedComments) . ')';
553
        $spamCount = '(' . count($spamComments) . ')';
554
555
        if ($fields->hasTabSet()) {
556
            $tabs = new TabSet(
557
                'Comments',
558
                new Tab('CommentsNewCommentsTab', _t('CommentAdmin.NewComments', 'New') . ' ' . $newCount,
559
                    $newGrid
560
                ),
561
                new Tab('CommentsCommentsTab', _t('CommentAdmin.Comments', 'Approved') . ' ' . $approvedCount,
562
                    $approvedGrid
563
                ),
564
                new Tab('CommentsSpamCommentsTab', _t('CommentAdmin.SpamComments', 'Spam') . ' ' . $spamCount,
565
                    $spamGrid
566
                )
567
            );
568
            $fields->addFieldToTab('Root', $tabs);
569
        } else {
570
            $fields->push($newGrid);
571
            $fields->push($approvedGrid);
572
            $fields->push($spamGrid);
573
        }
574
    }
575
576
    public function updateCMSFields(FieldList $fields)
577
    {
578
        // Disable moderation if not permitted
579
        if ($this->owner->canModerateComments()) {
580
            $this->updateModerationFields($fields);
581
        }
582
583
        // If this isn't a page we should merge the settings into the CMS fields
584
        if (!$this->attachedToSiteTree()) {
585
            $this->updateSettingsFields($fields);
586
        }
587
    }
588
}
589