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

codeigniter4 / shield / 10485595535

21 Aug 2024 07:37AM UTC coverage: 92.817% (+0.02%) from 92.798%
10485595535

push

github

web-flow
Merge pull request #1164 from kenjis/feat-command-user-create-g-option

feat: add -g option to `shield:user create`

19 of 19 new or added lines in 4 files covered. (100.0%)

1 existing line in 1 file now uncovered.

2791 of 3007 relevant lines covered (92.82%)

48.68 hits per line

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

95.0
/src/Models/UserModel.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter Shield.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Shield\Models;
15

16
use CodeIgniter\Database\Exceptions\DataException;
17
use CodeIgniter\I18n\Time;
18
use CodeIgniter\Shield\Authentication\Authenticators\Session;
19
use CodeIgniter\Shield\Entities\User;
20
use CodeIgniter\Shield\Entities\UserIdentity;
21
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
22
use CodeIgniter\Shield\Exceptions\LogicException;
23
use CodeIgniter\Shield\Exceptions\ValidationException;
24
use Faker\Generator;
25

26
/**
27
 * @phpstan-consistent-constructor
28
 */
29
class UserModel extends BaseModel
30
{
31
    protected $primaryKey     = 'id';
32
    protected $returnType     = User::class;
33
    protected $useSoftDeletes = true;
34
    protected $allowedFields  = [
35
        'username',
36
        'status',
37
        'status_message',
38
        'active',
39
        'last_active',
40
    ];
41
    protected $useTimestamps = true;
42
    protected $afterFind     = ['fetchIdentities'];
43
    protected $afterInsert   = ['saveEmailIdentity'];
44
    protected $afterUpdate   = ['saveEmailIdentity'];
45

46
    /**
47
     * Whether identity records should be included
48
     * when user records are fetched from the database.
49
     */
50
    protected bool $fetchIdentities = false;
51

52
    /**
53
     * Save the User for afterInsert and afterUpdate
54
     */
55
    protected ?User $tempUser = null;
56

57
    protected function initialize(): void
58
    {
59
        parent::initialize();
540✔
60

61
        $this->table = $this->tables['users'];
540✔
62
    }
63

64
    /**
65
     * Mark the next find* query to include identities
66
     *
67
     * @return $this
68
     */
69
    public function withIdentities(): self
70
    {
71
        $this->fetchIdentities = true;
10✔
72

73
        return $this;
10✔
74
    }
75

76
    /**
77
     * Populates identities for all records
78
     * returned from a find* method. Called
79
     * automatically when $this->fetchIdentities == true
80
     *
81
     * Model event callback called by `afterFind`.
82
     */
83
    protected function fetchIdentities(array $data): array
84
    {
85
        if (! $this->fetchIdentities) {
460✔
86
            return $data;
460✔
87
        }
88

89
        $userIds = $data['singleton']
10✔
90
            ? array_column($data, 'id')
4✔
91
            : array_column($data['data'], 'id');
6✔
92

93
        if ($userIds === []) {
10✔
94
            return $data;
4✔
95
        }
96

97
        /** @var UserIdentityModel $identityModel */
98
        $identityModel = model(UserIdentityModel::class);
6✔
99

100
        // Get our identities for all users
101
        $identities = $identityModel->getIdentitiesByUserIds($userIds);
6✔
102

103
        if (empty($identities)) {
6✔
104
            return $data;
×
105
        }
106

107
        $mappedUsers = $this->assignIdentities($data, $identities);
6✔
108

109
        $data['data'] = $data['singleton'] ? $mappedUsers[$data['id']] : $mappedUsers;
6✔
110

111
        return $data;
6✔
112
    }
113

114
    /**
115
     * Map our users by ID to make assigning simpler
116
     *
117
     * @param array              $data       Event $data
118
     * @param list<UserIdentity> $identities
119
     *
120
     * @return         list<User>              UserId => User object
121
     * @phpstan-return array<int|string, User> UserId => User object
122
     */
123
    private function assignIdentities(array $data, array $identities): array
124
    {
125
        $mappedUsers    = [];
6✔
126
        $userIdentities = [];
6✔
127

128
        $users = $data['singleton'] ? [$data['data']] : $data['data'];
6✔
129

130
        foreach ($users as $user) {
6✔
131
            $mappedUsers[$user->id] = $user;
6✔
132
        }
133
        unset($users);
6✔
134

135
        // Now group the identities by user
136
        foreach ($identities as $identity) {
6✔
137
            $userIdentities[$identity->user_id][] = $identity;
6✔
138
        }
139
        unset($identities);
6✔
140

141
        // Now assign the identities to the user
142
        foreach ($userIdentities as $userId => $identityArray) {
6✔
143
            $mappedUsers[$userId]->identities = $identityArray;
6✔
144
        }
145
        unset($userIdentities);
6✔
146

147
        return $mappedUsers;
6✔
148
    }
149

150
    /**
151
     * Adds a user to the default group.
152
     * Used during registration.
153
     */
154
    public function addToDefaultGroup(User $user): void
155
    {
156
        $defaultGroup = setting('AuthGroups.defaultGroup');
14✔
157

158
        /** @var GroupModel $groupModel */
159
        $groupModel = model(GroupModel::class);
14✔
160

161
        if (empty($defaultGroup) || ! $groupModel->isValidGroup($defaultGroup)) {
14✔
UNCOV
162
            throw new InvalidArgumentException(lang('Auth.unknownGroup', [$defaultGroup ?? '--not found--']));
×
163
        }
164

165
        $user->addGroup($defaultGroup);
14✔
166
    }
167

168
    public function fake(Generator &$faker): User
169
    {
170
        $this->checkReturnType();
446✔
171

172
        return new $this->returnType([
446✔
173
            'username' => $faker->unique()->userName(),
446✔
174
            'active'   => true,
446✔
175
        ]);
446✔
176
    }
177

178
    /**
179
     * Locates a User object by ID.
180
     *
181
     * @param int|string $id
182
     */
183
    public function findById($id): ?User
184
    {
185
        return $this->find($id);
166✔
186
    }
187

188
    /**
189
     * Locate a User object by the given credentials.
190
     *
191
     * @param array<string, string> $credentials
192
     */
193
    public function findByCredentials(array $credentials): ?User
194
    {
195
        // Email is stored in an identity so remove that here
196
        $email = $credentials['email'] ?? null;
94✔
197
        unset($credentials['email']);
94✔
198

199
        if ($email === null && $credentials === []) {
94✔
200
            return null;
2✔
201
        }
202

203
        // any of the credentials used should be case-insensitive
204
        foreach ($credentials as $key => $value) {
94✔
205
            $this->where(
6✔
206
                'LOWER(' . $this->db->protectIdentifiers($this->table . ".{$key}") . ')',
6✔
207
                strtolower($value)
6✔
208
            );
6✔
209
        }
210

211
        if ($email !== null) {
94✔
212
            /** @var array<string, int|string|null>|null $data */
213
            $data = $this->select(
88✔
214
                sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities'])
88✔
215
            )
88✔
216
                ->join($this->tables['identities'], sprintf('%1$s.user_id = %2$s.id', $this->tables['identities'], $this->table))
88✔
217
                ->where($this->tables['identities'] . '.type', Session::ID_TYPE_EMAIL_PASSWORD)
88✔
218
                ->where(
88✔
219
                    'LOWER(' . $this->db->protectIdentifiers($this->tables['identities'] . '.secret') . ')',
88✔
220
                    strtolower($email)
88✔
221
                )
88✔
222
                ->asArray()
88✔
223
                ->first();
88✔
224

225
            if ($data === null) {
88✔
226
                return null;
22✔
227
            }
228

229
            $email = $data['email'];
66✔
230
            unset($data['email']);
66✔
231
            $password_hash = $data['password_hash'];
66✔
232
            unset($data['password_hash']);
66✔
233

234
            $this->checkReturnType();
66✔
235

236
            $user                = new $this->returnType($data);
66✔
237
            $user->email         = $email;
66✔
238
            $user->password_hash = $password_hash;
66✔
239
            $user->syncOriginal();
66✔
240

241
            return $user;
66✔
242
        }
243

244
        return $this->first();
6✔
245
    }
246

247
    /**
248
     * Activate a User.
249
     */
250
    public function activate(User $user): void
251
    {
252
        $user->active = true;
×
253

254
        $this->save($user);
×
255
    }
256

257
    /**
258
     * Override the BaseModel's `insert()` method.
259
     * If you pass User object, also inserts Email Identity.
260
     *
261
     * @param array|User $row
262
     *
263
     * @return int|string|true Insert ID if $returnID is true
264
     *
265
     * @throws ValidationException
266
     */
267
    public function insert($row = null, bool $returnID = true)
268
    {
269
        // Clone User object for not changing the passed object.
270
        $this->tempUser = $row instanceof User ? clone $row : null;
458✔
271

272
        $result = parent::insert($row, $returnID);
458✔
273

274
        $this->checkQueryReturn($result);
458✔
275

276
        return $returnID ? $this->insertID : $result;
458✔
277
    }
278

279
    /**
280
     * Override the BaseModel's `update()` method.
281
     * If you pass User object, also updates Email Identity.
282
     *
283
     * @param array|int|string|null $id
284
     * @param array|User            $row
285
     *
286
     * @return true if the update is successful
287
     *
288
     * @throws ValidationException
289
     */
290
    public function update($id = null, $row = null): bool
291
    {
292
        // Clone User object for not changing the passed object.
293
        $this->tempUser = $row instanceof User ? clone $row : null;
60✔
294

295
        try {
296
            /** @throws DataException */
297
            $result = parent::update($id, $row);
60✔
298
        } catch (DataException $e) {
17✔
299
            // When $data is an array.
300
            if ($this->tempUser === null) {
17✔
301
                throw $e;
2✔
302
            }
303

304
            $messages = [
15✔
305
                lang('Database.emptyDataset', ['update']),
15✔
306
            ];
15✔
307

308
            if (in_array($e->getMessage(), $messages, true)) {
15✔
309
                $this->tempUser->saveEmailIdentity();
15✔
310

311
                return true;
15✔
312
            }
313

314
            throw $e;
×
315
        }
316

317
        $this->checkQueryReturn($result);
48✔
318

319
        return true;
48✔
320
    }
321

322
    /**
323
     * Override the BaseModel's `save()` method.
324
     * If you pass User object, also updates Email Identity.
325
     *
326
     * @param array|User $row
327
     *
328
     * @return true if the save is successful
329
     *
330
     * @throws ValidationException
331
     */
332
    public function save($row): bool
333
    {
334
        $result = parent::save($row);
74✔
335

336
        $this->checkQueryReturn($result);
72✔
337

338
        return true;
72✔
339
    }
340

341
    /**
342
     * Save Email Identity
343
     *
344
     * Model event callback called by `afterInsert` and `afterUpdate`.
345
     */
346
    protected function saveEmailIdentity(array $data): array
347
    {
348
        // If insert()/update() gets an array data, do nothing.
349
        if ($this->tempUser === null) {
458✔
350
            return $data;
20✔
351
        }
352

353
        // Insert
354
        if ($this->tempUser->id === null) {
456✔
355
            /** @var User $user */
356
            $user = $this->find($this->db->insertID());
442✔
357

358
            // If you get identity (email/password), the User object must have the id.
359
            $this->tempUser->id = $user->id;
442✔
360

361
            $user->email         = $this->tempUser->email ?? '';
442✔
362
            $user->password      = $this->tempUser->password ?? '';
442✔
363
            $user->password_hash = $this->tempUser->password_hash ?? '';
442✔
364

365
            $user->saveEmailIdentity();
442✔
366
            $this->tempUser = null;
442✔
367

368
            return $data;
442✔
369
        }
370

371
        // Update
372
        $this->tempUser->saveEmailIdentity();
51✔
373
        $this->tempUser = null;
51✔
374

375
        return $data;
51✔
376
    }
377

378
    /**
379
     * Updates the user's last active date.
380
     */
381
    public function updateActiveDate(User $user): void
382
    {
383
        assert($user->last_active instanceof Time);
384

385
        // Safe date string for database
386
        $last_active = $this->timeToDate($user->last_active);
26✔
387

388
        $this->builder()
26✔
389
            ->set('last_active', $last_active)
26✔
390
            ->where('id', $user->id)
26✔
391
            ->update();
26✔
392
    }
393

394
    private function checkReturnType(): void
395
    {
396
        if (! is_a($this->returnType, User::class, true)) {
464✔
397
            throw new LogicException('Return type must be a subclass of ' . User::class);
×
398
        }
399
    }
400
}
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