Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
09:08
created

EntriesController::mix()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 18
nc 4
nop 1
dl 0
loc 32
rs 9.6666
c 1
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\Controller;
14
15
use App\Controller\Component\AutoReloadComponent;
16
use App\Controller\Component\MarkAsReadComponent;
17
use App\Controller\Component\RefererComponent;
18
use App\Controller\Component\ThreadsComponent;
19
use App\Model\Table\EntriesTable;
20
use Cake\Core\Configure;
21
use Cake\Datasource\Exception\RecordNotFoundException;
22
use Cake\Event\Event;
23
use Cake\Http\Exception\BadRequestException;
24
use Cake\Http\Exception\ForbiddenException;
25
use Cake\Http\Exception\MethodNotAllowedException;
26
use Cake\Http\Exception\NotFoundException;
27
use Cake\Http\Response;
28
use Cake\Routing\RequestActionTrait;
29
use Saito\Exception\SaitoForbiddenException;
30
use Saito\Posting\Basic\BasicPostingInterface;
31
use Saito\User\CurrentUser\CurrentUserInterface;
32
use Stopwatch\Lib\Stopwatch;
33
34
/**
35
 * Class EntriesController
36
 *
37
 * @property CurrentUserInterface $CurrentUser
38
 * @property EntriesTable $Entries
39
 * @property MarkAsReadComponent $MarkAsRead
40
 * @property RefererComponent $Referer
41
 * @property ThreadsComponent $Threads
42
 */
43
class EntriesController extends AppController
44
{
45
    use RequestActionTrait;
0 ignored issues
show
Deprecated Code introduced by
The trait Cake\Routing\RequestActionTrait has been deprecated: 3.3.0 Use view cells instead. ( Ignorable by Annotation )

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

45
    use /** @scrutinizer ignore-deprecated */ RequestActionTrait;

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

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

Loading history...
46
47
    public $helpers = ['Posting', 'Text'];
48
49
    /**
50
     * {@inheritDoc}
51
     */
52
    public function initialize()
53
    {
54
        parent::initialize();
55
56
        $this->loadComponent('MarkAsRead');
57
        $this->loadComponent('Referer');
58
        $this->loadComponent('Threads', ['table' => $this->Entries]);
59
    }
60
61
    /**
62
     * posting index
63
     *
64
     * @return void|\Cake\Network\Response
65
     */
66
    public function index()
67
    {
68
        Stopwatch::start('Entries->index()');
69
70
        //= determine user sort order
71
        $sortKey = 'last_answer';
72
        if (!$this->CurrentUser->get('user_sort_last_answer')) {
73
            $sortKey = 'time';
74
        }
75
        $order = ['fixed' => 'DESC', $sortKey => 'DESC'];
76
77
        //= get threads
78
        $threads = $this->Threads->paginate($order, $this->CurrentUser);
79
        $this->set('entries', $threads);
80
81
        $currentPage = (int)$this->request->getQuery('page') ?: 1;
82
        if ($currentPage > 1) {
83
            $this->set('titleForLayout', __('page') . ' ' . $currentPage);
84
        }
85
        if ($currentPage === 1) {
86
            if ($this->MarkAsRead->refresh()) {
87
                return $this->redirect(['action' => 'index']);
88
            }
89
            $this->MarkAsRead->next();
90
        }
91
92
        // @bogus
93
        $this->request->getSession()->write('paginator.lastPage', $currentPage);
94
        $this->set('showDisclaimer', true);
95
        $this->set('showBottomNavigation', true);
96
        $this->set('allowThreadCollapse', true);
97
        $this->Slidetabs->show();
98
99
        $this->_setupCategoryChooser($this->CurrentUser);
100
101
        /** @var AutoReloadComponent */
102
        $autoReload = $this->loadComponent('AutoReload');
103
        $autoReload->after($this->CurrentUser);
0 ignored issues
show
Bug introduced by
The method after() does not exist on Cake\Controller\Component. It seems like you code against a sub-type of Cake\Controller\Component such as Cake\Controller\Component\FlashComponent or App\Controller\Component\AutoReloadComponent or Cake\Controller\Component\PaginatorComponent. ( Ignorable by Annotation )

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

103
        $autoReload->/** @scrutinizer ignore-call */ 
104
                     after($this->CurrentUser);
Loading history...
104
105
        Stopwatch::stop('Entries->index()');
106
    }
107
108
    /**
109
     * Mix view
110
     *
111
     * @param string $tid thread-ID
112
     * @return void|Response
113
     * @throws NotFoundException
114
     */
115
    public function mix($tid)
116
    {
117
        $tid = (int)$tid;
118
        if ($tid <= 0) {
119
            throw new BadRequestException();
120
        }
121
122
        try {
123
            $postings = $this->Entries->postingsForThread($tid, true, $this->CurrentUser);
124
        } catch (RecordNotFoundException $e) {
125
            /// redirect sub-posting to mix view of thread
126
            $actualTid = $this->Entries->getThreadId($tid);
127
128
            return $this->redirect([$actualTid, '#' => $tid], 301);
129
        }
130
131
        // check if anonymous tries to access internal categories
132
        $root = $postings;
133
        if (!$this->CurrentUser->getCategories()->permission('read', $root->get('category'))) {
134
            return $this->_requireAuth();
135
        }
136
137
        $this->_setRootEntry($root);
138
        $this->Title->setFromPosting($root, __('view.type.mix'));
139
140
        $this->set('showBottomNavigation', true);
141
        $this->set('entries', $postings);
142
143
        $this->_showAnsweringPanel();
144
145
        $this->Threads->incrementViewsForThread($root, $this->CurrentUser);
146
        $this->MarkAsRead->thread($postings);
147
    }
148
149
    /**
150
     * load front page force all entries mark-as-read
151
     *
152
     * @return void
153
     */
154
    public function update()
155
    {
156
        $this->autoRender = false;
157
        $this->CurrentUser->getLastRefresh()->set();
158
        $this->redirect('/entries/index');
159
    }
160
161
    /**
162
     * Outputs raw markup of an posting $id
163
     *
164
     * @param string $id posting-ID
165
     * @return void
166
     */
167
    public function source($id = null)
168
    {
169
        $this->viewBuilder()->enableAutoLayout(false);
170
        $this->view($id);
171
    }
172
173
    /**
174
     * View posting.
175
     *
176
     * @param string $id posting-ID
177
     * @return \Cake\Network\Response|void
178
     */
179
    public function view($id = null)
180
    {
181
        Stopwatch::start('Entries->view()');
182
183
        // redirect if no id is given
184
        if (!$id) {
185
            $this->Flash->set(__('Invalid post'), ['element' => 'error']);
186
187
            return $this->redirect(['action' => 'index']);
188
        }
189
190
        $entry = $this->Entries->get($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $primaryKey of App\Model\Table\EntriesTable::get(). ( Ignorable by Annotation )

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

190
        $entry = $this->Entries->get(/** @scrutinizer ignore-type */ $id);
Loading history...
191
192
        // redirect if posting doesn't exists
193
        if ($entry == false) {
194
            $this->Flash->set(__('Invalid post'));
195
196
            return $this->redirect('/');
197
        }
198
199
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
0 ignored issues
show
Bug introduced by
The method toPosting() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\Entry. ( Ignorable by Annotation )

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

199
        $posting = $entry->/** @scrutinizer ignore-call */ toPosting()->withCurrentUser($this->CurrentUser);
Loading history...
200
201
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
202
            return $this->_requireAuth();
203
        }
204
205
        $this->set('entry', $posting);
206
        $this->Threads->incrementViewsForPosting($posting, $this->CurrentUser);
207
        $this->_setRootEntry($posting);
208
        $this->_showAnsweringPanel();
209
210
        $this->MarkAsRead->posting($posting);
211
212
        // inline open
213
        if ($this->request->is('ajax')) {
214
            return $this->render('/Element/posting/view_posting');
215
        }
216
217
        // full page request
218
        $this->set(
219
            'tree',
220
            $this->Entries->postingsForThread($posting->get('tid'), false, $this->CurrentUser)
221
        );
222
        $this->Title->setFromPosting($posting);
223
224
        Stopwatch::stop('Entries->view()');
225
    }
226
227
    /**
228
     * Add new posting.
229
     *
230
     * @return void|\Cake\Network\Response
231
     */
232
    public function add()
233
    {
234
        $titleForPage = __('Write a New Posting');
235
        $this->set(compact('titleForPage'));
236
    }
237
238
    /**
239
     * Edit posting
240
     *
241
     * @param string $id posting-ID
242
     * @return void|\Cake\Network\Response
243
     * @throws NotFoundException
244
     * @throws BadRequestException
245
     */
246
    public function edit($id = null)
247
    {
248
        if (empty($id)) {
249
            throw new BadRequestException;
250
        }
251
252
        $entry = $this->Entries->get($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $primaryKey of App\Model\Table\EntriesTable::get(). ( Ignorable by Annotation )

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

252
        $entry = $this->Entries->get(/** @scrutinizer ignore-type */ $id);
Loading history...
253
        if (empty($entry)) {
254
            throw new NotFoundException;
255
        }
256
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
257
258
        if (!$posting->isEditingAllowed()) {
259
            throw new SaitoForbiddenException(
260
                'Access to posting in EntriesController:edit() forbidden.',
261
                ['CurrentUser' => $this->CurrentUser]
262
            );
263
        }
264
265
        // show editing form
266
        if (!$posting->isEditingAsUserAllowed()) {
267
            $this->Flash->set(
268
                __('notice_you_are_editing_as_mod'),
269
                ['element' => 'warning']
270
            );
271
        }
272
273
        $this->set(compact('posting'));
274
275
        // set headers
276
        $this->set(
277
            'headerSubnavLeftTitle',
278
            __('back_to_posting_from_linkname', $posting->get('name'))
279
        );
280
        $this->set('headerSubnavLeftUrl', ['action' => 'view', $id]);
281
        $this->set('form_title', __('edit_linkname'));
282
        $this->render('/Entries/add');
283
    }
284
285
    /**
286
     * Get thread-line to insert after an inline-answer
287
     *
288
     * @param string $id posting-ID
289
     * @return void|\Cake\Network\Response
290
     */
291
    public function threadLine($id = null)
292
    {
293
        $posting = $this->Entries->get($id)->toPosting()->withCurrentUser($this->CurrentUser);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type string; however, parameter $primaryKey of App\Model\Table\EntriesTable::get() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

293
        $posting = $this->Entries->get(/** @scrutinizer ignore-type */ $id)->toPosting()->withCurrentUser($this->CurrentUser);
Loading history...
294
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
295
            return $this->_requireAuth();
296
        }
297
298
        $this->set('entrySub', $posting);
299
        // ajax requests so far are always answers
300
        $this->response = $this->response->withType('json');
301
        $this->set('level', '1');
302
    }
303
304
    /**
305
     * Delete posting
306
     *
307
     * @param string $id posting-ID
308
     * @return void
309
     * @throws NotFoundException
310
     * @throws MethodNotAllowedException
311
     */
312
    public function delete(string $id)
313
    {
314
        //$this->request->allowMethod(['post', 'delete']);
315
        $id = (int)$id;
316
        if (!$id) {
317
            throw new NotFoundException;
318
        }
319
        /* @var Entry $posting */
320
        $posting = $this->Entries->get($id);
321
        if (!$posting) {
0 ignored issues
show
introduced by
$posting is of type App\Controller\Entry, thus it always evaluated to true.
Loading history...
322
            throw new NotFoundException;
323
        }
324
325
        $action = $posting->isRoot() ? 'thread' : 'answer';
326
        $allowed = $this->CurrentUser->getCategories()
327
            ->permission($action, $posting->get('category_id'));
328
        if (!$allowed) {
329
            throw new ForbiddenException(null, 1571309481);
330
        }
331
332
        $success = $this->Entries->deletePosting($id);
333
334
        if ($success) {
335
            $flashType = 'success';
336
            if ($posting->isRoot()) {
337
                $message = __('delete_tree_success');
338
                $redirect = '/';
339
            } else {
340
                $message = __('delete_subtree_success');
341
                $redirect = '/entries/view/' . $posting->get('pid');
342
            }
343
        } else {
344
            $flashType = 'error';
345
            $message = __('delete_tree_error');
346
            $redirect = $this->referer();
347
        }
348
        $this->Flash->set($message, ['element' => $flashType]);
349
        $this->redirect($redirect);
350
    }
351
352
    /**
353
     * Empty function for benchmarking
354
     *
355
     * @return void
356
     */
357
    public function e()
358
    {
359
        Stopwatch::start('Entries->e()');
360
        Stopwatch::stop('Entries->e()');
361
    }
362
363
    /**
364
     * Marks sub-entry $id as solution to its current root-entry
365
     *
366
     * @param string $id posting-ID
367
     * @return void
368
     * @throws BadRequestException
369
     */
370
    public function solve($id)
371
    {
372
        $this->autoRender = false;
373
        try {
374
            $posting = $this->Entries->get($id);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $primaryKey of App\Model\Table\EntriesTable::get(). ( Ignorable by Annotation )

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

374
            $posting = $this->Entries->get(/** @scrutinizer ignore-type */ $id);
Loading history...
375
376
            if (empty($posting)) {
377
                throw new \InvalidArgumentException('Posting to mark solved not found.');
378
            }
379
380
            if ($posting->isRoot()) {
0 ignored issues
show
Bug introduced by
The method isRoot() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\Entry. ( Ignorable by Annotation )

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

380
            if ($posting->/** @scrutinizer ignore-call */ isRoot()) {
Loading history...
381
                throw new \InvalidArgumentException('Root postings cannot mark themself solved.');
382
            }
383
384
            $rootId = $posting->get('tid');
385
            $rootPosting = $this->Entries->get($rootId);
386
            if ($rootPosting->get('user_id') !== $this->CurrentUser->getId()) {
387
                throw new SaitoForbiddenException(
388
                    sprintf('Attempt to mark posting %s as solution.', $posting->get('id')),
389
                    ['CurrentUser' => $this->CurrentUser]
390
                );
391
            }
392
393
            $success = $this->Entries->toggleSolve($posting);
0 ignored issues
show
Bug introduced by
It seems like $posting can also be of type array; however, parameter $posting of App\Model\Table\EntriesTable::toggleSolve() does only seem to accept App\Model\Entity\Entry, maybe add an additional type check? ( Ignorable by Annotation )

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

393
            $success = $this->Entries->toggleSolve(/** @scrutinizer ignore-type */ $posting);
Loading history...
394
395
            if (!$success) {
396
                throw new BadRequestException;
397
            }
398
        } catch (\Exception $e) {
399
            throw new BadRequestException();
400
        }
401
    }
402
403
    /**
404
     * Merge threads.
405
     *
406
     * @param string $sourceId posting-ID of thread to be merged
407
     * @return void
408
     * @throws NotFoundException
409
     * @td put into admin entries controller
410
     */
411
    public function merge($sourceId = null)
412
    {
413
        if (!$sourceId) {
414
            throw new NotFoundException();
415
        }
416
417
        /* @var Entry */
418
        $entry = $this->Entries->findById($sourceId)->first();
0 ignored issues
show
Bug introduced by
The method findById() does not exist on App\Model\Table\EntriesTable. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

418
        $entry = $this->Entries->/** @scrutinizer ignore-call */ findById($sourceId)->first();
Loading history...
419
420
        if (!$entry || !$entry->isRoot()) {
421
            throw new NotFoundException();
422
        }
423
424
        // perform move operation
425
        $targetId = $this->request->getData('targetId');
426
        if (!empty($targetId)) {
427
            if ($this->Entries->threadMerge($sourceId, $targetId)) {
0 ignored issues
show
Bug introduced by
$sourceId of type string is incompatible with the type integer expected by parameter $sourceId of App\Model\Table\EntriesTable::threadMerge(). ( Ignorable by Annotation )

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

427
            if ($this->Entries->threadMerge(/** @scrutinizer ignore-type */ $sourceId, $targetId)) {
Loading history...
Bug introduced by
$targetId of type array|string is incompatible with the type integer expected by parameter $targetId of App\Model\Table\EntriesTable::threadMerge(). ( Ignorable by Annotation )

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

427
            if ($this->Entries->threadMerge($sourceId, /** @scrutinizer ignore-type */ $targetId)) {
Loading history...
428
                $this->redirect('/entries/view/' . $sourceId);
429
430
                return;
431
            } else {
432
                $this->Flash->set(__('Error'), ['element' => 'error']);
433
            }
434
        }
435
436
        $this->viewBuilder()->setLayout('Admin.admin');
437
        $this->set('posting', $entry);
438
    }
439
440
    /**
441
     * Toggle posting property via ajax request.
442
     *
443
     * @param string $id posting-ID
444
     * @param string $toggle property
445
     *
446
     * @return \Cake\Network\Response
447
     */
448
    public function ajaxToggle($id = null, $toggle = null)
449
    {
450
        $allowed = ['fixed', 'locked'];
451
        if (!$id
452
            || !$toggle
453
            || !$this->request->is('ajax')
454
            || !in_array($toggle, $allowed)
455
        ) {
456
            throw new BadRequestException;
457
        }
458
459
        $current = $this->Entries->toggle((int)$id, $toggle);
460
        if ($current) {
461
            $out['html'] = __d('nondynamic', $toggle . '_unset_entry_link');
0 ignored issues
show
Comprehensibility Best Practice introduced by
$out was never initialized. Although not strictly required by PHP, it is generally a good practice to add $out = array(); before regardless.
Loading history...
462
        } else {
463
            $out['html'] = __d('nondynamic', $toggle . '_set_entry_link');
464
        }
465
466
        $this->response = $this->response->withType('json');
467
        $this->response = $this->response->withStringBody(json_encode($out));
468
469
        return $this->response;
470
    }
471
472
    /**
473
     * {@inheritDoc}
474
     */
475
    public function beforeFilter(Event $event)
476
    {
477
        parent::beforeFilter($event);
478
        Stopwatch::start('Entries->beforeFilter()');
479
480
        $this->Security->setConfig(
481
            'unlockedActions',
482
            ['solve', 'view']
483
        );
484
        $this->Authentication->allowUnauthenticated(['index', 'view', 'mix', 'update']);
485
486
        $this->AuthUser->authorizeAction('ajaxToggle', 'saito.core.posting.pinAndLock');
487
        $this->AuthUser->authorizeAction('merge', 'saito.core.posting.merge');
488
        $this->AuthUser->authorizeAction('delete', 'saito.core.posting.delete');
489
490
        Stopwatch::stop('Entries->beforeFilter()');
491
    }
492
493
    /**
494
     * set view vars for category chooser
495
     *
496
     * @param CurrentUserInterface $User CurrentUser
497
     * @return void
498
     */
499
    protected function _setupCategoryChooser(CurrentUserInterface $User)
500
    {
501
        if (!$User->isLoggedIn()) {
502
            return;
503
        }
504
        $globalActivation = Configure::read(
505
            'Saito.Settings.category_chooser_global'
506
        );
507
        if (!$globalActivation) {
508
            if (!Configure::read(
509
                'Saito.Settings.category_chooser_user_override'
510
            )
511
            ) {
512
                return;
513
            }
514
            if (!$User->get('user_category_override')) {
515
                return;
516
            }
517
        }
518
519
        $this->set(
520
            'categoryChooserChecked',
521
            $User->getCategories()->getCustom('read')
522
        );
523
        switch ($User->getCategories()->getType()) {
524
            case 'single':
525
                $title = $User->get('user_category_active');
526
                break;
527
            case 'custom':
528
                $title = __('Custom');
529
                break;
530
            default:
531
                $title = __('All Categories');
532
        }
533
        $this->set('categoryChooserTitleId', $title);
534
        $this->set(
535
            'categoryChooser',
536
            $User->getCategories()->getAll('read', 'select')
537
        );
538
    }
539
540
    /**
541
     * Decide if an answering panel is show when rendering a posting
542
     *
543
     * @return void
544
     */
545
    protected function _showAnsweringPanel()
546
    {
547
        $showAnsweringPanel = false;
548
549
        if ($this->CurrentUser->isLoggedIn()) {
550
            // Only logged in users see the answering buttons if they …
551
            if (// … directly on entries/view but not inline
552
                ($this->request->getParam('action') === 'view' && !$this->request->is('ajax'))
553
                // … directly in entries/mix
554
                || $this->request->getParam('action') === 'mix'
555
                // … inline viewing … on entries/index.
556
                || ($this->Referer->wasController('entries')
557
                    && $this->Referer->wasAction('index'))
558
            ) {
559
                $showAnsweringPanel = true;
560
            }
561
        }
562
        $this->set('showAnsweringPanel', $showAnsweringPanel);
563
    }
564
565
    /**
566
     * makes root posting of $posting avaiable in view
567
     *
568
     * @param BasicPostingInterface $posting posting for root entry
569
     * @return void
570
     */
571
    protected function _setRootEntry(BasicPostingInterface $posting)
572
    {
573
        if (!$posting->isRoot()) {
574
            $root = $this->Entries->find()
575
                ->select(['user_id'])
576
                ->where(['id' => $posting->get('tid')])
577
                ->first();
578
        } else {
579
            $root = $posting;
580
        }
581
        $this->set('rootEntry', $root);
582
    }
583
}
584