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

lonnieezell / Bonfire2 / 37825532472

08 Oct 2026 06:35PM UTC coverage: 45.132% (+0.007%) from 45.125%
37825532472

push

github

web-flow
style: apply php-cs-fixer 3.95.27 and rector 2.7 fixes (#591)

Newer tool releases flag code that develop's last CI run (Aug 2026)
passed: isset/null guards become ??=, a null ternary becomes ?->, and
one array alignment space is removed. No behaviour change.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

2 of 6 new or added lines in 5 files covered. (33.33%)

1590 of 3523 relevant lines covered (45.13%)

10.64 hits per line

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

9.18
/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 */
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.
NEW
193
        $user->id ??= $users->getInsertID();
×
194

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

359
        $ids = $this->request->getPost('selects');
×
360

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

366
        $users = model(UserModel::class);
×
367

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

469
        $user->syncPermissions(...$permissions);
×
470

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

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

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

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

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

500
        // TODO: will have to find a way to return error message via ajax fragment later
501
        return '';
×
502
    }
503
}
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