Completed
Push — feature/6.x ( 5f23eb...e064bf )
by Schlaefer
03:44
created

EntriesController::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 5
rs 10
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * Saito - The Threaded Web Forum
6
 *
7
 * @copyright Copyright (c) the Saito Project Developers
8
 * @link https://github.com/Schlaefer/Saito
9
 * @license http://opensource.org/licenses/MIT
10
 */
11
12
namespace App\Controller;
13
14
use Cake\Core\Configure;
15
use Cake\Datasource\Exception\RecordNotFoundException;
16
use Cake\Http\Exception\BadRequestException;
17
use Cake\Http\Exception\NotFoundException;
18
use Saito\Exception\SaitoForbiddenException;
19
use Saito\Posting\Basic\BasicPostingInterface;
20
use Saito\User\CurrentUser\CurrentUserInterface;
21
use Saito\User\Permission\ResourceAI;
22
use Stopwatch\Lib\Stopwatch;
23
24
/**
25
 * Class EntriesController
26
 *
27
 * @property \Saito\User\CurrentUser\CurrentUserInterface $CurrentUser
28
 * @property \App\Model\Table\EntriesTable $Entries
29
 * @property \App\Controller\Component\MarkAsReadComponent $MarkAsRead
30
 * @property \App\Controller\Component\PostingComponent $Posting
31
 * @property \App\Controller\Component\RefererComponent $Referer
32
 * @property \App\Controller\Component\ThreadsComponent $Threads
33
 */
34
class EntriesController extends AppController
35
{
36
    /**
37
     * {@inheritDoc}
38
     */
39
    public function initialize(): void
40
    {
41
        parent::initialize();
42
43
        $this->loadComponent('Posting');
44
        $this->loadComponent('MarkAsRead');
45
        $this->loadComponent('Referer');
46
        $this->loadComponent('Threads', ['table' => $this->Entries]);
47
    }
48
49
    /**
50
     * posting index
51
     *
52
     * @return void|\Cake\Http\Response
53
     */
54
    public function index()
55
    {
56
        Stopwatch::start('Entries->index()');
57
58
        //= determine user sort order
59
        $sortKey = 'last_answer';
60
        if (!$this->CurrentUser->get('user_sort_last_answer')) {
61
            $sortKey = 'time';
62
        }
63
        $order = ['fixed' => 'DESC', $sortKey => 'DESC'];
64
65
        //= get threads
66
        $threads = $this->Threads->paginate($order, $this->CurrentUser);
67
        $this->set('entries', $threads);
68
69
        $currentPage = (int)$this->request->getQuery('page') ?: 1;
70
        if ($currentPage > 1) {
71
            $this->set('titleForLayout', __('page') . ' ' . $currentPage);
72
        }
73
        if ($currentPage === 1) {
74
            if ($this->MarkAsRead->refresh()) {
75
                return $this->redirect(['action' => 'index']);
76
            }
77
            $this->MarkAsRead->next();
78
        }
79
80
        // @bogus
81
        $this->request->getSession()->write('paginator.lastPage', $currentPage);
82
        $this->set('showDisclaimer', true);
83
        $this->set('showBottomNavigation', true);
84
        $this->set('allowThreadCollapse', true);
85
        $this->Slidetabs->show();
86
87
        $this->_setupCategoryChooser($this->CurrentUser);
88
89
        /** @var \App\Controller\Component\AutoReloadComponent $autoReload */
90
        $autoReload = $this->loadComponent('AutoReload');
91
        $autoReload->after($this->CurrentUser);
92
93
        Stopwatch::stop('Entries->index()');
94
    }
95
96
    /**
97
     * Mix view
98
     *
99
     * @param string $tid thread-ID
100
     * @return void|\Cake\Http\Response
101
     * @throws \Cake\Http\Exception\NotFoundException
102
     */
103
    public function mix($tid)
104
    {
105
        $tid = (int)$tid;
106
        if ($tid <= 0) {
107
            throw new BadRequestException();
108
        }
109
110
        try {
111
            $postings = $this->Entries->postingsForThread($tid, true, $this->CurrentUser);
112
        } catch (RecordNotFoundException $e) {
113
            /// redirect sub-posting to mix view of thread
114
            $actualTid = $this->Entries->getThreadId($tid);
115
116
            return $this->redirect([$actualTid, '#' => $tid], 301);
117
        }
118
119
        // check if anonymous tries to access internal categories
120
        $root = $postings;
121
        if (!$this->CurrentUser->getCategories()->permission('read', $root->get('category'))) {
122
            return $this->_requireAuth();
123
        }
124
125
        $this->_setRootEntry($root);
126
        $this->Title->setFromPosting($root, __('view.type.mix'));
127
128
        $this->set('showBottomNavigation', true);
129
        $this->set('entries', $postings);
130
131
        $this->_showAnsweringPanel();
132
133
        $this->Threads->incrementViewsForThread($root, $this->CurrentUser);
134
        $this->MarkAsRead->thread($postings);
135
    }
136
137
    /**
138
     * load front page force all entries mark-as-read
139
     *
140
     * @return void
141
     */
142
    public function update()
143
    {
144
        $this->autoRender = false;
145
        $this->CurrentUser->getLastRefresh()->set();
146
        $this->redirect('/entries/index');
147
    }
148
149
    /**
150
     * Outputs raw markup of an posting $id
151
     *
152
     * @param string $id posting-ID
153
     * @return void
154
     */
155
    public function source($id = null)
156
    {
157
        $this->viewBuilder()->enableAutoLayout(false);
158
        $this->view($id);
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type null; however, parameter $id of App\Controller\EntriesController::view() does only seem to accept string, 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

158
        $this->view(/** @scrutinizer ignore-type */ $id);
Loading history...
159
    }
160
161
    /**
162
     * View posting.
163
     *
164
     * @param string $id posting-ID
165
     * @return \Cake\Http\Response|void
166
     */
167
    public function view(string $id)
168
    {
169
        $id = (int)$id;
170
        Stopwatch::start('Entries->view()');
171
172
        $entry = $this->Entries->get($id);
173
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
174
175
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
176
            return $this->_requireAuth();
177
        }
178
179
        $this->set('entry', $posting);
180
        $this->Threads->incrementViewsForPosting($posting, $this->CurrentUser);
181
        $this->_setRootEntry($posting);
182
        $this->_showAnsweringPanel();
183
184
        $this->MarkAsRead->posting($posting);
185
186
        // inline open
187
        if ($this->request->is('ajax')) {
188
            return $this->render('/element/entry/view_posting');
189
        }
190
191
        // full page request
192
        $this->set(
193
            'tree',
194
            $this->Entries->postingsForThread($posting->get('tid'), false, $this->CurrentUser)
195
        );
196
        $this->Title->setFromPosting($posting);
197
198
        Stopwatch::stop('Entries->view()');
199
    }
200
201
    /**
202
     * Add new posting.
203
     *
204
     * @return void|\Cake\Http\Response
205
     */
206
    public function add()
207
    {
208
        $titleForPage = __('Write a New Posting');
209
        $this->set(compact('titleForPage'));
210
    }
211
212
    /**
213
     * Edit posting
214
     *
215
     * @param string $id posting-ID
216
     * @return void|\Cake\Http\Response
217
     * @throws \Cake\Http\Exception\NotFoundException
218
     * @throws \Cake\Http\Exception\BadRequestException
219
     */
220
    public function edit(string $id)
221
    {
222
        $id = (int)$id;
223
        $entry = $this->Entries->get($id);
224
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
225
226
        if (!$posting->isEditingAllowed()) {
227
            throw new SaitoForbiddenException(
228
                'Access to posting in EntriesController:edit() forbidden.',
229
                ['CurrentUser' => $this->CurrentUser]
230
            );
231
        }
232
233
        // show editing form
234
        if (!$posting->isEditingAsUserAllowed()) {
235
            $this->Flash->set(
236
                __('notice_you_are_editing_as_mod'),
237
                ['element' => 'warning']
238
            );
239
        }
240
241
        $this->set(compact('posting'));
242
243
        // set headers
244
        $this->set(
245
            'headerSubnavLeftTitle',
246
            __('back_to_posting_from_linkname', $posting->get('name'))
247
        );
248
        $this->set('headerSubnavLeftUrl', ['action' => 'view', $id]);
249
        $this->set('form_title', __('edit_linkname'));
250
        $this->render('/Entries/add');
251
    }
252
253
    /**
254
     * Get thread-line to insert after an inline-answer
255
     *
256
     * @param string $id posting-ID
257
     * @return void|\Cake\Http\Response
258
     */
259
    public function threadline($id = null)
260
    {
261
        $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

261
        $posting = $this->Entries->get(/** @scrutinizer ignore-type */ $id)->toPosting()->withCurrentUser($this->CurrentUser);
Loading history...
262
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
263
            return $this->_requireAuth();
264
        }
265
266
        $this->set('entrySub', $posting);
267
        // ajax requests so far are always answers
268
        $this->response = $this->response->withType('json');
269
        $this->set('level', '1');
270
    }
271
272
    /**
273
     * Delete posting
274
     *
275
     * @param string $id posting-ID
276
     * @return void
277
     * @throws \Cake\Http\Exception\NotFoundException
278
     * @throws \Cake\Http\Exception\MethodNotAllowedException
279
     */
280
    public function delete(string $id)
281
    {
282
        //$this->request->allowMethod(['post', 'delete']);
283
        $id = (int)$id;
284
        if (!$id) {
285
            throw new NotFoundException();
286
        }
287
        /** @var \App\Model\Entity\Entry $posting */
288
        $posting = $this->Entries->get($id);
289
290
        $action = $posting->isRoot() ? 'thread' : 'answer';
291
        $allowed = $this->CurrentUser->getCategories()
292
            ->permission($action, $posting->get('category_id'));
293
        if (!$allowed) {
294
            throw new SaitoForbiddenException();
295
        }
296
297
        $success = $this->Entries->deletePosting($id);
298
299
        if ($success) {
300
            $flashType = 'success';
301
            if ($posting->isRoot()) {
302
                $message = __('delete_tree_success');
303
                $redirect = '/';
304
            } else {
305
                $message = __('delete_subtree_success');
306
                $redirect = '/entries/view/' . $posting->get('pid');
307
            }
308
        } else {
309
            $flashType = 'error';
310
            $message = __('delete_tree_error');
311
            $redirect = $this->referer();
312
        }
313
        $this->Flash->set($message, ['element' => $flashType]);
314
        $this->redirect($redirect);
315
    }
316
317
    /**
318
     * Empty function for benchmarking
319
     *
320
     * @return void
321
     */
322
    public function e()
323
    {
324
        Stopwatch::start('Entries->e()');
325
        Stopwatch::stop('Entries->e()');
326
    }
327
328
    /**
329
     * Marks sub-entry $id as solution to its current root-entry
330
     *
331
     * @param string $id posting-ID
332
     * @return void
333
     * @throws \Cake\Http\Exception\BadRequestException
334
     */
335
    public function solve($id)
336
    {
337
        $this->autoRender = false;
338
        try {
339
            $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

339
            $posting = $this->Entries->get(/** @scrutinizer ignore-type */ $id);
Loading history...
340
341
            if (empty($posting)) {
342
                throw new \InvalidArgumentException('Posting to mark solved not found.');
343
            }
344
345
            $rootId = $posting->get('tid');
346
            $rootPosting = $this->Entries->get($rootId);
347
348
            $allowed = $this->CurrentUser->permission(
349
                'saito.core.posting.solves.set',
350
                (new ResourceAI())->onRole($rootPosting->get('user')->getRole())->onOwner($rootPosting->get('user_id'))
351
            );
352
            if (!$allowed) {
353
                throw new SaitoForbiddenException(
354
                    sprintf('Attempt to mark posting %s as solution.', $posting->get('id')),
355
                    ['CurrentUser' => $this->CurrentUser]
356
                );
357
            }
358
359
            $value = $posting->get('solves') ? 0 : $rootPosting->get('tid');
360
            $success = $this->Entries->updateEntry($posting, ['solves' => $value]);
361
362
            if (!$success) {
363
                throw new BadRequestException();
364
            }
365
        } catch (\Exception $e) {
366
            throw new BadRequestException();
367
        }
368
    }
369
370
    /**
371
     * Merge threads.
372
     *
373
     * @param string $sourceId posting-ID of thread to be merged
374
     * @return void
375
     * @throws \Cake\Http\Exception\NotFoundException
376
     * @td put into admin entries controller
377
     */
378
    public function merge(?string $sourceId = null)
379
    {
380
        $sourceId = (int)$sourceId;
381
        if (empty($sourceId)) {
382
            throw new NotFoundException();
383
        }
384
385
        $entry = $this->Entries->get($sourceId);
386
387
        if (!$entry->isRoot()) {
388
            throw new NotFoundException();
389
        }
390
391
        // perform move operation
392
        $targetId = $this->request->getData('targetId');
393
        if (!empty($targetId)) {
394
            if ($this->Entries->threadMerge($sourceId, $targetId)) {
0 ignored issues
show
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

394
            if ($this->Entries->threadMerge($sourceId, /** @scrutinizer ignore-type */ $targetId)) {
Loading history...
395
                $this->redirect('/entries/view/' . $sourceId);
396
397
                return;
398
            } else {
399
                $this->Flash->set(__('Error'), ['element' => 'error']);
400
            }
401
        }
402
403
        $this->viewBuilder()->setLayout('Admin.admin');
404
        $this->set('posting', $entry);
405
    }
406
407
    /**
408
     * Toggle posting property via ajax request.
409
     *
410
     * @param string $id posting-ID
411
     * @param string $toggle property
412
     *
413
     * @return \Cake\Http\Response
414
     */
415
    public function ajaxToggle($id = null, $toggle = null)
416
    {
417
        $allowed = ['fixed', 'locked'];
418
        if (
419
            !$id
420
            || !$toggle
421
            || !$this->request->is('ajax')
422
            || !in_array($toggle, $allowed)
423
        ) {
424
            throw new BadRequestException();
425
        }
426
427
        $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

427
        $posting = $this->Entries->get(/** @scrutinizer ignore-type */ $id);
Loading history...
428
        $data = ['id' => (int)$id, $toggle => !$posting->get($toggle)];
429
        $this->Posting->update($posting, $data, $this->CurrentUser);
430
431
        $this->response = $this->response->withType('json');
432
        $this->response = $this->response->withStringBody(json_encode('OK'));
433
434
        return $this->response;
435
    }
436
437
    /**
438
     * {@inheritDoc}
439
     */
440
    public function beforeFilter(\Cake\Event\EventInterface $event)
441
    {
442
        parent::beforeFilter($event);
443
        Stopwatch::start('Entries->beforeFilter()');
444
445
        $this->Security->setConfig(
446
            'unlockedActions',
447
            ['solve', 'view']
448
        );
449
        $this->Authentication->allowUnauthenticated(['index', 'view', 'mix', 'update']);
450
451
        $this->AuthUser->authorizeAction('ajaxToggle', 'saito.core.posting.pinAndLock');
452
        $this->AuthUser->authorizeAction('merge', 'saito.core.posting.merge');
453
        $this->AuthUser->authorizeAction('delete', 'saito.core.posting.delete');
454
455
        Stopwatch::stop('Entries->beforeFilter()');
456
    }
457
458
    /**
459
     * set view vars for category chooser
460
     *
461
     * @param \Saito\User\CurrentUser\CurrentUserInterface $User CurrentUser
462
     * @return void
463
     */
464
    protected function _setupCategoryChooser(CurrentUserInterface $User)
465
    {
466
        if (!$User->isLoggedIn()) {
467
            return;
468
        }
469
        $globalActivation = Configure::read(
470
            'Saito.Settings.category_chooser_global'
471
        );
472
        if (!$globalActivation) {
473
            if (
474
                !Configure::read(
475
                    'Saito.Settings.category_chooser_user_override'
476
                )
477
            ) {
478
                return;
479
            }
480
            if (!$User->get('user_category_override')) {
481
                return;
482
            }
483
        }
484
485
        $this->set(
486
            'categoryChooserChecked',
487
            $User->getCategories()->getCustom('read')
488
        );
489
        switch ($User->getCategories()->getType()) {
490
            case 'single':
491
                $title = $User->get('user_category_active');
492
                break;
493
            case 'custom':
494
                $title = __('Custom');
495
                break;
496
            default:
497
                $title = __('All Categories');
498
        }
499
        $this->set('categoryChooserTitleId', $title);
500
        $this->set(
501
            'categoryChooser',
502
            $User->getCategories()->getAll('read', 'select')
503
        );
504
    }
505
506
    /**
507
     * Decide if an answering panel is show when rendering a posting
508
     *
509
     * @return void
510
     */
511
    protected function _showAnsweringPanel()
512
    {
513
        $showAnsweringPanel = false;
514
515
        if ($this->CurrentUser->isLoggedIn()) {
516
            // Only logged in users see the answering buttons if they …
517
            if (
518
// … directly on entries/view but not inline
519
                ($this->request->getParam('action') === 'view' && !$this->request->is('ajax'))
520
                // … directly in entries/mix
521
                || $this->request->getParam('action') === 'mix'
522
                // … inline viewing … on entries/index.
523
                || ($this->Referer->wasController('entries')
524
                    && $this->Referer->wasAction('index'))
525
            ) {
526
                $showAnsweringPanel = true;
527
            }
528
        }
529
        $this->set('showAnsweringPanel', $showAnsweringPanel);
530
    }
531
532
    /**
533
     * makes root posting of $posting avaiable in view
534
     *
535
     * @param \Saito\Posting\Basic\BasicPostingInterface $posting posting for root entry
536
     * @return void
537
     */
538
    protected function _setRootEntry(BasicPostingInterface $posting)
539
    {
540
        if (!$posting->isRoot()) {
541
            $root = $this->Entries->find()
542
                ->select(['user_id', 'Users.user_type'])
543
                ->where(['Entries.id' => $posting->get('tid')])
544
                ->contain(['Users'])
545
                ->first();
546
        } else {
547
            $root = $posting;
548
        }
549
        $this->set('rootEntry', $root);
550
    }
551
}
552