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

lonnieezell / Bonfire2 / 18263264420

05 Oct 2025 07:22PM UTC coverage: 45.14% (+0.05%) from 45.094%
18263264420

Pull #577

github

web-flow
Merge 0cc3faebc into 9641e0456
Pull Request #577: Rector suggested changes on consistent string handling

4 of 6 new or added lines in 6 files covered. (66.67%)

103 existing lines in 1 file now uncovered.

1593 of 3529 relevant lines covered (45.14%)

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✔
UNCOV
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
    {
UNCOV
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');
×
UNCOV
74
        asort($groups);
×
75

76
        helper('form');
×
77

UNCOV
78
        return $this->render($this->viewPrefix . 'form', [
×
UNCOV
79
            'groups' => $groups,
×
UNCOV
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
UNCOV
93
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
94
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
95
        }
96

97
        $users = new UserModel();
×
98

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

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

107
        helper('form');
×
108

109
        return $this->render($this->viewPrefix . 'form', [
×
110
            'user'   => $user,
×
UNCOV
111
            'groups' => $groups,
×
UNCOV
112
            'itsMe'  => $itsMe,
×
UNCOV
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
UNCOV
128
        if (! auth()->user()->can('users.edit') && ! $itsMe) {
×
129
            return redirect()->back()->with('error', lang('Bonfire.notAuthorized'));
×
130
        }
131

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

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

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

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

158
        // Fill in basic details
UNCOV
159
        $user->fill($this->request->getPost());
×
160

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

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

191
        // Save basic details
192
        $users->save($user);
×
193

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

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

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

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

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

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

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

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

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

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

UNCOV
308
        if (! $this->validate(['password' => 'required|strong_password', 'pass_confirm' => 'required|matches[password]'])) {
×
UNCOV
309
            return redirect()->back()->withInput()->with('errors', service('validation')->getErrors());
×
310
        }
311

312
        // Save the new user's email/password
313
        $password = $this->request->getPost('password');
×
314
        $identity = $user->getEmailIdentity();
×
315

UNCOV
316
        if ($password !== null) {
×
317
            $identity->secret2 = service('passwords')->hash($password);
×
318
        }
319

UNCOV
320
        if ($identity->hasChanged()) {
×
321
            model(UserIdentityModel::class)->save($identity);
×
322
        }
323

UNCOV
324
        return redirect()->to($user->adminLink('/security'))->with('message', lang('Bonfire.resourceSaved', [lang('Users.user')]));
×
325
    }
326

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

UNCOV
338
        $users = model(UserModel::class);
×
339
        /** @var User|null $user */
340
        $user = $users->find($userId);
×
341

UNCOV
342
        if ($user === null) {
×
343
            return redirect()->back()->with('error', lang('Bonfire.resourceNotFound', [lang('Users.userGenitive')]));
×
344
        }
345

346
        if (! $users->delete($user->id)) {
×
UNCOV
347
            log_message('error', implode(' ', $users->errors()));
×
348

349
            return redirect()->back()->with('error', lang('Bonfire.unknownError'));
×
350
        }
351

UNCOV
352
        return redirect()->back()->with('message', lang('Bonfire.resourceDeleted', [lang('Users.user')]));
×
353
    }
354

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

365
        $ids = $this->request->getPost('selects');
×
366

367
        if (empty($ids)) {
×
UNCOV
368
            return redirect()->back()->with('error', lang('Bonfire.resourcesNotSelected', [lang('Users.users')]));
×
369
        }
UNCOV
370
        $ids = array_keys($ids);
×
371

372
        $users = model(UserModel::class);
×
373

374
        if (! $users->delete($ids)) {
×
UNCOV
375
            log_message('error', implode(' ', $users->errors()));
×
376

377
            return redirect()->back()->with('error', lang('Bonfire.unknownError'));
×
378
        }
379

UNCOV
380
        return redirect()->back()->with('message', lang('Bonfire.resourcesDeleted', [lang('Users.users')]));
×
381
    }
382

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

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

403
        /** @var LoginModel $loginModel */
404
        $loginModel = model(LoginModel::class);
×
405
        $logins     = $loginModel->where('identifier', $user->email)->orderBy('date', 'desc')->findAll(20);
×
406

407
        return $this->render($this->viewPrefix . 'security', [
×
UNCOV
408
            'user'   => $user,
×
UNCOV
409
            'logins' => $logins,
×
UNCOV
410
        ]);
×
411
    }
412

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

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

UNCOV
431
        $permissions = setting('AuthGroups.permissions');
×
UNCOV
432
        if (is_array($permissions)) {
×
433
            ksort($permissions);
×
434
        }
435

436
        return $this->render($this->viewPrefix . 'permissions', [
×
UNCOV
437
            'user'        => $user,
×
UNCOV
438
            'permissions' => $permissions,
×
UNCOV
439
        ]);
×
440
    }
441

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

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

UNCOV
460
        $permissions = $this->request->getPost('permissions') ?? [];
×
461

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

UNCOV
475
        $user->syncPermissions(...$permissions);
×
476

UNCOV
477
        return redirect()->back()->with('message', lang('Bonfire.resourceSaved', [lang('Users.permissions')]));
×
478
    }
479

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

UNCOV
491
        $users = new UserModel();
×
492
        /**
493
         * @var User
494
         */
495
        $user = $users->find($userId);
×
496

497
        if (auth()->user()->can('users.edit') || $itsMe) {
×
498
            $avatarDir = FCPATH . (setting('Users.avatarDirectory') ?? 'uploads/avatars');
×
499
            if ($user->avatar && file_exists($avatarDir . '/' . $user->avatar)) {
×
UNCOV
500
                @unlink($avatarDir . '/' . $user->avatar);
×
UNCOV
501
                $user->avatar = null;
×
502
                $users->save($user);
×
503
            }
504

UNCOV
505
            return $this->render($this->viewPrefix . '_avatar', ['user' => $user]);
×
506
        }
507

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

© 2025 Coveralls, Inc