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

daycry / auth / 21478354896

23 Jan 2026 11:45AM UTC coverage: 66.864%. Remained the same
21478354896

push

github

daycry
Update dependencies and Shield version

Replaced 'daycry/cronjob' with 'daycry/jobs' in composer.json and updated SHIELD_VERSION in Auth.php from 3.0.2 to 3.0.5.

2256 of 3374 relevant lines covered (66.86%)

33.12 hits per line

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

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

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
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 Daycry\Auth\Models;
15

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

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

50
    /**
51
     * Whether identity records should be included
52
     * when user records are fetched from the database.
53
     */
54
    protected bool $fetchIdentities = false;
55

56
    /**
57
     * Save the User for afterInsert and afterUpdate
58
     */
59
    protected ?User $tempUser = null;
60

61
    protected function initialize(): void
62
    {
63
        parent::initialize();
202✔
64

65
        $this->table = $this->tables['users'];
202✔
66
    }
67

68
    /**
69
     * Mark the next find* query to include identities
70
     *
71
     * @return $this
72
     */
73
    public function withIdentities(): self
74
    {
75
        $this->fetchIdentities = true;
4✔
76

77
        return $this;
4✔
78
    }
79

80
    /**
81
     * Populates identities for all records
82
     * returned from a find* method. Called
83
     * automatically when $this->fetchIdentities == true
84
     *
85
     * Model event callback called by `afterFind`.
86
     */
87
    protected function fetchIdentities(array $data): array
88
    {
89
        if (! $this->fetchIdentities || empty($data['data'])) {
154✔
90
            return $data;
154✔
91
        }
92

93
        $userIds = $data['singleton']
2✔
94
            ? [$data['data']->id]
1✔
95
            : array_column($data['data'], 'id');
1✔
96

97
        if ($userIds === []) {
2✔
98
            return $data;
×
99
        }
100

101
        /** @var UserIdentityModel $identityModel */
102
        $identityModel = model(UserIdentityModel::class);
2✔
103

104
        // Get our identities for all users
105
        $identities = $identityModel->getIdentitiesByUserIds($userIds);
2✔
106

107
        if (empty($identities)) {
2✔
108
            return $data;
×
109
        }
110

111
        $mappedUsers = $this->assignIdentities($data, $identities);
2✔
112

113
        $data['data'] = $data['singleton'] ? $mappedUsers[array_column($data, 'id')[0]] : $mappedUsers;
2✔
114

115
        return $data;
2✔
116
    }
117

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

132
        $users = $data['singleton'] ? [$data['data']] : $data['data'];
2✔
133

134
        foreach ($users as $user) {
2✔
135
            $mappedUsers[$user->id] = $user;
2✔
136
        }
137
        unset($users);
2✔
138

139
        // Now group the identities by user
140
        foreach ($identities as $identity) {
2✔
141
            $userIdentities[$identity->user_id][] = $identity;
2✔
142
        }
143
        unset($identities);
2✔
144

145
        // Now assign the identities to the user
146
        foreach ($userIdentities as $userId => $identityArray) {
2✔
147
            $mappedUsers[$userId]->identities = $identityArray;
2✔
148
        }
149
        unset($userIdentities);
2✔
150

151
        return $mappedUsers;
2✔
152
    }
153

154
    /**
155
     * Activate a User.
156
     */
157
    public function activate(User $user): void
158
    {
159
        $user->active = true;
×
160

161
        $this->save($user);
×
162
    }
163

164
    /**
165
     * Override the BaseModel's `insert()` method.
166
     * If you pass User object, also inserts Email Identity.
167
     *
168
     * @param array|User $data
169
     *
170
     * @return int|string|true Insert ID if $returnID is true
171
     *
172
     * @throws ValidationException
173
     */
174
    public function insert($data = null, bool $returnID = true)
175
    {
176
        // Clone User object for not changing the passed object.
177
        $this->tempUser = $data instanceof User ? clone $data : null;
156✔
178

179
        $result = parent::insert($data, $returnID);
156✔
180

181
        $this->checkQueryReturn($result);
156✔
182

183
        return $returnID ? $this->insertID : $result;
156✔
184
    }
185

186
    /**
187
     * Override the BaseModel's `update()` method.
188
     * If you pass User object, also updates Email Identity.
189
     *
190
     * @param array|int|string|null $id
191
     * @param array|User            $data
192
     *
193
     * @return true if the update is successful
194
     *
195
     * @throws ValidationException
196
     */
197
    public function update($id = null, $data = null): bool
198
    {
199
        // Clone User object for not changing the passed object.
200
        $this->tempUser = $data instanceof User ? clone $data : null;
29✔
201

202
        try {
203
            /** @throws DataException */
204
            $result = parent::update($id, $data);
29✔
205
        } catch (DataException $e) {
10✔
206
            // When $data is an array.
207
            if ($this->tempUser === null) {
10✔
208
                throw $e;
1✔
209
            }
210

211
            $messages = [
9✔
212
                lang('Database.emptyDataset', ['update']),
9✔
213
            ];
9✔
214

215
            if (in_array($e->getMessage(), $messages, true)) {
9✔
216
                $this->saveEmailIdentity([]);
9✔
217

218
                return true;
9✔
219
            }
220

221
            throw $e;
×
222
        }
223

224
        $this->checkQueryReturn($result);
22✔
225

226
        return true;
22✔
227
    }
228

229
    /**
230
     * Override the BaseModel's `save()` method.
231
     * If you pass User object, also updates Email Identity.
232
     *
233
     * @param array|User $data
234
     *
235
     * @return true if the save is successful
236
     *
237
     * @throws ValidationException
238
     */
239
    public function save($data): bool
240
    {
241
        $result = parent::save($data);
33✔
242

243
        $this->checkQueryReturn($result);
32✔
244

245
        return true;
32✔
246
    }
247

248
    /**
249
     * Save Email Identity
250
     *
251
     * Model event callback called by `afterInsert` and `afterUpdate`.
252
     */
253
    protected function saveEmailIdentity(array $data): array
254
    {
255
        // If insert()/update() gets an array data, do nothing.
256
        if ($this->tempUser === null) {
156✔
257
            return $data;
7✔
258
        }
259

260
        /** @var User $user */
261
        $user = $this->tempUser;
155✔
262

263
        if ($user->id === null) {
155✔
264
            $user->id = $data['id'] ?? $this->db->insertID();
150✔
265
        }
266

267
        $email        = $user->getEmail();
155✔
268
        $password     = $user->getPassword();
155✔
269
        $passwordHash = $user->getPasswordHash();
155✔
270

271
        if (($email === null || $email === '' || $email === '0') && ($password === null || $password === '' || $password === '0') && ($passwordHash === null || $passwordHash === '' || $passwordHash === '0')) {
155✔
272
            $this->tempUser = null;
144✔
273

274
            return $data;
144✔
275
        }
276

277
        /** @var UserIdentityModel $identityModel */
278
        $identityModel = model(UserIdentityModel::class);
21✔
279
        $identity      = $identityModel->getIdentityByType($user, Session::ID_TYPE_EMAIL_PASSWORD);
21✔
280

281
        if ($identity === null) {
21✔
282
            $identityModel->createEmailIdentity($user, [
15✔
283
                'email'    => $email,
15✔
284
                'password' => '',
15✔
285
            ]);
15✔
286

287
            $identity = $identityModel->getIdentityByType($user, Session::ID_TYPE_EMAIL_PASSWORD);
15✔
288
        }
289

290
        if ($email !== null && $email !== '' && $email !== '0') {
21✔
291
            $identity->secret = $email;
21✔
292
        }
293

294
        if ($password !== null && $password !== '' && $password !== '0') {
21✔
295
            $identity->secret2 = service('passwords')->hash($password);
14✔
296
        }
297

298
        if ($passwordHash !== null && $passwordHash !== '' && $passwordHash !== '0' && ($password === null || $password === '' || $password === '0')) {
21✔
299
            $identity->secret2 = $passwordHash;
10✔
300
        }
301

302
        try {
303
            /** @throws DataException */
304
            $identityModel->save($identity);
21✔
305
        } catch (DataException $e) {
4✔
306
            // There may be no data to update.
307
            $messages = [
4✔
308
                lang('Database.emptyDataset', ['insert']),
4✔
309
                lang('Database.emptyDataset', ['update']),
4✔
310
            ];
4✔
311
            if (! in_array($e->getMessage(), $messages, true)) {
4✔
312
                throw $e;
×
313
            }
314
        }
315

316
        $this->tempUser = null;
21✔
317

318
        return $data;
21✔
319
    }
320

321
    /**
322
     * Adds a user to the default group.
323
     * Used during registration.
324
     */
325
    public function addToDefaultGroup(User $user): void
326
    {
327
        $defaultGroup = setting('Auth.defaultGroup');
1✔
328

329
        $rows = model(GroupModel::class)->findAll();
1✔
330

331
        $allowedGroups = array_column($rows, 'name');
1✔
332

333
        if (empty($defaultGroup) || ! in_array($defaultGroup, $allowedGroups, true)) {
1✔
334
            throw new InvalidArgumentException(lang('Auth.unknownGroup', [$defaultGroup ?? '--not found--']));
×
335
        }
336

337
        $user->addGroup($defaultGroup);
1✔
338
    }
339

340
    public function fake(Generator &$faker): User
341
    {
342
        return new User([
143✔
343
            'username' => $faker->unique()->userName(),
143✔
344
            'active'   => true,
143✔
345
        ]);
143✔
346
    }
347

348
    /**
349
     * Locates a User object by ID.
350
     *
351
     * @param int|string $id
352
     */
353
    public function findById($id): ?User
354
    {
355
        return $this->find($id);
54✔
356
    }
357

358
    /**
359
     * Locate a User object by the given credentials.
360
     *
361
     * @param array<string, string> $credentials
362
     */
363
    public function findByCredentials(array $credentials): ?User
364
    {
365
        // Email is stored in an identity so remove that here
366
        $email = $credentials['email'] ?? null;
38✔
367
        unset($credentials['email']);
38✔
368

369
        if ($email === null && $credentials === []) {
38✔
370
            return null;
1✔
371
        }
372

373
        // any of the credentials used should be case-insensitive
374
        foreach ($credentials as $key => $value) {
38✔
375
            $this->where(
2✔
376
                'LOWER(' . $this->db->protectIdentifiers($this->table . ".{$key}") . ')',
2✔
377
                strtolower($value),
2✔
378
            );
2✔
379
        }
380

381
        if ($email !== null) {
38✔
382
            /** @var array<string, int|string|null>|null $data */
383
            $data = $this->select(
36✔
384
                sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities']),
36✔
385
            )
36✔
386
                ->join($this->tables['identities'], sprintf('%1$s.user_id = %2$s.id', $this->tables['identities'], $this->table))
36✔
387
                ->where($this->tables['identities'] . '.type', Session::ID_TYPE_EMAIL_PASSWORD)
36✔
388
                ->where(
36✔
389
                    'LOWER(' . $this->db->protectIdentifiers($this->tables['identities'] . '.secret') . ')',
36✔
390
                    strtolower($email),
36✔
391
                )
36✔
392
                ->asArray()
36✔
393
                ->first();
36✔
394

395
            if ($data === null) {
36✔
396
                return null;
9✔
397
            }
398

399
            $email = $data['email'];
27✔
400
            unset($data['email']);
27✔
401
            $password_hash = $data['password_hash'];
27✔
402
            unset($data['password_hash']);
27✔
403

404
            $user                = new User($data);
27✔
405
            $user->email         = $email;
27✔
406
            $user->password_hash = $password_hash;
27✔
407
            $user->syncOriginal();
27✔
408

409
            return $user;
27✔
410
        }
411

412
        return $this->first();
2✔
413
    }
414

415
    /**
416
     * Updates the user's last active date.
417
     */
418
    public function updateActiveDate(User $user): void
419
    {
420
        assert($user->last_active instanceof Time);
421

422
        // Safe date string for database
423
        $last_active = $user->last_active->format('Y-m-d H:i:s');
5✔
424

425
        $this->builder()
5✔
426
            ->set('last_active', $last_active)
5✔
427
            ->where('id', $user->id)
5✔
428
            ->update();
5✔
429
    }
430
}
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

© 2026 Coveralls, Inc