Completed
Branch feature/currentUserRefactoring (e6f778)
by Schlaefer
02:47
created

EntriesController::mix()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 33
rs 9.392
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\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\MethodNotAllowedException;
25
use Cake\Http\Exception\NotFoundException;
26
use Cake\Http\Response;
27
use Cake\Routing\RequestActionTrait;
28
use Saito\Exception\SaitoForbiddenException;
29
use Saito\Posting\Basic\BasicPostingInterface;
30
use Saito\User\CurrentUser\CurrentUserInterface;
31
use Stopwatch\Lib\Stopwatch;
32
33
/**
34
 * Class EntriesController
35
 *
36
 * @property CurrentUserInterface $CurrentUser
37
 * @property EntriesTable $Entries
38
 * @property MarkAsReadComponent $MarkAsRead
39
 * @property RefererComponent $Referer
40
 * @property ThreadsComponent $Threads
41
 */
42
class EntriesController extends AppController
43
{
44
    use RequestActionTrait;
0 ignored issues
show
Deprecated Code introduced by
The trait Cake\Routing\RequestActionTrait has been deprecated with message: 3.3.0 Use view cells instead.

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

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

Loading history...
45
46
    public $helpers = ['Posting', 'Text'];
47
48
    public $actionAuthConfig = [
49
        'ajaxToggle' => 'mod',
50
        'merge' => 'mod',
51
        'delete' => 'mod'
52
    ];
53
54
    /**
55
     * {@inheritDoc}
56
     */
57
    public function initialize()
58
    {
59
        parent::initialize();
60
61
        $this->loadComponent('MarkAsRead');
62
        $this->loadComponent('Referer');
63
        $this->loadComponent('Threads', ['table' => $this->Entries]);
64
    }
65
66
    /**
67
     * posting index
68
     *
69
     * @return void|\Cake\Network\Response
70
     */
71
    public function index()
72
    {
73
        Stopwatch::start('Entries->index()');
74
75
        //= determine user sort order
76
        $sortKey = 'last_answer';
77
        if (!$this->CurrentUser->get('user_sort_last_answer')) {
78
            $sortKey = 'time';
79
        }
80
        $order = ['fixed' => 'DESC', $sortKey => 'DESC'];
81
82
        //= get threads
83
        $threads = $this->Threads->paginate($order, $this->CurrentUser);
84
        $this->set('entries', $threads);
85
86
        $currentPage = (int)$this->request->getQuery('page') ?: 1;
87
        if ($currentPage > 1) {
88
            $this->set('titleForLayout', __('page') . ' ' . $currentPage);
89
        }
90
        if ($currentPage === 1) {
91
            if ($this->MarkAsRead->refresh()) {
92
                return $this->redirect(['action' => 'index']);
93
            }
94
            $this->MarkAsRead->next();
95
        }
96
97
        // @bogus
98
        $this->request->getSession()->write('paginator.lastPage', $currentPage);
99
        $this->set('showDisclaimer', true);
100
        $this->set('showBottomNavigation', true);
101
        $this->set('allowThreadCollapse', true);
102
        $this->Slidetabs->show();
103
104
        $this->_setupCategoryChooser($this->CurrentUser);
105
106
        /** @var AutoReloadComponent */
107
        $autoReload = $this->loadComponent('AutoReload');
108
        $autoReload->after($this->CurrentUser);
109
110
        Stopwatch::stop('Entries->index()');
111
    }
112
113
    /**
114
     * Mix view
115
     *
116
     * @param string $tid thread-ID
117
     * @return void|Response
118
     * @throws NotFoundException
119
     */
120
    public function mix($tid)
121
    {
122
        $tid = (int)$tid;
123
        if ($tid <= 0) {
124
            throw new BadRequestException();
125
        }
126
127
        try {
128
            $postings = $this->Entries->postingsForThread($tid, true, $this->CurrentUser);
129
        } catch (RecordNotFoundException $e) {
130
            /// redirect sub-posting to mix view of thread
131
            $actualTid = $this->Entries->getThreadId($tid);
132
133
            return $this->redirect([$actualTid, '#' => $tid], 301);
134
        }
135
136
        // check if anonymous tries to access internal categories
137
        $root = $postings;
138
        if (!$this->CurrentUser->getCategories()->permission('read', $root->get('category'))) {
139
            return $this->_requireAuth();
140
        }
141
142
        $this->_setRootEntry($root);
143
        $this->Title->setFromPosting($root, __('view.type.mix'));
0 ignored issues
show
Compatibility introduced by
$root of type object<Saito\Posting\PostingInterface> is not a sub-type of object<Saito\Posting\Posting>. It seems like you assume a concrete implementation of the interface Saito\Posting\PostingInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
144
145
        $this->set('showBottomNavigation', true);
146
        $this->set('entries', $postings);
147
148
        $this->_showAnsweringPanel();
149
150
        $this->Threads->incrementViewsForThread($root, $this->CurrentUser);
151
        $this->MarkAsRead->thread($postings);
152
    }
153
154
    /**
155
     * load front page force all entries mark-as-read
156
     *
157
     * @return void
158
     */
159
    public function update()
160
    {
161
        $this->autoRender = false;
162
        $this->CurrentUser->getLastRefresh()->set();
163
        $this->redirect('/entries/index');
164
    }
165
166
    /**
167
     * Outputs raw markup of an posting $id
168
     *
169
     * @param string $id posting-ID
170
     * @return void
171
     */
172
    public function source($id = null)
173
    {
174
        $this->viewBuilder()->enableAutoLayout(false);
175
        $this->view($id);
176
    }
177
178
    /**
179
     * View posting.
180
     *
181
     * @param string $id posting-ID
182
     * @return \Cake\Network\Response|void
183
     */
184
    public function view($id = null)
185
    {
186
        Stopwatch::start('Entries->view()');
187
188
        // redirect if no id is given
189
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
190
            $this->Flash->set(__('Invalid post'), ['element' => 'error']);
191
192
            return $this->redirect(['action' => 'index']);
193
        }
194
195
        $entry = $this->Entries->get($id);
196
197
        // redirect if posting doesn't exists
198
        if ($entry == false) {
199
            $this->Flash->set(__('Invalid post'));
200
201
            return $this->redirect('/');
202
        }
203
204
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
205
206
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
207
            return $this->_requireAuth();
208
        }
209
210
        $this->set('entry', $posting);
211
        $this->Threads->incrementViewsForPosting($posting, $this->CurrentUser);
212
        $this->_setRootEntry($posting);
213
        $this->_showAnsweringPanel();
214
215
        $this->MarkAsRead->posting($posting);
216
217
        // inline open
218
        if ($this->request->is('ajax')) {
219
            return $this->render('/Element/posting/view_posting');
220
        }
221
222
        // full page request
223
        $this->set(
224
            'tree',
225
            $this->Entries->postingsForThread($posting->get('tid'), false, $this->CurrentUser)
226
        );
227
        $this->Title->setFromPosting($posting);
228
229
        Stopwatch::stop('Entries->view()');
230
    }
231
232
    /**
233
     * Add new posting.
234
     *
235
     * @return void|\Cake\Network\Response
236
     */
237
    public function add()
238
    {
239
        $titleForPage = __('Write a New Posting');
240
        $this->set(compact('titleForPage'));
241
    }
242
243
    /**
244
     * Edit posting
245
     *
246
     * @param string $id posting-ID
247
     * @return void|\Cake\Network\Response
248
     * @throws NotFoundException
249
     * @throws BadRequestException
250
     */
251
    public function edit($id = null)
252
    {
253
        if (empty($id)) {
254
            throw new BadRequestException;
255
        }
256
257
        $entry = $this->Entries->get($id);
258
        if (empty($entry)) {
259
            throw new NotFoundException;
260
        }
261
        $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser);
262
263
        if (!$posting->isEditingAllowed()) {
264
            throw new SaitoForbiddenException(
265
                'Access to posting in EntriesController:edit() forbidden.',
266
                ['CurrentUser' => $this->CurrentUser]
267
            );
268
        }
269
270
        // show editing form
271
        if (!$posting->isEditingAsUserAllowed()) {
272
            $this->Flash->set(
273
                __('notice_you_are_editing_as_mod'),
274
                ['element' => 'warning']
275
            );
276
        }
277
278
        $this->set(compact('posting'));
279
280
        // set headers
281
        $this->set(
282
            'headerSubnavLeftTitle',
283
            __('back_to_posting_from_linkname', $posting->get('name'))
284
        );
285
        $this->set('headerSubnavLeftUrl', ['action' => 'view', $id]);
286
        $this->set('form_title', __('edit_linkname'));
287
        $this->render('/Entries/add');
288
    }
289
290
    /**
291
     * Get thread-line to insert after an inline-answer
292
     *
293
     * @param string $id posting-ID
294
     * @return void|\Cake\Network\Response
295
     */
296
    public function threadLine($id = null)
297
    {
298
        $posting = $this->Entries->get($id)->toPosting()->withCurrentUser($this->CurrentUser);
299
        if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) {
300
            return $this->_requireAuth();
301
        }
302
303
        $this->set('entrySub', $posting);
304
        // ajax requests so far are always answers
305
        $this->response = $this->response->withType('json');
306
        $this->set('level', '1');
307
    }
308
309
    /**
310
     * Delete posting
311
     *
312
     * @param string $id posting-ID
313
     * @return void
314
     * @throws NotFoundException
315
     * @throws MethodNotAllowedException
316
     */
317
    public function delete(string $id)
318
    {
319
        //$this->request->allowMethod(['post', 'delete']);
320
        $id = (int)$id;
321
        if (!$id) {
322
            throw new NotFoundException;
323
        }
324
        $posting = $this->Entries->get($id);
325
        if (!$posting) {
326
            throw new NotFoundException;
327
        }
328
329
        $success = $this->Entries->deletePosting($id);
330
331
        if ($success) {
332
            $flashType = 'success';
333
            if ($posting->isRoot()) {
334
                $message = __('delete_tree_success');
335
                $redirect = '/';
336
            } else {
337
                $message = __('delete_subtree_success');
338
                $redirect = '/entries/view/' . $posting->get('pid');
339
            }
340
        } else {
341
            $flashType = 'error';
342
            $message = __('delete_tree_error');
343
            $redirect = $this->referer();
344
        }
345
        $this->Flash->set($message, ['element' => $flashType]);
346
        $this->redirect($redirect);
347
    }
348
349
    /**
350
     * Empty function for benchmarking
351
     *
352
     * @return void
353
     */
354
    public function e()
355
    {
356
        Stopwatch::start('Entries->e()');
357
        Stopwatch::stop('Entries->e()');
358
    }
359
360
    /**
361
     * Marks sub-entry $id as solution to its current root-entry
362
     *
363
     * @param string $id posting-ID
364
     * @return void
365
     * @throws BadRequestException
366
     */
367
    public function solve($id)
368
    {
369
        $this->autoRender = false;
370
        try {
371
            $posting = $this->Entries->get($id);
372
373
            if (empty($posting)) {
374
                throw new \InvalidArgumentException('Posting to mark solved not found.');
375
            }
376
377
            if ($posting->isRoot()) {
378
                throw new \InvalidArgumentException('Root postings cannot mark themself solved.');
379
            }
380
381
            $rootId = $posting->get('tid');
382
            $rootPosting = $this->Entries->get($rootId);
383
            if ($rootPosting->get('user_id') !== $this->CurrentUser->getId()) {
384
                throw new SaitoForbiddenException(
385
                    sprintf('Attempt to mark posting %s as solution.', $posting->get('id')),
386
                    ['CurrentUser' => $this->CurrentUser]
387
                );
388
            }
389
390
            $success = $this->Entries->toggleSolve($posting);
0 ignored issues
show
Bug introduced by
It seems like $posting defined by $this->Entries->get($id) on line 371 can also be of type array; however, App\Model\Table\EntriesTable::toggleSolve() does only seem to accept object<App\Model\Entity\Entry>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
391
392
            if (!$success) {
393
                throw new BadRequestException;
394
            }
395
        } catch (\Exception $e) {
396
            throw new BadRequestException();
397
        }
398
    }
399
400
    /**
401
     * Merge threads.
402
     *
403
     * @param string $sourceId posting-ID of thread to be merged
404
     * @return void
405
     * @throws NotFoundException
406
     * @td put into admin entries controller
407
     */
408
    public function merge($sourceId = null)
409
    {
410
        if (!$sourceId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sourceId of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
411
            throw new NotFoundException();
412
        }
413
414
        /* @var Entry */
415
        $entry = $this->Entries->findById($sourceId)->first();
0 ignored issues
show
Documentation Bug introduced by
The method findById does not exist on object<App\Model\Table\EntriesTable>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
416
417
        if (!$entry || !$entry->isRoot()) {
418
            throw new NotFoundException();
419
        }
420
421
        // perform move operation
422
        $targetId = $this->request->getData('targetId');
423 View Code Duplication
        if (!empty($targetId)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
424
            if ($this->Entries->threadMerge($sourceId, $targetId)) {
0 ignored issues
show
Documentation introduced by
$targetId is of type array|string, but the function expects a integer.

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...
425
                $this->redirect('/entries/view/' . $sourceId);
426
427
                return;
428
            } else {
429
                $this->Flash->set(__('Error'), ['element' => 'error']);
430
            }
431
        }
432
433
        $this->viewBuilder()->setLayout('Admin.admin');
434
        $this->set('posting', $entry);
435
    }
436
437
    /**
438
     * Toggle posting property via ajax request.
439
     *
440
     * @param string $id posting-ID
441
     * @param string $toggle property
442
     *
443
     * @return \Cake\Network\Response
444
     */
445
    public function ajaxToggle($id = null, $toggle = null)
446
    {
447
        $allowed = ['fixed', 'locked'];
448
        if (!$id
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
449
            || !$toggle
0 ignored issues
show
Bug Best Practice introduced by
The expression $toggle of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
450
            || !$this->request->is('ajax')
451
            || !in_array($toggle, $allowed)
452
        ) {
453
            throw new BadRequestException;
454
        }
455
456
        $current = $this->Entries->toggle((int)$id, $toggle);
457
        if ($current) {
458
            $out['html'] = __d('nondynamic', $toggle . '_unset_entry_link');
0 ignored issues
show
Coding Style Comprehensibility introduced by
$out was never initialized. Although not strictly required by PHP, it is generally a good practice to add $out = 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...
459
        } else {
460
            $out['html'] = __d('nondynamic', $toggle . '_set_entry_link');
0 ignored issues
show
Coding Style Comprehensibility introduced by
$out was never initialized. Although not strictly required by PHP, it is generally a good practice to add $out = 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...
461
        }
462
463
        $this->response = $this->response->withType('json');
464
        $this->response = $this->response->withStringBody(json_encode($out));
465
466
        return $this->response;
467
    }
468
469
    /**
470
     * {@inheritDoc}
471
     */
472
    public function beforeFilter(Event $event)
473
    {
474
        parent::beforeFilter($event);
475
        Stopwatch::start('Entries->beforeFilter()');
476
477
        $this->Security->setConfig(
478
            'unlockedActions',
479
            ['solve', 'view']
480
        );
481
        $this->Authentication->allowUnauthenticated(['index', 'view', 'mix', 'update']);
482
483
        Stopwatch::stop('Entries->beforeFilter()');
484
    }
485
486
    /**
487
     * set view vars for category chooser
488
     *
489
     * @param CurrentUserInterface $User CurrentUser
490
     * @return void
491
     */
492
    protected function _setupCategoryChooser(CurrentUserInterface $User)
493
    {
494
        if (!$User->isLoggedIn()) {
495
            return;
496
        }
497
        $globalActivation = Configure::read(
498
            'Saito.Settings.category_chooser_global'
499
        );
500
        if (!$globalActivation) {
501
            if (!Configure::read(
502
                'Saito.Settings.category_chooser_user_override'
503
            )
504
            ) {
505
                return;
506
            }
507
            if (!$User->get('user_category_override')) {
508
                return;
509
            }
510
        }
511
512
        $this->set(
513
            'categoryChooserChecked',
514
            $User->getCategories()->getCustom('read')
515
        );
516
        switch ($User->getCategories()->getType()) {
517
            case 'single':
518
                $title = $User->get('user_category_active');
519
                break;
520
            case 'custom':
521
                $title = __('Custom');
522
                break;
523
            default:
524
                $title = __('All Categories');
525
        }
526
        $this->set('categoryChooserTitleId', $title);
527
        $this->set(
528
            'categoryChooser',
529
            $User->getCategories()->getAll('read', 'select')
530
        );
531
    }
532
533
    /**
534
     * Decide if an answering panel is show when rendering a posting
535
     *
536
     * @return void
537
     */
538
    protected function _showAnsweringPanel()
539
    {
540
        $showAnsweringPanel = false;
541
542
        if ($this->CurrentUser->isLoggedIn()) {
543
            // Only logged in users see the answering buttons if they …
544
            if (// … directly on entries/view but not inline
545
                ($this->request->getParam('action') === 'view' && !$this->request->is('ajax'))
546
                // … directly in entries/mix
547
                || $this->request->getParam('action') === 'mix'
548
                // … inline viewing … on entries/index.
549
                || ($this->Referer->wasController('entries')
550
                    && $this->Referer->wasAction('index'))
551
            ) {
552
                $showAnsweringPanel = true;
553
            }
554
        }
555
        $this->set('showAnsweringPanel', $showAnsweringPanel);
556
    }
557
558
    /**
559
     * makes root posting of $posting avaiable in view
560
     *
561
     * @param BasicPostingInterface $posting posting for root entry
562
     * @return void
563
     */
564
    protected function _setRootEntry(BasicPostingInterface $posting)
565
    {
566
        if (!$posting->isRoot()) {
567
            $root = $this->Entries->find()
568
                ->select(['user_id'])
569
                ->where(['id' => $posting->get('tid')])
570
                ->first();
571
        } else {
572
            $root = $posting;
573
        }
574
        $this->set('rootEntry', $root);
575
    }
576
}
577