• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lonnieezell / Bonfire2 / 31749763656

13 Aug 2026 10:23PM UTC coverage: 45.125%. Remained the same
31749763656

push

github

lonnieezell
style: apply php-cs-fixer

Pre-existing drift surfaced by the current php-cs-fixer; cosmetic only,
mostly docblock and union-type spacing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

0 of 1 new or added line in 1 file covered. (0.0%)

13 existing lines in 11 files now uncovered.

1592 of 3528 relevant lines covered (45.12%)

10.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.13
/src/Users/Controllers/UserController.php
1
<?php
2

3
/**
4
 * This file is part of Bonfire.
5
 *
6
 * (c) Lonnie Ezell <lonnieje@gmail.com>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11

12
namespace Bonfire\Users\Controllers;
13

14
use Bonfire\Core\AdminController;
15
use Bonfire\Users\Models\UserFilter;
16
use Bonfire\Users\Models\UserModel;
17
use Bonfire\Users\User;
18
use CodeIgniter\HTTP\RedirectResponse;
19
use CodeIgniter\Shield\Models\LoginModel;
20
use CodeIgniter\Shield\Models\UserIdentityModel;
21
use ReflectionException;
22

23
class UserController extends AdminController
24
{
25
    protected $theme      = 'Admin';
26
    protected $viewPrefix = 'Bonfire\Users\Views\\';
27

28
    /**
29
     * Display the uses currently in the system.
30
     *
31
     * @return RedirectResponse|string
32
     */
33
    public function list()
34
    {
35
        if (! auth()->user()->can('users.list')) {
1✔
36
            return redirect()->to(ADMIN_AREA)->with('error', lang('Bonfire.notAuthorized'));
×
37
        }
38

39
        /** @var UserFilter $userModel */
40
        $userModel = model(UserFilter::class);
1✔
41

42
        $userModel->filter($this->request->getGet('filters'))
1✔
43
            ->withPermissions()
1✔
44
            ->withIdentities()
1✔
45
            ->withGroups();
1✔
46

47
        $view = $this->request->hasHeader('HX-Request')
1✔
48
            ? $this->viewPrefix . '_table'
×
49
            : $this->viewPrefix . 'list';
1✔
50

51
        return $this->render($view, [
1✔
52
            'headers' => [
1✔
53
                'email'       => lang('Users.headers.email'),
1✔
54
                'username'    => lang('Users.headers.username'),
1✔
55
                'groups'      => lang('Users.headers.groups'),
1✔
56
                'last_active' => lang('Users.headers.last_active'),
1✔
57
            ],
1✔
58
            'showSelectAll' => true,
1✔
59
            'users'         => $userModel->paginate(setting('Site.perPage')),
1✔
60
            'pager'         => $userModel->pager,
1✔
61
        ]);
1✔
62
    }
63

64
    /**
65
     * Display the "new user" form.
66
     */
67
    public function create()
68
    {
69
        if (! auth()->user()->can('users.create')) {
×
70
            return redirect()->to(ADMIN_AREA . '/users')->with('error', lang('Bonfire.notAuthorized'));
×
71
        }
72

73
        $groups = setting('AuthGroups.groups');
×
74
        asort($groups);
×
75

76
        helper('form');
×
77

78
        return $this->render($this->viewPrefix . 'form', [
×
79
            'groups' => $groups,
×
80
        ]);
×
81
    }
82

83
    /**
84
     * Display the Edit form for a single user.
85
     *
86
     * @return RedirectResponse|string
87
     */
88
    public function edit(int $userId)
89
    {
90
        // check if it's the current user
91
        $itsMe = (auth()->user()->can('me.edit') || auth()->user()->can('me.security')) && auth()->id() === $userId;
×
92
        // check if the user should be granted access
93
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
94
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
95
        }
96

97
        $users = new UserModel();
×
98

99
        $user = $users->find($userId);
×
100
        if ($user === null) {
×
101
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
102
        }
103

104
        $groups = setting('AuthGroups.groups');
×
105
        asort($groups);
×
106

107
        helper('form');
×
108

109
        return $this->render($this->viewPrefix . 'form', [
×
110
            'user'   => $user,
×
111
            'groups' => $groups,
×
112
            'itsMe'  => $itsMe,
×
113
        ]);
×
114
    }
115

116
    /**
117
     * Creates or saves the basic user details.
118
     *
119
     * @return RedirectResponse|void
120
     *
121
     * @throws ReflectionException
122
     */
123
    public function save(?int $userId = null)
124
    {
125
        // check if it's the current user
126
        $itsMe = auth()->user()->can('me.edit') && auth()->id() === $userId;
×
127
        // check if the user should be permitted access
128
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
129
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
130
        }
131

132
        $users = new UserModel();
×
133
        /** @var User */
UNCOV
134
        $user = $userId !== null
×
135
            ? $users->find($userId)
×
136
            : new User();
×
137

138
        /** @phpstan-ignore-next-line */
139
        if ($user === null) {
×
140
            return redirect()->back()->withInput()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
141
        }
142

143
        /**
144
         * Perform validation here so we can merge the
145
         * basic model validation rules with the meta info rules.
146
         *
147
         * @var array
148
         */
149
        $rules = config('Users')->validation;
×
150
        $rules = array_merge($rules, $user->validationRules('meta'));
×
151

152
        if (! $this->validate($rules)) {
×
153
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
×
154
        }
155

156
        // Fill in basic details
157
        $user->fill($this->request->getPost());
×
158

159
        // Mark the user active if it is created by admin, or if it is marked active by admin
160
        if (
161
            $userId === null
×
162
            || (
163
                $user->isNotActivated()
×
164
                && auth()->user()->can('users.edit')
×
165
                && (int) $this->request->getPost('activate') === 1
×
166
            )
167
        ) {
168
            $user->active = 1;
×
169
        }
170

171
        // Limits on banning:
172
        // (1) Cannot ban oneself
173
        // (2) Only users who can manage admins can ban admins
174
        if (
175
            auth()->user()->can('users.edit')
×
176
            && ! $itsMe
×
177
            && (
178
                ! $user->inGroup('admin', 'superadmin')
×
179
                || auth()->user()->can('users.manage-admins')
×
180
            )
181
        ) {
182
            if ((int) $this->request->getPost('ban') === 1) {
×
183
                $user->ban($this->request->getPost('ban_reason'));
×
184
            } elseif ($user->isBanned() && (int) $this->request->getPost('ban') === 0) {
×
185
                $user->unBan();
×
186
            }
187
        }
188

189
        // Save basic details
190
        $users->save($user);
×
191

192
        // We need an ID to on the entity to save groups.
193
        if ($user->id === null) {
×
194
            $user->id = $users->getInsertID();
×
195
        }
196

197
        // Check for an avatar to upload
198
        if (($file = $this->request->getFile('avatar')) && $file->isValid()) {
×
199
            // Check if the avatar is to be resized
200
            $avatarResize     = setting('Users.avatarResize') ?? false;
×
201
            $maxDimension     = setting('Users.avatarSize') ?? 140;
×
202
            [$width, $height] = getimagesize($file->getPathname());
×
203
            if ($avatarResize && ($width > (int) $maxDimension || $height > (int) $maxDimension)) {
×
204
                $image = service('image')->withFile($file->getPathname());
×
205
                $image->resize($maxDimension, $maxDimension, true);
×
206
                $image->save();
×
207
            }
208
            $avatarDir = FCPATH . (setting('Users.avatarDirectory') ?? 'uploads/avatars');
×
209
            helper('text');
×
210
            $randomString = random_string('alnum', 5);
×
211
            $filename     = $user->id . '_' . $randomString . '.jpg';
×
212
            // Create if uploads/avatar directories not exist
213
            if (! is_dir($avatarDir)) {
×
214
                mkdir($avatarDir, 0755, true);
×
215
            }
216
            // delete the previous file if there is one in db & filesystem
217
            if ($user->avatar && file_exists($avatarDir . '/' . $user->avatar)) {
×
218
                @unlink($avatarDir . '/' . $user->avatar);
×
219
            }
220
            // move the uploaded file and update user object
221
            if ($file->move($avatarDir, $filename, true)) {
×
222
                $users->update($user->id, ['avatar' => $filename]);
×
223
            }
224
        }
225

226
        // Save the new user's email/password
227
        $password = $this->request->getPost('password');
×
228
        $identity = $user->getEmailIdentity();
×
229
        if ($identity === null) {
×
230
            helper('text');
×
231
            $user->createEmailIdentity([
×
232
                'email'    => $this->request->getPost('email'),
×
233
                'password' => empty($password) ? random_string('alnum', 12) : $password,
×
234
            ]);
×
235
        }
236
        // Update existing user's email identity
237
        else {
238
            $identity->secret = $this->request->getPost('email');
×
239
            if ($password !== null) {
×
240
                $identity->secret2 = service('passwords')->hash($password);
×
241
            }
242
            if ($identity->hasChanged()) {
×
243
                model(UserIdentityModel::class)->save($identity);
×
244
            }
245
        }
246

247
        // Save the user's groups if the user has right permissions
248
        if (auth()->user()->can('users.edit')) {
×
249
            $groups = $this->request->getPost('groups') ?? [];
×
250
            // omit previously unset admin groups if user performing changes
251
            // should not manage admins
252
            if (! auth()->user()->can('users.manage-admins')) {
×
253
                // prevent adding
254
                foreach ($groups as $key => $group) {
×
255
                    if (
256
                        ! $user->inGroup($group)
×
257
                        && in_array($group, ['admin', 'superadmin'], true)
×
258
                    ) {
259
                        unset($groups[$key]);
×
260
                    }
261
                }
262

263
                // prevent removing: return any removed admin role
264
                foreach ($user->getGroups() as $group) {
×
265
                    if (in_array($group, ['admin', 'superadmin'], true) && ! in_array($group, $groups, true)) {
×
266
                        $groups[] = $group;
×
267
                    }
268
                }
269
            }
270
            $user->syncGroups(...$groups);
×
271
        }
272

273
        // Save the user's meta fields
274
        $user->syncMeta($this->request->getPost('meta') ?? []);
×
275

276
        return redirect()->to($user->adminLink())->with('message', lang('Bonfire.resourceSaved', [lang('Users.user')]));
×
277
    }
278

279
    /**
280
     * Change user's password.
281
     *
282
     * @return RedirectResponse|void
283
     *
284
     * @throws ReflectionException
285
     */
286
    public function changePassword(?int $userId = null)
287
    {
288
        $itsMe = auth()->user()->can('me.security') && auth()->id() === $userId;
×
289
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
290
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
291
        }
292

293
        $users = new UserModel();
×
294
        /** @var User */
UNCOV
295
        $user = $userId !== null
×
296
            ? $users->find($userId)
×
297
            : new User();
×
298

299
        /** @phpstan-ignore-next-line */
300
        if ($user === null) {
×
301
            return redirect()->back()->withInput()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
302
        }
303

304
        if (! $this->validate(['password' => 'required|strong_password', 'pass_confirm' => 'required|matches[password]'])) {
×
305
            return redirect()->back()->withInput()->with('errors', service('validation')->getErrors());
×
306
        }
307

308
        // Save the new user's email/password
309
        $password = $this->request->getPost('password');
×
310
        $identity = $user->getEmailIdentity();
×
311

312
        if ($password !== null) {
×
313
            $identity->secret2 = service('passwords')->hash($password);
×
314
        }
315

316
        if ($identity->hasChanged()) {
×
317
            model(UserIdentityModel::class)->save($identity);
×
318
        }
319

320
        return redirect()->to($user->adminLink('/security'))->with('message', lang('Bonfire.resourceSaved', [lang('Users.user')]));
×
321
    }
322

323
    /**
324
     * Delete the specified user.
325
     *
326
     * @return RedirectResponse
327
     */
328
    public function delete(int $userId)
329
    {
330
        if (! auth()->user()->can('users.delete')) {
×
331
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
332
        }
333

334
        $users = model(UserModel::class);
×
335
        /** @var User|null $user */
336
        $user = $users->find($userId);
×
337

338
        if ($user === null) {
×
339
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
340
        }
341

342
        if (! $users->delete($user->id)) {
×
343
            log_message('error', implode(' ', $users->errors()));
×
344

345
            return redirect()->back()->with('error', lang('Bonfire.unknownError'));
×
346
        }
347

348
        return redirect()->back()->with('message', lang('Bonfire.resourceDeleted', [lang('Users.user')]));
×
349
    }
350

351
    /**
352
     * Deletes multiple users from the database.
353
     * Called via the checked() records in the table.
354
     */
355
    public function deleteBatch()
356
    {
357
        if (! auth()->user()->can('users.delete')) {
×
358
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
359
        }
360

361
        $ids = $this->request->getPost('selects');
×
362

363
        if (empty($ids)) {
×
364
            return redirect()->back()->with('error', lang('Bonfire.resourcesNotSelected', [lang('Users.users')]));
×
365
        }
366
        $ids = array_keys($ids);
×
367

368
        $users = model(UserModel::class);
×
369

370
        if (! $users->delete($ids)) {
×
371
            log_message('error', implode(' ', $users->errors()));
×
372

373
            return redirect()->back()->with('error', lang('Bonfire.unknownError'));
×
374
        }
375

376
        return redirect()->back()->with('message', lang('Bonfire.resourcesDeleted', [lang('Users.users')]));
×
377
    }
378

379
    /**
380
     * Displays basic security info, like previous login info,
381
     * and ability to force a password reset, ban, etc.
382
     *
383
     * @return RedirectResponse|string
384
     */
385
    public function security(int $userId)
386
    {
387
        $itsMe = auth()->user()->can('me.security') && auth()->id() === $userId;
×
388
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
389
            return redirect()->to(ADMIN_AREA)->with('error', lang('Bonfire.notAuthorized'));
×
390
        }
391

392
        $users = model(UserModel::class);
×
393
        /** @var User|null $user */
394
        $user = $users->find($userId);
×
395
        if ($user === null) {
×
396
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
397
        }
398

399
        /** @var LoginModel $loginModel */
400
        $loginModel = model(LoginModel::class);
×
401
        $logins     = $loginModel->where('identifier', $user->email)->orderBy('date', 'desc')->findAll(20);
×
402

403
        return $this->render($this->viewPrefix . 'security', [
×
404
            'user'   => $user,
×
405
            'logins' => $logins,
×
406
        ]);
×
407
    }
408

409
    /**
410
     * Displays basic security info, like previous login info,
411
     * and ability to force a password reset, ban, etc.
412
     *
413
     * @return RedirectResponse|string
414
     */
415
    public function permissions(int $userId)
416
    {
417
        if (! auth()->user()->can('users.view')) {
×
418
            return redirect()->to(ADMIN_AREA)->with('error', lang('Bonfire.notAuthorized'));
×
419
        }
420

421
        $users = model(UserModel::class);
×
422
        $user  = $users->find($userId);
×
423
        if ($user === null) {
×
424
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
425
        }
426

427
        $permissions = setting('AuthGroups.permissions');
×
428
        if (is_array($permissions)) {
×
429
            ksort($permissions);
×
430
        }
431

432
        return $this->render($this->viewPrefix . 'permissions', [
×
433
            'user'        => $user,
×
434
            'permissions' => $permissions,
×
435
        ]);
×
436
    }
437

438
    /**
439
     * Updates the permissions for a single user.
440
     *
441
     * @return RedirectResponse
442
     */
443
    public function savePermissions(int $userId)
444
    {
445
        if (! auth()->user()->can('users.edit')) {
×
446
            return redirect()->to(ADMIN_AREA)->with('error', lang('Bonfire.notAuthorized'));
×
447
        }
448

449
        $users = model(UserModel::class);
×
450
        /** @var User|null $user */
451
        $user = $users->find($userId);
×
452
        if ($user === null) {
×
453
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
454
        }
455

456
        $permissions = $this->request->getPost('permissions') ?? [];
×
457

458
        // if the administrator cannot manage admins, remove all user-management related permissions
459
        // unless they have been set previously
460
        if (! auth()->user()->can('users.manage-admins')) {
×
461
            foreach ($permissions as $key => $permission) {
×
462
                if (
463
                    ! $user->hasPermission($permission)
×
464
                    && explode('.', (string) $permission)[0] === 'users'
×
465
                ) {
466
                    unset($permissions[$key]);
×
467
                }
468
            }
469
        }
470

471
        $user->syncPermissions(...$permissions);
×
472

473
        return redirect()->back()->with('message', lang('Bonfire.resourceSaved', [lang('Users.permissions')]));
×
474
    }
475

476
    /**
477
     * Deletes user avatar on HTMX ajax request
478
     *
479
     * @return string
480
     */
481
    public function deleteAvatar(int $userId)
482
    {
483
        // check if it's the current user
484
        $itsMe = auth()->user()->can('me.edit') && auth()->id() === $userId;
×
485
        // check if the user should be permitted access
486

487
        $users = new UserModel();
×
488
        /** @var User */
UNCOV
489
        $user = $users->find($userId);
×
490

491
        if (auth()->user()->can('users.edit') || $itsMe) {
×
492
            $avatarDir = FCPATH . (setting('Users.avatarDirectory') ?? 'uploads/avatars');
×
493
            if ($user->avatar && file_exists($avatarDir . '/' . $user->avatar)) {
×
494
                @unlink($avatarDir . '/' . $user->avatar);
×
495
                $user->avatar = null;
×
496
                $users->save($user);
×
497
            }
498

499
            return $this->render($this->viewPrefix . '_avatar', ['user' => $user]);
×
500
        }
501

502
        // TODO: will have to find a way to return error message via ajax fragment later
503
        return '';
×
504
    }
505
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc