Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.24% covered (danger)
20.24%
17 / 84
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
UsersController
20.24% covered (danger)
20.24%
17 / 84
14.29% covered (danger)
14.29%
1 / 7
222.98
0.00% covered (danger)
0.00%
0 / 1
 beforeFilter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 index
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 view
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 edit
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
7.51
 delete
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 logout
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare(strict_types=1);
3
4namespace App\Controller\Admin;
5
6use App\Controller\AppController;
7use App\Model\Entity\User;
8use Cake\Event\EventInterface;
9use Cake\Http\Response;
10
11/**
12 * Users Controller
13 *
14 * Manages user-related actions such as listing, viewing, adding, editing, and deleting users.
15 *
16 * @property \App\Model\Table\UsersTable $Users
17 */
18class UsersController extends AppController
19{
20    /**
21     * Configures actions that can be accessed without authentication.
22     *
23     * @param \Cake\Event\EventInterface $event The event object.
24     * @return void
25     */
26    public function beforeFilter(EventInterface $event): void
27    {
28        parent::beforeFilter($event);
29        $this->Authentication->allowUnauthenticated(['login']);
30    }
31
32    /**
33     * Lists users and handles search functionality.
34     *
35     * Processes both standard and AJAX requests for listing users.
36     * Paginates user data for standard requests and performs search for AJAX requests.
37     *
38     * @return \Cake\Http\Response|null Returns a response for AJAX requests, null otherwise.
39     */
40    public function index(): ?Response
41    {
42        $statusFilter = $this->request->getQuery('status');
43        $query = $this->Users->find()
44            ->select([
45                'Users.id',
46                'Users.username',
47                'Users.email',
48                'Users.is_admin',
49                'Users.active',
50                'Users.created',
51                'Users.modified',
52                'Users.image',
53                'Users.dir',
54            ]);
55
56        if ($statusFilter !== null) {
57            $query->where(['Users.active' => (int)$statusFilter]);
58        }
59
60        $search = $this->request->getQuery('search');
61        if (!empty($search)) {
62            $query->where([
63                'OR' => [
64                    'Users.username LIKE' => '%' . $search . '%',
65                    'Users.email LIKE' => '%' . $search . '%',
66                ],
67            ]);
68        }
69        $users = $this->paginate($query);
70        if ($this->request->is('ajax')) {
71            $this->set(compact('users'));
72            $this->viewBuilder()->setLayout('ajax');
73
74            return $this->render('search_results');
75        }
76
77        $this->set(compact('users'));
78
79        return null;
80    }
81
82    /**
83     * Displays details of a specific user.
84     *
85     * @param string|null $id User id.
86     * @return void
87     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
88     */
89    public function view(?string $id = null): void
90    {
91        $user = $this->Users->get($id, contain: ['Articles', 'Comments.Articles']);
92        $this->set(compact('user'));
93    }
94
95    /**
96     * Adds a new user.
97     *
98     * @return \Cake\Http\Response|null Redirects on successful add, null otherwise.
99     */
100    public function add(): ?Response
101    {
102        $user = $this->Users->newEmptyEntity();
103        if ($this->request->is('post')) {
104            $user->setAccess('is_admin', true);
105            $user->setAccess('active', true);
106            $user = $this->Users->patchEntity($user, $this->request->getData());
107            if ($this->Users->save($user)) {
108                $this->Flash->success(__('The user has been saved.'));
109
110                return $this->redirect(['action' => 'index']);
111            }
112            $this->Flash->error(__('The user could not be saved. Please, try again.'));
113        }
114        $this->set(compact('user'));
115
116        return null;
117    }
118
119    /**
120     * Edits user information.
121     *
122     * Handles updating of user details with security checks to prevent self-locking their account.
123     *
124     * @param string|null $id The ID of the user to edit.
125     * @return \Cake\Http\Response|null Redirects on successful edit, null otherwise.
126     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
127     */
128    public function edit(?string $id = null): ?Response
129    {
130        $user = $this->Users->get($id, contain: []);
131        $identity = $this->Authentication->getIdentity();
132        $originalData = $identity->getOriginalData();
133        $currentUserId = $originalData instanceof User
134            ? $originalData->id
135            : ($originalData['id'] ?? null);
136
137        if ($this->request->is(['patch', 'post', 'put'])) {
138            $user->setAccess('is_admin', true);
139            $user->setAccess('active', true);
140
141            $data = $this->request->getData();
142
143            // Prevent changing own admin status
144            if ($user->lockAdminAccountError($currentUserId, $data)) {
145                $this->Flash->error(__('You cannot remove your own admin status.'));
146
147                return $this->redirect(['action' => 'edit', $id]);
148            }
149            //prevent disabling own account
150            if ($user->lockEnabledAccountError($currentUserId, $data)) {
151                $this->Flash->error(__('You cannot disable your own account.'));
152
153                return $this->redirect(['action' => 'edit', $id]);
154            }
155
156            $user = $this->Users->patchEntity($user, $data);
157            if ($this->Users->save($user)) {
158                $this->Flash->success(__('The user has been saved.'));
159
160                return $this->redirect(['action' => 'index']);
161            }
162            $this->Flash->error(__('The user could not be saved. Please, try again.'));
163        }
164        $this->set(compact('user'));
165
166        return null;
167    }
168
169    /**
170     * Deletes a user.
171     *
172     * @param string|null $id User id.
173     * @return \Cake\Http\Response Redirects to index.
174     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
175     * @throws \Cake\Http\Exception\MethodNotAllowedException When invalid method is used.
176     */
177    public function delete(?string $id = null): Response
178    {
179        $this->request->allowMethod(['post', 'delete']);
180        $user = $this->Users->get($id);
181        $identity = $this->Authentication->getIdentity();
182        $originalData = $identity->getOriginalData();
183        $currentUserId = $originalData instanceof User
184            ? $originalData->id
185            : ($originalData['id'] ?? null);
186        if ($currentUserId == $user->id) {
187            $this->Flash->error(__('No deleting your own account.'));
188
189            return $this->redirect(['action' => 'index']);
190        }
191        if ($this->Users->delete($user)) {
192            $this->Flash->success(__('The user has been deleted.'));
193        } else {
194            $this->Flash->error(__('The user could not be deleted. Please, try again.'));
195        }
196
197        return $this->redirect(['action' => 'index']);
198    }
199
200    /**
201     * Logs out the current user.
202     *
203     * @return \Cake\Http\Response|null Redirects to the login page.
204     */
205    public function logout(): ?Response
206    {
207        $this->Authentication->logout();
208
209        return $this->redirect('/');
210    }
211}