CommentComponent::beforeFilter()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 9.4285
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Comment\Controller\Component;
13
14
use Cake\Controller\Component;
15
use Cake\Controller\ComponentRegistry;
16
use Cake\Datasource\EntityInterface;
17
use Cake\Event\Event;
18
use Cake\ORM\TableRegistry;
19
use Cake\Routing\Router;
20
use Cake\Validation\Validator;
21
use Captcha\CaptchaManager;
22
use CMS\Core\Plugin;
23
use Comment\Model\Entity\Comment;
24
use Field\Utility\TextToolbox;
25
use User\Model\Entity\User;
26
27
/**
28
 * Manages entity's comments.
29
 *
30
 * You must use this Component in combination with `Commentable` behavior and
31
 * `CommentHelper`. CommentHelper is automatically attached to your controller
32
 * when this component is attached.
33
 *
34
 * When this component is attached you can render entity's comments using the
35
 * CommentHelper:
36
 *
37
 * ```php
38
 * // in any view:
39
 * $this->Comment->config('visibility', 1);
40
 * $this->Comment->render($entity);
41
 *
42
 * // in any controller
43
 * $this->Comment->config('visibility', 1);
44
 * ```
45
 *
46
 * You can set `visibility` using this component at controller side, or using
47
 * CommentHelper as example above, accepted values are:
48
 *
49
 * - 0: Closed; can't post new comments nor read existing ones. (by default)
50
 * - 1: Read & Write; can post new comments and read existing ones.
51
 * - 2: Read Only; can't post new comments but can read existing ones.
52
 */
53
class CommentComponent extends Component
54
{
55
56
    /**
57
     * The controller this component is attached to.
58
     *
59
     * @var \Cake\Controller\Controller
60
     */
61
    protected $_controller;
62
63
    /**
64
     * Default configuration.
65
     *
66
     * - redirectOnSuccess: Set to true to redirect to `referer` page on success.
67
     *   Set to false for no redirection, or set to an array|string compatible with
68
     *   `Controller::redirect()` method.
69
     *
70
     * - successMessage: Custom success alert-message. Or a callable method which
71
     *   must return a customized message.
72
     *
73
     * - errorMessage: Custom error alert-message. Or a callable method which must
74
     *   return a customized message.
75
     *
76
     * - arrayContext: Information for the ArrayContext provider used by FormHelper
77
     *   when rendering comments form.
78
     *
79
     * - validator: A custom validator object, if not provided it automatically
80
     *   creates one for you using the information below:
81
     *
82
     * - settings: Array of additional settings parameters, will be merged with
83
     *   those coming from Comment Plugin's configuration panel (at backend).
84
     *
85
     * When defining `successMessage` or `errorMessage` as callable functions you
86
     * should expect two arguments. A comment entity as first argument and the
87
     * controller instance this component is attached to as second argument:
88
     *
89
     * ```php
90
     * $options['successMessage'] = function ($comment, $controller) {
91
     *     return 'My customized success message';
92
     * }
93
     *
94
     * $options['errorMessage'] = function ($comment, $controller) {
95
     *     return 'My customized error message';
96
     * }
97
     * ```
98
     *
99
     * @var array
100
     */
101
    protected $_defaultConfig = [
102
        'redirectOnSuccess' => true,
103
        'successMessage' => 'Comment saved!',
104
        'errorMessage' => 'Your comment could not be saved, please check your information.',
105
        'arrayContext' => [
106
            'schema' => [
107
                'comment' => [
108
                    'parent_id' => ['type' => 'integer'],
109
                    'author_name' => ['type' => 'string'],
110
                    'author_email' => ['type' => 'string'],
111
                    'author_web' => ['type' => 'string'],
112
                    'subject' => ['type' => 'string'],
113
                    'body' => ['type' => 'string'],
114
                ]
115
            ],
116
            'defaults' => [
117
                'comment' => [
118
                    'parent_id' => null,
119
                    'author_name' => null,
120
                    'author_email' => null,
121
                    'author_web' => null,
122
                    'subject' => null,
123
                    'body' => null,
124
                ]
125
            ],
126
            'errors' => [
127
                'comment' => []
128
            ]
129
        ],
130
        'validator' => false,
131
        'settings' => [], // auto-filled with Comment plugin's settings
132
    ];
133
134
    /**
135
     * Constructor.
136
     *
137
     * @param \Cake\Controller\ComponentRegistry $collection A ComponentRegistry
138
     *  for this component
139
     * @param array $config Array of configuration options to merge with defaults
140
     */
141
    public function __construct(ComponentRegistry $collection, array $config = [])
142
    {
143
        $this->_defaultConfig['settings'] = plugin('Comment')->settings();
144
        $this->_defaultConfig['settings']['visibility'] = 0;
145
        $this->_defaultConfig['errorMessage'] = __d('comment', 'Your comment could not be saved, please check your information.');
146
        $this->_defaultConfig['successMessage'] = function () {
147
            if ($this->config('settings.auto_approve') ||
148
                $this->_controller->request->is('userAdmin')
149
            ) {
150
                return __d('comment', 'Comment saved!');
151
            }
152
153
            return __d('comment', 'Your comment is awaiting moderation.');
154
        };
155
        parent::__construct($collection, $config);
156
        $this->_controller = $this->_registry->getController();
157
        $this->_loadSettings();
158
    }
159
160
    /**
161
     * Called before the controller's beforeFilter method.
162
     *
163
     * @param Event $event The event that was triggered
164
     * @return void
165
     */
166
    public function beforeFilter(Event $event)
167
    {
168
        $this->_controller->set('__commentComponentLoaded__', true);
169
        $this->_controller->set('_commentFormContext', $this->config('arrayContext'));
170
    }
171
172
    /**
173
     * Called after the controller executes the requested action.
174
     *
175
     * @param Event $event The event that was triggered
176
     * @return void
177
     */
178
    public function beforeRender(Event $event)
179
    {
180
        $this->_controller->helpers['Comment.Comment'] = $this->config('settings');
181
    }
182
183
    /**
184
     * Reads/writes settings for this component or for CommentHelper class.
185
     *
186
     * @param string|array|null $key The key to get/set, or a complete array of configs.
187
     * @param mixed|null $value The value to set.
188
     * @param bool $merge Whether to merge or overwrite existing config, defaults to true.
189
     * @return mixed Config value being read, or the object itself on write operations.
190
     * @throws \Cake\Core\Exception\Exception When trying to set a key that is invalid.
191
     */
192
    public function config($key = null, $value = null, $merge = true)
193
    {
194
        if ($key !== null && in_array($key, array_keys($this->_defaultConfig['settings']))) {
195
            $key = "settings.{$key}";
196
        }
197
198
        if (!$this->_configInitialized) {
199
            $this->_config = $this->_defaultConfig;
200
            $this->_configInitialized = true;
201
        }
202
203
        if (is_array($key) || func_num_args() >= 2) {
204
            $this->_configWrite($key, $value, $merge);
205
206
            return $this;
207
        }
208
209
        return $this->_configRead($key);
210
    }
211
212
    /**
213
     * Adds a new comment for the given entity.
214
     *
215
     * @param \Cake\Datasource\EntityInterface $entity The entity where to attach new comment
216
     * @return bool True on success, false otherwise
217
     */
218
    public function post(EntityInterface $entity)
219
    {
220
        $pk = (string)TableRegistry::get($entity->source())->primaryKey();
221
        if (empty($this->_controller->request->data['comment']) ||
222
            $this->config('settings.visibility') !== 1 ||
223
            !$entity->has($pk)
224
        ) {
225
            return false;
226
        }
227
228
        $this->_controller->loadModel('Comment.Comments');
229
        $data = $this->_getRequestData($entity);
230
        $this->_controller->Comments->validator('commentValidation', $this->_createValidator());
231
        $comment = $this->_controller->Comments->newEntity($data, ['validate' => 'commentValidation']);
232
        $errors = $comment->errors();
233
        $errors = !empty($errors);
234
235
        if (!$errors) {
236
            $persist = true;
237
            $saved = true;
238
            $this->_controller->Comments->addBehavior('Tree', [
239
                'scope' => [
240
                    'entity_id' => $data['entity_id'],
241
                    'table_alias' => $data['table_alias'],
242
                ]
243
            ]);
244
245
            if ($this->config('settings.use_akismet')) {
246
                $newStatus = $this->_akismetStatus($data);
247
                $comment->set('status', $newStatus);
248
249
                if ($newStatus == 'spam' &&
250
                    $this->config('settings.akismet_action') != 'mark'
251
                ) {
252
                    $persist = false;
253
                }
254
            }
255
256
            if ($persist) {
257
                $saved = $this->_controller->Comments->save($comment);
258
            }
259
260
            if ($saved) {
261
                $this->_afterSave($comment);
262
263
                return true; // all OK
264
            } else {
265
                $errors = true;
266
            }
267
        }
268
269
        if ($errors) {
270
            $this->_setErrors($comment);
271
            $errorMessage = $this->config('errorMessage');
272
            if (is_callable($errorMessage)) {
273
                $errorMessage = $errorMessage($comment, $this->_controller);
274
            }
275
            $this->_controller->Flash->danger($errorMessage, ['key' => 'commentsForm']);
276
        }
277
278
        return false;
279
    }
280
281
    /**
282
     * Calculates comment's status using akismet.
283
     *
284
     * @param array $data Comment's data to be validated by Akismet
285
     * @return string Filtered comment's status
286
     */
287
    protected function _akismetStatus($data)
288
    {
289
        require_once Plugin::classPath('Comment') . 'Lib/Akismet.php';
290
291
        try {
292
            $akismet = new \Akismet(Router::url('/'), $this->config('settings.akismet_key'));
293
294
            if (!empty($data['author_name'])) {
295
                $akismet->setCommentAuthor($data['author_name']);
296
            }
297
298
            if (!empty($data['author_email'])) {
299
                $akismet->setCommentAuthorEmail($data['author_email']);
300
            }
301
302
            if (!empty($data['author_web'])) {
303
                $akismet->setCommentAuthorURL($data['author_web']);
304
            }
305
306
            if (!empty($data['body'])) {
307
                $akismet->setCommentContent($data['body']);
308
            }
309
310
            if ($akismet->isCommentSpam()) {
311
                return 'spam';
312
            }
313
        } catch (\Exception $ex) {
314
            return 'pending';
315
        }
316
317
        return $data['status'];
318
    }
319
320
    /**
321
     * Logic triggered after comment was successfully saved.
322
     *
323
     * @param \Cake\Datasource\EntityInterface $comment Comment that was just saved
324
     * @return void
325
     */
326
    protected function _afterSave(EntityInterface $comment)
327
    {
328
        $successMessage = $this->config('successMessage');
329
        if (is_callable($successMessage)) {
330
            $successMessage = $successMessage($comment, $this->_controller);
331
        }
332
333
        $this->_controller->Flash->success($successMessage, ['key' => 'commentsForm']);
334
        if ($this->config('redirectOnSuccess')) {
335
            $redirectTo = $this->config('redirectOnSuccess') === true ? $this->_controller->referer() : $this->config('redirectOnSuccess');
336
            $this->_controller->redirect($redirectTo);
337
        }
338
    }
339
340
    /**
341
     * Extract data from request and prepares for inserting a new comment for
342
     * the given entity.
343
     *
344
     * @param \Cake\Datasource\EntityInterface $entity Entity used to guess table name
345
     * @return array
346
     */
347
    protected function _getRequestData(EntityInterface $entity)
348
    {
349
        $pk = (string)TableRegistry::get($entity->source())->primaryKey();
350
        $data = $this->_controller->request->data('comment');
351
        $return = [
352
            'parent_id' => null,
353
            'subject' => '',
354
            'body' => '',
355
            'status' => 'pending',
356
            'author_name' => null,
357
            'author_email' => null,
358
            'author_web' => null,
359
            'author_ip' => $this->_controller->request->clientIp(),
360
            'table_alias' => $this->_getTableAlias($entity),
361
            'entity_id' => $entity->get($pk),
362
        ];
363
364
        if (!empty($this->_controller->request->data['comment'])) {
365
            $data = $this->_controller->request->data['comment'];
366
        }
367
368
        if ($this->_controller->request->is('userLoggedIn')) {
369
            $return['user_id'] = user()->id;
370
            $return['author_name'] = null;
371
            $return['author_email'] = null;
372
            $return['author_web'] = null;
373
        } else {
374
            $return['author_name'] = !empty($data['author_name']) ? h($data['author_name']) : null;
375
            $return['author_email'] = !empty($data['author_email']) ? h($data['author_email']) : null;
376
            $return['author_web'] = !empty($data['author_web']) ? h($data['author_web']) : null;
377
        }
378
379
        if (!empty($data['subject'])) {
380
            $return['subject'] = h($data['subject']);
381
        }
382
383
        if (!empty($data['parent_id'])) {
384
            // this is validated at Model side
385
            $return['parent_id'] = $data['parent_id'];
386
        }
387
388
        if (!empty($data['body'])) {
389
            $return['body'] = TextToolbox::process($data['body'], $this->config('settings.text_processing'));
390
        }
391
392
        if ($this->config('settings.auto_approve') ||
393
            $this->_controller->request->is('userAdmin')
394
        ) {
395
            $return['status'] = 'approved';
396
        }
397
398
        return $return;
399
    }
400
401
    /**
402
     * Get table alias for the given entity.
403
     *
404
     * @param \Cake\Datasource\EntityInterface $entity The entity
405
     * @return string Table alias
406
     */
407
    protected function _getTableAlias(EntityInterface $entity)
408
    {
409
        $alias = $entity->source();
410
        if (mb_strpos($alias, '.') !== false) {
411
            $parts = explode('.', $alias);
412
            $alias = array_pop($parts);
413
        }
414
415
        return strtolower($alias);
416
    }
417
418
    /**
419
     * Prepares error messages for FormHelper.
420
     *
421
     * @param \Comment\Model\Entity\Comment $comment The invalidated comment entity
422
     * to extract error messages
423
     * @return void
424
     */
425
    protected function _setErrors(Comment $comment)
426
    {
427
        $arrayContext = $this->config('arrayContext');
428
        foreach ((array)$comment->errors() as $field => $msg) {
429
            $arrayContext['errors']['comment'][$field] = $msg;
430
        }
431
        $this->config('arrayContext', $arrayContext);
432
        $this->_controller->set('_commentFormContext', $this->config('arrayContext'));
433
    }
434
435
    /**
436
     * Fetch settings from data base and merges
437
     * with this component's configuration.
438
     *
439
     * @return array
440
     */
441
    protected function _loadSettings()
442
    {
443
        $settings = plugin('Comment')->settings();
444
        foreach ($settings as $k => $v) {
445
            $this->config("settings.{$k}", $v);
446
        }
447
    }
448
449
    /**
450
     * Creates a validation object on the fly.
451
     *
452
     * @return \Cake\Validation\Validator
453
     */
454
    protected function _createValidator()
455
    {
456
        $config = $this->config();
457
        if ($config['validator'] instanceof Validator) {
458
            return $config['validator'];
459
        }
460
461
        $this->_controller->loadModel('Comment.Comments');
462
        if ($this->_controller->request->is('userLoggedIn')) {
463
            // logged user posting
464
            $validator = $this->_controller->Comments->validationDefault(new Validator());
465
            $validator
466
                ->requirePresence('user_id')
467
                ->notEmpty('user_id', __d('comment', 'Invalid user.'))
468
                ->add('user_id', 'checkUserId', [
469
                    'rule' => function ($value, $context) {
470
                        if (!empty($value)) {
471
                            $valid = TableRegistry::get('User.Users')->find()
472
                                ->where(['Users.id' => $value, 'Users.status' => 1])
473
                                ->count() === 1;
474
475
                            return $valid;
476
                        }
477
478
                        return false;
479
                    },
480
                    'message' => __d('comment', 'Invalid user, please try again.'),
481
                    'provider' => 'table',
482
                ]);
483
        } elseif ($this->config('settings.allow_anonymous')) {
484
            // anonymous user posting
485
            $validator = $this->_controller->Comments->validator('anonymous');
486
        } else {
487
            // other case
488
            $validator = new Validator();
489
        }
490
491
        if ($this->config('settings.use_captcha')) {
492
            $validator
493
                ->add('body', 'humanCheck', [
494
                    'rule' => function ($value, $context) {
495
                        return CaptchaManager::adapter()->validate($this->_controller->request);
496
                    },
497
                    'message' => __d('comment', 'We were not able to verify you as human. Please try again.'),
498
                    'provider' => 'table',
499
                ]);
500
        }
501
502
        return $validator;
503
    }
504
}
505