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

codeigniter4 / shield / 6132899896

09 Sep 2023 08:02PM UTC coverage: 92.601% (+0.05%) from 92.549%
6132899896

push

github

web-flow
Merge pull request #811 from kenjis/reuse-workflows

chore: use workflows in codeigniter4/.github

2115 of 2284 relevant lines covered (92.6%)

49.05 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace CodeIgniter\Shield\Models;
6

7
use CodeIgniter\Database\Exceptions\DataException;
8
use CodeIgniter\I18n\Time;
9
use CodeIgniter\Shield\Authentication\Authenticators\Session;
10
use CodeIgniter\Shield\Entities\User;
11
use CodeIgniter\Shield\Entities\UserIdentity;
12
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
13
use CodeIgniter\Shield\Exceptions\ValidationException;
14
use Faker\Generator;
15

16
/**
17
 * @phpstan-consistent-constructor
18
 */
19
class UserModel extends BaseModel
20
{
21
    protected $primaryKey     = 'id';
22
    protected $returnType     = User::class;
23
    protected $useSoftDeletes = true;
24
    protected $allowedFields  = [
25
        'username',
26
        'status',
27
        'status_message',
28
        'active',
29
        'last_active',
30
        'deleted_at',
31
    ];
32
    protected $useTimestamps = true;
33
    protected $afterFind     = ['fetchIdentities'];
34
    protected $afterInsert   = ['saveEmailIdentity'];
35
    protected $afterUpdate   = ['saveEmailIdentity'];
36

37
    /**
38
     * Whether identity records should be included
39
     * when user records are fetched from the database.
40
     */
41
    protected bool $fetchIdentities = false;
42

43
    /**
44
     * Save the User for afterInsert and afterUpdate
45
     */
46
    protected ?User $tempUser = null;
47

48
    protected function initialize(): void
49
    {
50
        parent::initialize();
426✔
51

52
        $this->table = $this->tables['users'];
426✔
53
    }
54

55
    /**
56
     * Mark the next find* query to include identities
57
     *
58
     * @return $this
59
     */
60
    public function withIdentities(): self
61
    {
62
        $this->fetchIdentities = true;
10✔
63

64
        return $this;
10✔
65
    }
66

67
    /**
68
     * Populates identities for all records
69
     * returned from a find* method. Called
70
     * automatically when $this->fetchIdentities == true
71
     *
72
     * Model event callback called by `afterFind`.
73
     */
74
    protected function fetchIdentities(array $data): array
75
    {
76
        if (! $this->fetchIdentities) {
352✔
77
            return $data;
352✔
78
        }
79

80
        $userIds = $data['singleton']
10✔
81
            ? array_column($data, 'id')
4✔
82
            : array_column($data['data'], 'id');
6✔
83

84
        if ($userIds === []) {
10✔
85
            return $data;
4✔
86
        }
87

88
        /** @var UserIdentityModel $identityModel */
89
        $identityModel = model(UserIdentityModel::class);
6✔
90

91
        // Get our identities for all users
92
        $identities = $identityModel->getIdentitiesByUserIds($userIds);
6✔
93

94
        if (empty($identities)) {
6✔
95
            return $data;
×
96
        }
97

98
        $mappedUsers = $this->assignIdentities($data, $identities);
6✔
99

100
        $data['data'] = $data['singleton'] ? $mappedUsers[$data['id']] : $mappedUsers;
6✔
101

102
        return $data;
6✔
103
    }
104

105
    /**
106
     * Map our users by ID to make assigning simpler
107
     *
108
     * @param array          $data       Event $data
109
     * @param UserIdentity[] $identities
110
     *
111
     * @return User[] UserId => User object
112
     * @phpstan-return array<int|string, User> UserId => User object
113
     */
114
    private function assignIdentities(array $data, array $identities): array
115
    {
116
        $mappedUsers    = [];
6✔
117
        $userIdentities = [];
6✔
118

119
        $users = $data['singleton'] ? [$data['data']] : $data['data'];
6✔
120

121
        foreach ($users as $user) {
6✔
122
            $mappedUsers[$user->id] = $user;
6✔
123
        }
124
        unset($users);
6✔
125

126
        // Now group the identities by user
127
        foreach ($identities as $identity) {
6✔
128
            $userIdentities[$identity->user_id][] = $identity;
6✔
129
        }
130
        unset($identities);
6✔
131

132
        // Now assign the identities to the user
133
        foreach ($userIdentities as $userId => $identityArray) {
6✔
134
            $mappedUsers[$userId]->identities = $identityArray;
6✔
135
        }
136
        unset($userIdentities);
6✔
137

138
        return $mappedUsers;
6✔
139
    }
140

141
    /**
142
     * Adds a user to the default group.
143
     * Used during registration.
144
     */
145
    public function addToDefaultGroup(User $user): void
146
    {
147
        $defaultGroup  = setting('AuthGroups.defaultGroup');
10✔
148
        $allowedGroups = array_keys(setting('AuthGroups.groups'));
10✔
149

150
        if (empty($defaultGroup) || ! in_array($defaultGroup, $allowedGroups, true)) {
10✔
151
            throw new InvalidArgumentException(lang('Auth.unknownGroup', [$defaultGroup ?? '--not found--']));
×
152
        }
153

154
        $user->addGroup($defaultGroup);
10✔
155
    }
156

157
    public function fake(Generator &$faker): User
158
    {
159
        return new User([
344✔
160
            'username' => $faker->unique()->userName(),
344✔
161
            'active'   => true,
344✔
162
        ]);
344✔
163
    }
164

165
    /**
166
     * Locates a User object by ID.
167
     *
168
     * @param int|string $id
169
     */
170
    public function findById($id): ?User
171
    {
172
        return $this->find($id);
106✔
173
    }
174

175
    /**
176
     * Locate a User object by the given credentials.
177
     *
178
     * @param array<string, string> $credentials
179
     */
180
    public function findByCredentials(array $credentials): ?User
181
    {
182
        // Email is stored in an identity so remove that here
183
        $email = $credentials['email'] ?? null;
54✔
184
        unset($credentials['email']);
54✔
185

186
        if ($email === null && $credentials === []) {
54✔
187
            return null;
2✔
188
        }
189

190
        // any of the credentials used should be case-insensitive
191
        foreach ($credentials as $key => $value) {
54✔
192
            $this->where(
6✔
193
                'LOWER(' . $this->db->protectIdentifiers($this->table . ".{$key}") . ')',
6✔
194
                strtolower($value)
6✔
195
            );
6✔
196
        }
197

198
        if ($email !== null) {
54✔
199
            $data = $this->select(
48✔
200
                sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities'])
48✔
201
            )
48✔
202
                ->join($this->tables['identities'], sprintf('%1$s.user_id = %2$s.id', $this->tables['identities'], $this->table))
48✔
203
                ->where($this->tables['identities'] . '.type', Session::ID_TYPE_EMAIL_PASSWORD)
48✔
204
                ->where(
48✔
205
                    'LOWER(' . $this->db->protectIdentifiers($this->tables['identities'] . '.secret') . ')',
48✔
206
                    strtolower($email)
48✔
207
                )
48✔
208
                ->asArray()
48✔
209
                ->first();
48✔
210

211
            if ($data === null) {
48✔
212
                return null;
10✔
213
            }
214

215
            $email = $data['email'];
38✔
216
            unset($data['email']);
38✔
217
            $password_hash = $data['password_hash'];
38✔
218
            unset($data['password_hash']);
38✔
219

220
            $user                = new User($data);
38✔
221
            $user->email         = $email;
38✔
222
            $user->password_hash = $password_hash;
38✔
223
            $user->syncOriginal();
38✔
224

225
            return $user;
38✔
226
        }
227

228
        return $this->first();
6✔
229
    }
230

231
    /**
232
     * Activate a User.
233
     */
234
    public function activate(User $user): void
235
    {
236
        $user->active = true;
×
237

238
        $this->save($user);
×
239
    }
240

241
    /**
242
     * Override the BaseModel's `insert()` method.
243
     * If you pass User object, also inserts Email Identity.
244
     *
245
     * @param array|User $data
246
     *
247
     * @return int|string|true Insert ID if $returnID is true
248
     *
249
     * @throws ValidationException
250
     */
251
    public function insert($data = null, bool $returnID = true)
252
    {
253
        // Clone User object for not changing the passed object.
254
        $this->tempUser = $data instanceof User ? clone $data : null;
352✔
255

256
        $result = parent::insert($data, $returnID);
352✔
257

258
        $this->checkQueryReturn($result);
352✔
259

260
        return $returnID ? $this->insertID : $result;
352✔
261
    }
262

263
    /**
264
     * Override the BaseModel's `update()` method.
265
     * If you pass User object, also updates Email Identity.
266
     *
267
     * @param array|int|string|null $id
268
     * @param array|User            $data
269
     *
270
     * @return true if the update is successful
271
     *
272
     * @throws ValidationException
273
     */
274
    public function update($id = null, $data = null): bool
275
    {
276
        // Clone User object for not changing the passed object.
277
        $this->tempUser = $data instanceof User ? clone $data : null;
46✔
278

279
        try {
280
            /** @throws DataException */
281
            $result = parent::update($id, $data);
46✔
282
        } catch (DataException $e) {
11✔
283
            // When $data is an array.
284
            if ($this->tempUser === null) {
11✔
285
                throw $e;
2✔
286
            }
287

288
            $messages = [
9✔
289
                lang('Database.emptyDataset', ['update']),
9✔
290
            ];
9✔
291

292
            if (in_array($e->getMessage(), $messages, true)) {
9✔
293
                $this->tempUser->saveEmailIdentity();
9✔
294

295
                return true;
9✔
296
            }
297

298
            throw $e;
×
299
        }
300

301
        $this->checkQueryReturn($result);
38✔
302

303
        return true;
38✔
304
    }
305

306
    /**
307
     * Override the BaseModel's `save()` method.
308
     * If you pass User object, also updates Email Identity.
309
     *
310
     * @param array|User $data
311
     *
312
     * @return true if the save is successful
313
     *
314
     * @throws ValidationException
315
     */
316
    public function save($data): bool
317
    {
318
        $result = parent::save($data);
56✔
319

320
        $this->checkQueryReturn($result);
54✔
321

322
        return true;
54✔
323
    }
324

325
    /**
326
     * Save Email Identity
327
     *
328
     * Model event callback called by `afterInsert` and `afterUpdate`.
329
     */
330
    protected function saveEmailIdentity(array $data): array
331
    {
332
        // If insert()/update() gets an array data, do nothing.
333
        if ($this->tempUser === null) {
352✔
334
            return $data;
16✔
335
        }
336

337
        // Insert
338
        if ($this->tempUser->id === null) {
350✔
339
            /** @var User $user */
340
            $user = $this->find($this->db->insertID());
336✔
341

342
            // If you get identity (email/password), the User object must have the id.
343
            $this->tempUser->id = $user->id;
336✔
344

345
            $user->email         = $this->tempUser->email ?? '';
336✔
346
            $user->password      = $this->tempUser->password ?? '';
336✔
347
            $user->password_hash = $this->tempUser->password_hash ?? '';
336✔
348

349
            $user->saveEmailIdentity();
336✔
350
            $this->tempUser = null;
336✔
351

352
            return $data;
336✔
353
        }
354

355
        // Update
356
        $this->tempUser->saveEmailIdentity();
43✔
357
        $this->tempUser = null;
43✔
358

359
        return $data;
43✔
360
    }
361

362
    /**
363
     * Updates the user's last active date.
364
     */
365
    public function updateActiveDate(User $user): void
366
    {
367
        assert($user->last_active instanceof Time);
368

369
        // Safe date string for database
370
        $last_active = $user->last_active->format('Y-m-d H:i:s');
14✔
371

372
        $this->builder()
14✔
373
            ->set('last_active', $last_active)
14✔
374
            ->where('id', $user->id)
14✔
375
            ->update();
14✔
376
    }
377
}
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