Completed
Branch feature/Save_drafts_on_composi... (9c2717)
by Schlaefer
03:40 queued 01:14
created

EntriesTable::toggle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\Model\Table;
14
15
use App\Lib\Model\Table\AppTable;
16
use App\Model\Entity\Entry;
17
use App\Model\Table\CategoriesTable;
18
use App\Model\Table\DraftsTable;
19
use Bookmarks\Model\Table\BookmarksTable;
20
use Cake\Cache\Cache;
21
use Cake\Event\Event;
22
use Cake\Http\Exception\NotFoundException;
23
use Cake\ORM\Entity;
24
use Cake\ORM\Query;
25
use Cake\ORM\RulesChecker;
26
use Cake\Validation\Validator;
27
use Saito\App\Registry;
28
use Saito\Posting\Posting;
29
use Saito\RememberTrait;
30
use Saito\User\CurrentUser\CurrentUserInterface;
31
use Saito\Validation\SaitoValidationProvider;
32
use Search\Manager;
33
use Stopwatch\Lib\Stopwatch;
34
35
/**
36
 * Stores postings
37
 *
38
 * Field notes:
39
 * - `edited_by` - Came from mylittleforum. @td Should by migrated to User.id.
40
 * - `name` - Came from mylittleforum. Is still used in fulltext index.
41
 *
42
 * @property BookmarksTable $Bookmarks
43
 * @property CategoriesTable $Categories
44
 * @property DraftsTable $Drafts
45
 * @method array treeBuild(array $postings)
46
 * @method createPosting(array $data, CurrentUserInterface $CurrentUser)
47
 * @method updatePosting(Entry $posting, array $data, CurrentUserInterface $CurrentUser)
48
 * @method array prepareChildPosting(BasicPostingInterface $parent, array $data)
49
 */
50
class EntriesTable extends AppTable
51
{
52
    use RememberTrait;
53
54
    /**
55
     * Fields for search plugin
56
     *
57
     * @var array
58
     */
59
    public $filterArgs = [
60
        'subject' => ['type' => 'like'],
61
        'text' => ['type' => 'like'],
62
        'name' => ['type' => 'like'],
63
        'category' => ['type' => 'value'],
64
    ];
65
66
    /**
67
     * field list necessary for displaying a thread_line
68
     *
69
     * Entry.text determine if Entry is n/t
70
     *
71
     * @var array
72
     */
73
    public $threadLineFieldList = [
74
        'Entries.id',
75
        'Entries.pid',
76
        'Entries.tid',
77
        'Entries.subject',
78
        'Entries.text',
79
        'Entries.time',
80
        'Entries.fixed',
81
        'Entries.last_answer',
82
        'Entries.views',
83
        'Entries.user_id',
84
        'Entries.locked',
85
        'Entries.name',
86
        'Entries.solves',
87
        'Users.username',
88
        'Categories.id',
89
        'Categories.accession',
90
        'Categories.category',
91
        'Categories.description'
92
    ];
93
94
    /**
95
     * fields additional to $threadLineFieldList to show complete entry
96
     *
97
     * @var array
98
     */
99
    public $showEntryFieldListAdditional = [
100
        'Entries.edited',
101
        'Entries.edited_by',
102
        'Entries.ip',
103
        'Entries.category_id',
104
        'Users.id',
105
        'Users.avatar',
106
        'Users.signature',
107
        'Users.user_place'
108
    ];
109
110
    protected $_defaultConfig = [
111
        'subject_maxlength' => 100
112
    ];
113
114
    /**
115
     * {@inheritDoc}
116
     */
117
    public function initialize(array $config)
118
    {
119
        $this->setPrimaryKey('id');
120
121
        $this->addBehavior('Posting');
122
        $this->addBehavior('IpLogging');
123
        $this->addBehavior('Timestamp');
124
        $this->addBehavior('Tree');
125
126
        $this->addBehavior(
127
            'CounterCache',
128
            [
129
                // cache how many postings a user has
130
                'Users' => ['entry_count'],
131
                // cache how many threads a category has
132
                'Categories' => [
133
                    'thread_count' => function ($event, Entry $entity, $table, $original) {
134
                        if (!$entity->isRoot()) {
135
                            return false;
136
                        }
137
                        // posting is moved to new category…
138
                        if ($original) {
139
                            // update old category (should decrement counter)
140
                            $categoryId = $entity->getOriginal('category_id');
141
                        } else {
142
                            // update new category (increment counter)
143
                            $categoryId = $entity->get('category_id');
144
                        }
145
146
                        $query = $table->find('all', ['conditions' => [
147
                            'pid' => 0, 'category_id' => $categoryId
148
                        ]]);
149
                        $count = $query->count();
150
151
                        return $count;
152
                    }
153
                ]
154
            ]
155
        );
156
157
        $this->belongsTo('Categories', ['foreignKey' => 'category_id']);
158
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
159
160
        $this->hasMany(
161
            'Bookmarks',
162
            ['foreignKey' => 'entry_id', 'dependent' => true]
163
        );
164
165
        // Releation never queried. Just for quick access to the table.
166
        $this->hasOne('Drafts');
167
    }
168
169
    /**
170
     * {@inheritDoc}
171
     */
172
    public function validationDefault(Validator $validator)
173
    {
174
        $validator->setProvider('saito', SaitoValidationProvider::class);
175
176
        /// category_id
177
        $categoryRequiredL10N = __('vld.entries.categories.notEmpty');
178
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

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

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

Loading history...
179
            ->notEmpty('category_id', $categoryRequiredL10N)
180
            ->requirePresence('category_id', 'create', $categoryRequiredL10N)
181
            ->add(
182
                'category_id',
183
                [
184
                    'numeric' => ['rule' => 'numeric'],
185
                    'assoc' => [
186
                        'rule' => ['validateAssoc', 'Categories'],
187
                        'last' => true,
188
                        'provider' => 'saito'
189
                    ]
190
                ]
191
            );
192
193
        /// last_answer
194
        $validator
195
            ->requirePresence('last_answer', 'create')
196
            ->notEmptyDateTime('last_answer', null, 'create');
197
198
        /// name
199
        $validator
200
            ->requirePresence('name', 'create')
201
            ->notEmptyString('name', null, 'create');
202
203
        /// pid
204
        $validator->requirePresence('pid', 'create');
205
206
        /// subject
207
        $subjectRequiredL10N = __('vld.entries.subject.notEmpty');
208
        $validator
209
            ->notEmptyString('subject', $subjectRequiredL10N)
210
            ->requirePresence('subject', 'create', $subjectRequiredL10N)
211
            ->add(
212
                'subject',
213
                [
214
                    'maxLength' => [
215
                        'rule' => ['maxLength', $this->getConfig('subject_maxlength')],
216
                        'message' => __('vld.entries.subject.maxlength')
217
                    ]
218
                ]
219
            );
220
221
        /// time
222
        $validator
223
            ->requirePresence('time', 'create')
224
            ->notEmptyDateTime('time', null, 'create');
225
226
        /// user_id
227
        $validator
228
            ->requirePresence('user_id', 'create')
229
            ->add('user_id', ['numeric' => ['rule' => 'numeric']]);
230
231
        /// views
232
        $validator->add(
233
            'views',
234
            ['comparison' => ['rule' => ['comparison', '>=', 0]]]
235
        );
236
237
        return $validator;
238
    }
239
240
    /**
241
     * {@inheritDoc}
242
     */
243
    public function buildRules(RulesChecker $rules)
244
    {
245
        $rules = parent::buildRules($rules);
246
247
        $rules->addUpdate(
248
            function ($entity) {
249
                if ($entity->isDirty('category_id')) {
250
                    return $entity->isRoot();
251
                }
252
253
                return true;
254
            },
255
            'checkCategoryChangeOnlyOnRootPostings',
256
            [
257
                'errorField' => 'category_id',
258
                'message' => 'Cannot change category on non-root-postings.',
259
            ]
260
        );
261
262
        return $rules;
263
    }
264
265
    /**
266
     * Advanced search configuration from SaitoSearch plugin
267
     *
268
     * @see https://github.com/FriendsOfCake/search
269
     *
270
     * @return Manager
271
     */
272
    public function searchManager(): Manager
273
    {
274
        /** @var Manager $searchManager */
275
        $searchManager = $this->getBehavior('Search')->searchManager();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Cake\ORM\Behavior as the method searchManager() does only exist in the following sub-classes of Cake\ORM\Behavior: Search\Model\Behavior\SearchBehavior. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
276
        $searchManager
277
        ->like('subject', [
278
            'before' => true,
279
            'after' => true,
280
            'fieldMode' => 'OR',
281
            'comparison' => 'LIKE',
282
            'wildcardAny' => '*',
283
            'wildcardOne' => '?',
284
            'field' => ['subject'],
285
            'filterEmpty' => true,
286
        ])
287
        ->like('text', [
288
            'before' => true,
289
            'after' => true,
290
            'fieldMode' => 'OR',
291
            'comparison' => 'LIKE',
292
            'wildcardAny' => '*',
293
            'wildcardOne' => '?',
294
            'field' => ['text'],
295
            'filterEmpty' => true,
296
        ])
297
        ->value('name', ['filterEmpty' => true]);
298
299
        return $searchManager;
300
    }
301
302
    /**
303
     * Get recent postings
304
     *
305
     * ### Options:
306
     *
307
     * - `user_id` int|<null> If provided finds only postings of that user.
308
     * - `limit` int <10> Number of postings to find.
309
     *
310
     * @param CurrentUserInterface $User User who has access to postings
311
     * @param array $options find options
312
     *
313
     * @return array Array of Postings
314
     */
315
    public function getRecentEntries(
316
        CurrentUserInterface $User,
317
        array $options = []
318
    ) {
319
        Stopwatch::start('Model->User->getRecentEntries()');
320
321
        $options += [
322
            'user_id' => null,
323
            'limit' => 10,
324
        ];
325
326
        $options['category_id'] = $User->getCategories()->getAll('read');
327
328
        $read = function () use ($options) {
329
            $conditions = [];
330
            if ($options['user_id'] !== null) {
331
                $conditions[]['Entries.user_id'] = $options['user_id'];
332
            }
333
            if ($options['category_id'] !== null) {
334
                $conditions[]['Entries.category_id IN'] = $options['category_id'];
335
            };
336
337
            $result = $this
338
                ->find(
339
                    'all',
340
                    [
341
                        'contain' => ['Users', 'Categories'],
342
                        'fields' => $this->threadLineFieldList,
343
                        'conditions' => $conditions,
344
                        'limit' => $options['limit'],
345
                        'order' => ['time' => 'DESC']
346
                    ]
347
                )
348
                // hydrating kills performance
349
                ->enableHydration(false)
350
                ->all();
351
352
            return $result;
353
        };
354
355
        $key = 'Entry.recentEntries-' . md5(serialize($options));
356
        $results = Cache::remember($key, $read, 'entries');
357
358
        $threads = [];
359
        foreach ($results as $result) {
360
            $threads[$result['id']] = Registry::newInstance(
361
                '\Saito\Posting\Posting',
362
                ['rawData' => $result]
363
            );
364
        }
365
366
        Stopwatch::stop('Model->User->getRecentEntries()');
367
368
        return $threads;
369
    }
370
371
    /**
372
     * Finds the thread-id for a posting
373
     *
374
     * @param int $id Posting-Id
375
     * @return int Thread-Id
376
     * @throws \UnexpectedValueException
377
     */
378
    public function getThreadId($id)
379
    {
380
        $entry = $this->find(
381
            'all',
382
            ['conditions' => ['id' => $id], 'fields' => 'tid']
383
        )->first();
384
        if (empty($entry)) {
385
            throw new \UnexpectedValueException(
386
                'Posting not found. Posting-Id: ' . $id
387
            );
388
        }
389
390
        return $entry->get('tid');
391
    }
392
393
    /**
394
     * Shorthand for reading an entry with full data
395
     *
396
     * @param int $primaryKey key
397
     * @param array $options options
398
     * @return mixed Posting if found false otherwise
399
     */
400
    public function get($primaryKey, $options = [])
401
    {
402
        $options += ['return' => 'Posting'];
403
        $return = $options['return'];
404
        unset($options['return']);
405
406
        /** @var Entry */
407
        $result = $this->find('entry')
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->find('entry')->wh...$primaryKey))->first(); of type Cake\Datasource\EntityInterface|array|null adds the type array to the return on line 420 which is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.
Loading history...
408
            ->where([$this->getAlias() . '.id' => $primaryKey])
409
            ->first();
410
411
        if (!$result) {
412
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
413
        }
414
415
        switch ($return) {
416
            case 'Posting':
417
                return $result->toPosting();
418
            case 'Entity':
419
            default:
420
                return $result;
421
        }
422
    }
423
424
    /**
425
     * get parent id
426
     *
427
     * @param int $id id
428
     * @return mixed
429
     * @throws \UnexpectedValueException
430
     */
431
    public function getParentId($id)
432
    {
433
        $entry = $this->find()->select('pid')->where(['id' => $id])->first();
434
        if (!$entry) {
435
            throw new \UnexpectedValueException(
436
                'Posting not found. Posting-Id: ' . $id
437
            );
438
        }
439
440
        return $entry->get('pid');
441
    }
442
443
    /**
444
     * creates a new root or child entry for a node
445
     *
446
     * fields in $data are filtered
447
     *
448
     * @param array $data data
449
     * @return Entry|null on success, null otherwise
450
     */
451
    public function createEntry(array $data): ?Entry
452
    {
453
        $data['time'] = bDate();
454
        $data['last_answer'] = bDate();
455
456
        /** @var Entry */
457
        $posting = $this->newEntity($data);
458
        $errors = $posting->getErrors();
459
        if (!empty($errors)) {
460
            return $posting;
461
        }
462
463
        $posting = $this->save($posting);
464
        if (!$posting) {
465
            return null;
466
        }
467
468
        $id = $posting->get('id');
469
        /** @var Entry */
470
        $posting = $this->get($id, ['return' => 'Entity']);
471
472
        if ($posting->isRoot()) {
473
            // posting started a new thread, so set thread-ID to posting's own ID
474
            /** @var Entry */
475
            $posting = $this->patchEntity($posting, ['tid' => $id]);
476
            if (!$this->save($posting)) {
477
                return $posting;
478
            }
479
480
            $this->_dispatchEvent('Model.Thread.create', ['subject' => $id, 'data' => $posting]);
481
        } else {
482
            // update last answer time of root entry
483
            $this->updateAll(
484
                ['last_answer' => $posting->get('last_answer')],
485
                ['id' => $posting->get('tid')]
486
            );
487
488
            $eventData = ['subject' => $posting->get('pid'), 'data' => $posting];
489
            $this->_dispatchEvent('Model.Entry.replyToEntry', $eventData);
490
            $this->_dispatchEvent('Model.Entry.replyToThread', $eventData);
491
        }
492
493
        return $posting;
494
    }
495
496
    /**
497
     * Updates a posting
498
     *
499
     * fields in $data are filtered except for $id!
500
     *
501
     * @param Entry $posting Entity
502
     * @param array $data data
503
     * @return Entry|null
504
     */
505
    public function updateEntry(Entry $posting, array $data): ?Entry
506
    {
507
        $data['id'] = $posting->get('id');
508
        $data['edited'] = bDate();
509
510
        /** @var Entry */
511
        $patched = $this->patchEntity($posting, $data);
512
        $errors = $patched->getErrors();
513
        if (!empty($errors)) {
514
            return $patched;
515
        }
516
517
        /** @var Entry */
518
        $new = $this->save($posting);
519
        if (!$new) {
520
            return null;
521
        }
522
523
        $this->_dispatchEvent(
524
            'Model.Entry.update',
525
            ['subject' => $posting->get('id'), 'data' => $posting]
526
        );
527
528
        return $new;
529
    }
530
531
    /**
532
     * tree of a single node and its subentries
533
     *
534
     * $options = array(
535
     *    'root' => true // performance improvements if it's a known thread-root
536
     * );
537
     *
538
     * @param int $id id
539
     * @param array $options options
540
     * @return Posting|null tree or null if nothing found
541
     */
542
    public function treeForNode(int $id, ?array $options = []): ?Posting
543
    {
544
        $options += [
545
            'root' => false,
546
            'complete' => false
547
        ];
548
549
        if ($options['root']) {
550
            $tid = $id;
551
        } else {
552
            $tid = $this->getThreadId($id);
553
        }
554
555
        $fields = null;
556
        if ($options['complete']) {
557
            $fields = array_merge(
558
                $this->threadLineFieldList,
559
                $this->showEntryFieldListAdditional
560
            );
561
        }
562
563
        $tree = $this->treesForThreads([$tid], null, $fields);
564
565
        if (!$tree) {
566
            return null;
567
        }
568
569
        $tree = reset($tree);
570
571
        //= extract subtree
572
        if ((int)$tid !== (int)$id) {
573
            $tree = $tree->getThread()->get($id);
574
        }
575
576
        return $tree;
577
    }
578
579
    /**
580
     * trees for multiple tids
581
     *
582
     * @param array $ids ids
583
     * @param array $order order
584
     * @param array $fieldlist fieldlist
585
     * @return array|null array of Postings, null if nothing found
586
     */
587
    public function treesForThreads(array $ids, ?array $order = null, array $fieldlist = null): ?array
588
    {
589
        if (empty($ids)) {
590
            return [];
591
        }
592
593
        if (empty($order)) {
594
            $order = ['last_answer' => 'ASC'];
595
        }
596
597
        if ($fieldlist === null) {
598
            $fieldlist = $this->threadLineFieldList;
599
        }
600
601
        Stopwatch::start('EntriesTable::treesForThreads() DB');
602
        $postings = $this->_getThreadEntries(
603
            $ids,
604
            ['order' => $order, 'fields' => $fieldlist]
605
        );
606
        Stopwatch::stop('EntriesTable::treesForThreads() DB');
607
608
        if (!$postings->count()) {
609
            return null;
610
        }
611
612
        Stopwatch::start('EntriesTable::treesForThreads() CPU');
613
        $threads = [];
614
        $postings = $this->treeBuild($postings);
0 ignored issues
show
Documentation introduced by
$postings is of type object<Cake\ORM\Query>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
615
        foreach ($postings as $thread) {
616
            $id = $thread['tid'];
617
            $threads[$id] = $thread;
618
            $threads[$id] = Registry::newInstance(
619
                '\Saito\Posting\Posting',
620
                ['rawData' => $thread]
621
            );
622
        }
623
        Stopwatch::stop('EntriesTable::treesForThreads() CPU');
624
625
        return $threads;
626
    }
627
628
    /**
629
     * Returns all entries of threads $tid
630
     *
631
     * @param array $tid ids
632
     * @param array $params params
633
     * - 'fields' array of thread-ids: [1, 2, 5]
634
     * - 'order' sort order for threads ['time' => 'ASC'],
635
     * @return mixed unhydrated result set
636
     */
637
    protected function _getThreadEntries(array $tid, array $params = [])
638
    {
639
        $params += [
640
            'fields' => $this->threadLineFieldList,
641
            'order' => ['last_answer' => 'ASC']
642
        ];
643
644
        $threads = $this
645
            ->find(
646
                'all',
647
                [
648
                    'conditions' => ['tid IN' => $tid],
649
                    'contain' => ['Users', 'Categories'],
650
                    'fields' => $params['fields'],
651
                    'order' => $params['order']
652
                ]
653
            )
654
            // hydrating kills performance
655
            ->enableHydration(false);
656
657
        return $threads;
658
    }
659
660
    /**
661
     * Marks a sub-entry as solution to a root entry
662
     *
663
     * @param Entry $posting posting to toggle
664
     * @return bool success
665
     */
666
    public function toggleSolve(Entry $posting)
667
    {
668
        if ($posting->get('solves')) {
669
            $value = 0;
670
        } else {
671
            $value = $posting->get('tid');
672
        }
673
674
        $this->patchEntity($posting, ['solves' => $value]);
675
        if (!$this->save($posting)) {
676
            return false;
677
        }
678
679
        $this->_dispatchEvent(
680
            'Model.Entry.update',
681
            ['subject' => $posting->get('id'), 'data' => $posting]
682
        );
683
684
        return true;
685
    }
686
687
    /**
688
     * {@inheritDoc}
689
     */
690
    public function toggle($id, $key)
691
    {
692
        $result = parent::toggle($id, $key);
693
        if ($key === 'locked') {
694
            $this->_threadLock($id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
695
        }
696
697
        $entry = $this->get($id);
698
        $this->_dispatchEvent(
699
            'Model.Entry.update',
700
            [
701
                'subject' => $entry->get('id'),
702
                'data' => $entry
703
            ]
704
        );
705
706
        return $result;
707
    }
708
709
    /**
710
     * {@inheritDoc}
711
     */
712
    public function beforeMarshal(Event $event, \ArrayObject $data, \ArrayObject $options)
2 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
713
    {
714
        /// Trim whitespace on subject and text
715
        $toTrim = ['subject', 'text'];
716
        foreach ($toTrim as $field) {
717
            if (!empty($data[$field])) {
718
                $data[$field] = trim($data[$field]);
719
            }
720
        }
721
    }
722
723
    /**
724
     * Deletes posting incl. all its subposting and associated data
725
     *
726
     * @param int $id id
727
     * @throws \InvalidArgumentException
728
     * @throws \Exception
729
     * @return bool
730
     */
731
    public function treeDeleteNode($id)
732
    {
733
        $root = $this->treeForNode((int)$id);
734
735
        if (empty($root)) {
736
            throw new \Exception;
737
        }
738
739
        $nodesToDelete[] = $root;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$nodesToDelete was never initialized. Although not strictly required by PHP, it is generally a good practice to add $nodesToDelete = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
740
        $nodesToDelete = array_merge($nodesToDelete, $root->getAllChildren());
741
742
        $idsToDelete = [];
743
        foreach ($nodesToDelete as $node) {
744
            $idsToDelete[] = $node->get('id');
745
        };
746
747
        $success = $this->deleteAll(['id IN' => $idsToDelete]);
748
749
        if (!$success) {
750
            return false;
751
        }
752
753
        $this->Bookmarks->deleteAll(['entry_id IN' => $idsToDelete]);
754
755
        $this->dispatchSaitoEvent(
756
            'Model.Saito.Posting.delete',
757
            ['subject' => $root, 'table' => $this]
758
        );
759
760
        return true;
761
    }
762
763
    /**
764
     * Anonymizes the entries for a user
765
     *
766
     * @param int $userId user-ID
767
     * @return void
768
     */
769
    public function anonymizeEntriesFromUser(int $userId): void
770
    {
771
        // remove username from all entries and reassign to anonyme user
772
        $success = (bool)$this->updateAll(
773
            [
774
                'edited_by' => null,
775
                'ip' => null,
776
                'name' => null,
777
                'user_id' => 0,
778
            ],
779
            ['user_id' => $userId]
780
        );
781
782
        if ($success) {
783
            $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
784
        }
785
    }
786
787
    /**
788
     * Merge thread on to entry $targetId
789
     *
790
     * @param int $sourceId root-id of the posting that is merged onto another
791
     *     thread
792
     * @param int $targetId id of the posting the source-thread should be
793
     *     appended to
794
     * @return bool true if merge was successfull false otherwise
795
     */
796
    public function threadMerge($sourceId, $targetId)
797
    {
798
        $sourcePosting = $this->get($sourceId, ['return' => 'Entity']);
799
800
        // check that source is thread-root and not an subposting
801
        if (!$sourcePosting->isRoot()) {
802
            return false;
803
        }
804
805
        $targetPosting = $this->get($targetId);
806
807
        // check that target exists
808
        if (!$targetPosting) {
809
            return false;
810
        }
811
812
        // check that a thread is not merged onto itself
813
        if ($targetPosting->get('tid') === $sourcePosting->get('tid')) {
814
            return false;
815
        }
816
817
        // set target entry as new parent entry
818
        $this->patchEntity(
819
            $sourcePosting,
820
            ['pid' => $targetPosting->get('id')]
821
        );
822
        if ($this->save($sourcePosting)) {
823
            // associate all entries in source thread to target thread
824
            $this->updateAll(
825
                ['tid' => $targetPosting->get('tid')],
826
                ['tid' => $sourcePosting->get('tid')]
827
            );
828
829
            // appended source entries get category of target thread
830
            $this->_threadChangeCategory(
831
                $targetPosting->get('tid'),
832
                $targetPosting->get('category_id')
833
            );
834
835
            // update target thread last answer if source is newer
836
            $sourceLastAnswer = $sourcePosting->get('last_answer');
837
            $targetLastAnswer = $targetPosting->get('last_answer');
838
            if ($sourceLastAnswer->gt($targetLastAnswer)) {
839
                $targetRoot = $this->get(
840
                    $targetPosting->get('tid'),
841
                    ['return' => 'Entity']
842
                );
843
                $targetRoot = $this->patchEntity(
844
                    $targetRoot,
845
                    ['last_answer' => $sourceLastAnswer]
846
                );
847
                $this->save($targetRoot);
848
            }
849
850
            // propagate pinned property from target to source
851
            $isTargetPinned = $targetPosting->isLocked();
852
            $isSourcePinned = $sourcePosting->isLocked();
853
            if ($isSourcePinned !== $isTargetPinned) {
854
                $this->_threadLock($targetPosting->get('tid'), $isTargetPinned);
855
            }
856
857
            $this->_dispatchEvent(
858
                'Model.Thread.change',
859
                ['subject' => $targetPosting->get('tid')]
860
            );
861
862
            return true;
863
        }
864
865
        return false;
866
    }
867
868
    /**
869
     * Implements the custom find type 'entry'
870
     *
871
     * @param Query $query query
872
     * @return Query
873
     */
874
    public function findEntry(Query $query)
875
    {
876
        $fields = array_merge(
877
            $this->threadLineFieldList,
878
            $this->showEntryFieldListAdditional
879
        );
880
        $query->select($fields)->contain(['Users', 'Categories']);
881
882
        return $query;
883
    }
884
885
    /**
886
     * Implements the custom find type 'index paginator'
887
     *
888
     * @param Query $query query
889
     * @param array $options finder options
890
     * @return Query
891
     */
892
    public function findIndexPaginator(Query $query, array $options)
893
    {
894
        $query
895
            ->select(['id', 'pid', 'tid', 'time', 'last_answer', 'fixed'])
896
            ->where(['Entries.pid' => 0]);
897
898
        if (!empty($options['counter'])) {
899
            $query->counter($options['counter']);
900
        }
901
902
        return $query;
903
    }
904
905
    /**
906
     * Un-/Locks thread: sets posting in thread $tid to $locked
907
     *
908
     * @param int $tid thread-ID
909
     * @param bool $locked flag
910
     * @return void
911
     */
912
    protected function _threadLock($tid, $locked)
913
    {
914
        $this->updateAll(['locked' => $locked], ['tid' => $tid]);
915
    }
916
917
    /**
918
     * {@inheritDoc}
919
     */
920
    public function beforeSave(Event $event, Entity $entity)
921
    {
922
        $success = true;
923
924
        /// change category of thread if category of root entry changed
925
        if (!$entity->isNew() && $entity->isDirty('category_id')) {
926
            $success &= $this->_threadChangeCategory(
927
                // rules checks that only roots are allowed to change category, so tid = id
928
                $entity->get('id'),
929
                $entity->get('category_id')
930
            );
931
        }
932
933
        if (!$success) {
934
            $event->stopPropagation();
935
        }
936
    }
937
938
    /**
939
     * {@inheritDoc}
940
     */
941
    public function afterSave(Event $event, Entity $entity, \ArrayObject $options)
2 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
942
    {
943
        if ($entity->isNew()) {
944
            $this->Drafts->deleteDraftForPosting($entity);
945
        }
946
    }
947
948
    /**
949
     * Changes the category of a thread.
950
     *
951
     * Assigns the new category-id to all postings in that thread.
952
     *
953
     * @param int $tid thread-ID
954
     * @param int $newCategoryId id for new category
955
     * @return bool success
956
     * @throws NotFoundException
957
     */
958
    protected function _threadChangeCategory(int $tid, int $newCategoryId): bool
959
    {
960
        $exists = $this->Categories->exists($newCategoryId);
961
        if (!$exists) {
962
            throw new NotFoundException();
963
        }
964
        $affected = $this->updateAll(
965
            ['category_id' => $newCategoryId],
966
            ['tid' => $tid]
967
        );
968
969
        return $affected > 0;
970
    }
971
}
972