Passed
Push — dev ( 8f2a35...efb0ec )
by Mattia
05:10 queued 10s
created

Core::requestedUsernameInDb()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App;
6
7
use App\Events\Account\UsernameChangeEvent;
8
use App\Helpers\Storage\Files\SkinsStorage;
9
use App\Helpers\UserDataValidator;
10
use App\Image\IsometricAvatar;
11
use App\Image\Sections\Avatar;
12
use App\Image\Sections\Skin;
13
use App\Minecraft\MojangAccount;
14
use App\Minecraft\MojangClient;
15
use App\Models\Account;
16
use App\Models\AccountNotFound;
17
use App\Repositories\AccountRepository;
18
use App\Repositories\AccountStatsRepository;
19
20
/**
21
 * Class Core.
22
 */
23
class Core
24
{
25
    /**
26
     * Requested string.
27
     *
28
     * @var string
29
     */
30
    private $request = '';
31
32
    /**
33
     * Userdata from/to DB.
34
     *
35
     * @var Account
36
     */
37
    private $userdata;
38
39
    /**
40
     * Full userdata.
41
     *
42
     * @var MojangAccount
43
     */
44
    private $apiUserdata;
45
46
    /**
47
     * User data has been updated?
48
     *
49
     * @var bool
50
     */
51
    private $dataUpdated = false;
52
53
    /**
54
     * Set force update.
55
     *
56
     * @var bool
57
     */
58
    private $forceUpdate;
59
60
    /**
61
     * Minepic error string.
62
     *
63
     * @var string
64
     */
65
    private $error = false;
66
67
    /**
68
     * Account not found?
69
     *
70
     * @var bool
71
     */
72
    private $accountNotFound = false;
73
74
    /**
75
     * Retry for nonexistent usernames.
76
     *
77
     * @var string
78
     */
79
    private $retryUnexistentCheck = false;
80
81
    /**
82
     * Current image path.
83
     *
84
     * @var string
85
     */
86
    public $currentUserSkinImage;
87
88
    /**
89
     * @var AccountRepository
90
     */
91
    private $accountRepository;
92
93
    /**
94
     * @var MojangClient
95
     */
96
    private $mojangClient;
97
98
    /**
99
     * @var AccountStatsRepository
100
     */
101
    private $accountStatsRepository;
102
103
    /**
104
     * Core constructor.
105
     * @param AccountRepository $accountRepository Where user data is stored
106
     * @param AccountStatsRepository $accountStatsRepository
107
     * @param MojangClient $mojangClient Client for Mojang API
108
     */
109
    public function __construct(
110
        AccountRepository $accountRepository,
111
        AccountStatsRepository $accountStatsRepository,
112
        MojangClient $mojangClient
113
    ) {
114
        $this->accountRepository = $accountRepository;
115
        $this->mojangClient = $mojangClient;
116
        $this->accountStatsRepository = $accountStatsRepository;
117
    }
118
119
    /**
120
     * Display error.
121
     */
122
    public function error(): string
123
    {
124
        return $this->error;
125
    }
126
127
    /**
128
     * Check if is a valid UUID.
129
     */
130
    public function isCurrentRequestValidUuid(): bool
131
    {
132
        return UserDataValidator::isValidUuid($this->request);
133
    }
134
135
    /**
136
     * Normalize request.
137
     */
138
    private function normalizeRequest(): void
139
    {
140
        $this->request = \preg_replace("#\.png.*#", '', $this->request);
141
        $this->request = \preg_replace('#[^a-zA-Z0-9_]#', '', $this->request);
142
    }
143
144
    /**
145
     * Check if cache is still valid.
146
     *
147
     * @param int
148
     * @return bool
149
     */
150
    private function checkDbCache(): bool
151
    {
152
        $accountUpdatedAtTimestamp = $this->userdata->updated_at->timestamp ?? 0;
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
153
        return (\time() - $accountUpdatedAtTimestamp) < env('USERDATA_CACHE_TIME');
154
    }
155
156
    /**
157
     * Load saved Account information.
158
     *
159
     * @param Account|null $account
160
     * @return bool
161
     */
162
    private function loadAccountData(?Account $account): bool
163
    {
164
        if ($account !== null) {
165
            $this->userdata = $account;
166
            $this->currentUserSkinImage = SkinsStorage::getPath($this->userdata->uuid);
167
168
            return true;
169
        }
170
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
171
172
        return false;
173
    }
174
175
    /**
176
     * Return loaded user data.
177
     *
178
     * @return Account
179
     */
180
    public function getUserdata(): Account
181
    {
182
        return $this->userdata ?? new Account();
183
    }
184
185
    /**
186
     * Check if an UUID is in the database.
187
     *
188
     * @return bool Returns true/false
189
     */
190
    private function requestedUuidInDb(): bool
191
    {
192
        $account = $this->accountRepository->findByUuid($this->request);
193
        return $this->loadAccountData($account);
194
    }
195
196
    /**
197
     * Check if a username is in the database.
198
     *
199
     * @return bool Returns true/false
200
     */
201
    private function requestedUsernameInDb(): bool
202
    {
203
        $account = $this->accountRepository->findLastUpdatedByUsername($this->request);
204
        return $this->loadAccountData($account);
0 ignored issues
show
Bug introduced by
It seems like $account can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $account of App\Core::loadAccountData() does only seem to accept App\Models\Account|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
        return $this->loadAccountData(/** @scrutinizer ignore-type */ $account);
Loading history...
205
    }
206
207
    /**
208
     * Insert user data in database.
209
     *
210
     * @param void
211
     * @return bool
212
     */
213
    public function insertNewUuid(): bool
214
    {
215
        if ($this->getFullUserdataApi()) {
216
            $this->userdata = $this->accountRepository->create([
217
                'username' => $this->apiUserdata->username,
218
                'uuid' => $this->apiUserdata->uuid,
219
                'skin' => $this->apiUserdata->skin && \mb_strlen($this->apiUserdata->skin) > 1 ? $this->apiUserdata->skin : '',
220
                'cape' => $this->apiUserdata->cape && \mb_strlen($this->apiUserdata->cape) > 1 ? $this->apiUserdata->cape : '',
221
            ]);
222
223
            $this->saveRemoteSkin();
224
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->uuid);
225
226
            $this->accountStatsRepository->create([
227
                'uuid' => $this->userdata->uuid,
228
                'count_search' => 0,
229
                'count_request' => 0,
230
                'time_search' => 0,
231
                'time_request' => 0,
232
            ]);
233
234
            return true;
235
        }
236
237
        return false;
238
    }
239
240
    /**
241
     * Get UUID from username.
242
     *
243
     * @param string
244
     * @return bool
245
     */
246
    private function convertRequestToUuid(): bool
247
    {
248
        if (UserDataValidator::isValidUsername($this->request) || UserDataValidator::isValidEmail($this->request)) {
249
            try {
250
                $account = $this->mojangClient->sendUsernameInfoRequest($this->request);
251
                $this->request = $account->uuid;
252
253
                return true;
254
            } catch (\Exception $e) {
255
                \Log::error($e);
256
257
                return false;
258
            }
259
        }
260
261
        return false;
262
    }
263
264
    /**
265
     * Salva account inesistente.
266
     *
267
     * @return mixed
268
     */
269
    public function saveUnexistentAccount()
270
    {
271
        $notFound = AccountNotFound::firstOrNew(['request' => $this->request]);
272
        $notFound->request = $this->request;
273
274
        return $notFound->save();
275
    }
276
277
    /**
278
     * Check if requested string is a failed request.
279
     *
280
     * @return bool
281
     */
282
    public function isUnexistentAccount(): bool
283
    {
284
        /** @var \App\Models\AccountNotFound $result */
285
        $result = AccountNotFound::find($this->request);
286
        if ($result !== null) {
287
            if ((\time() - $result->updated_at->timestamp) > env('USERDATA_CACHE_TIME')) {
288
                $result->touch();
289
                $this->retryUnexistentCheck = true;
0 ignored issues
show
Documentation Bug introduced by
The property $retryUnexistentCheck was declared of type string, but true is of type true. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
290
            } else {
291
                $this->retryUnexistentCheck = false;
292
            }
293
            $this->accountNotFound = true;
294
295
            return true;
296
        }
297
        $this->accountNotFound = false;
298
299
        return false;
300
    }
301
302
    /**
303
     * Delete current request from failed cache.
304
     */
305
    public function removeFailedRequest(): bool
306
    {
307
        $result = AccountNotFound::where('request', $this->request)->delete();
308
309
        return \count($result) > 0;
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type integer; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
        return \count(/** @scrutinizer ignore-type */ $result) > 0;
Loading history...
310
    }
311
312
    /**
313
     * Check requested string and initialize objects.
314
     *
315
     * @param string
316
     * @return bool
317
     * @throws \Exception
318
     */
319
    public function initialize(string $string): bool
320
    {
321
        $this->dataUpdated = false;
322
        $this->request = $string;
323
        $this->normalizeRequest();
324
325
        if ($this->request === '' || \mb_strlen($this->request) > 32) {
326
            throw new \Exception('Invalid Username or UUID provided');
327
        }
328
329
        if ($this->isCurrentRequestValidUuid()) {
330
331
            if ($this->initializeUuidRequest()) {
332
                return true;
333
            }
334
335
        } elseif ($this->requestedUsernameInDb()) {
336
            return $this->initializeUsernameRequest();
337
        } else if (!$this->isUnexistentAccount() || $this->retryUnexistentCheck) {
338
            if (!$this->isCurrentRequestValidUuid() && !$this->convertRequestToUuid()) {
339
                $this->saveUnexistentAccount();
340
                $this->setFailedRequest('Invalid requested username');
341
342
                return false;
343
            }
344
345
            // Check if the uuid is already in the database, maybe the user has changed username and the check
346
            // nameInDb() has failed
347
            if ($this->requestedUuidInDb()) {
348
                $this->updateDbUser();
349
350
                return true;
351
            }
352
353
            if ($this->insertNewUuid()) {
354
                if ($this->accountNotFound) {
355
                    $this->removeFailedRequest();
356
                }
357
358
                return true;
359
            }
360
        }
361
362
        $this->setFailedRequest('Account not found');
363
        return false;
364
    }
365
366
    /**
367
     * Update current user fail count.
368
     */
369
    private function updateUserFailUpdate(): bool
370
    {
371
        if (isset($this->userdata->uuid)) {
372
            ++$this->userdata->fail_count;
373
374
            return $this->userdata->save();
375
        }
376
377
        return false;
378
    }
379
380
    /**
381
     * Update db user data.
382
     */
383
    private function updateDbUser(): bool
384
    {
385
        if (isset($this->userdata->username) && $this->userdata->uuid !== '') {
386
            // Get data from API
387
            if ($this->getFullUserdataApi()) {
388
                $originalUsername = $this->userdata->username;
389
                // Update database
390
                $this->accountRepository->update([
391
                    'username' => $this->apiUserdata->username,
392
                    'skin' => $this->apiUserdata->skin,
393
                    'cape' => $this->apiUserdata->cape,
394
                    'fail_count' => 0,
395
                ], $this->userdata->id);
396
397
                $this->userdata->refresh();
398
399
                // Update skin
400
                $this->saveRemoteSkin();
401
402
                // Log username change
403
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
404
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
405
                }
406
                $this->dataUpdated = true;
407
408
                return true;
409
            }
410
411
            $this->updateUserFailUpdate();
412
413
            if (!SkinsStorage::exists($this->userdata->uuid)) {
414
                SkinsStorage::copyAsSteve($this->userdata->uuid);
415
            }
416
        }
417
        $this->dataUpdated = false;
418
419
        return false;
420
    }
421
422
    /**
423
     * Return if data has been updated.
424
     */
425
    public function userDataUpdated(): bool
426
    {
427
        return $this->dataUpdated;
428
    }
429
430
    /**
431
     * Log the username change.
432
     *
433
     * @param $uuid string User UUID
434
     * @param $prev string Previous username
435
     * @param $new string New username
436
     */
437
    private function logUsernameChange(string $uuid, string $prev, string $new): void
438
    {
439
        \Event::dispatch(new UsernameChangeEvent($uuid, $prev, $new));
0 ignored issues
show
Bug introduced by
The method dispatch() does not exist on Event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

439
        \Event::/** @scrutinizer ignore-call */ 
440
                dispatch(new UsernameChangeEvent($uuid, $prev, $new));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
440
    }
441
442
    /**
443
     * Get userdata from Mojang API.
444
     *
445
     * @param mixed
446
     * @return bool
447
     */
448
    private function getFullUserdataApi(): bool
449
    {
450
        try {
451
            $this->apiUserdata = $this->mojangClient->getUuidInfo($this->request);
452
453
            return true;
454
        } catch (\Exception $e) {
455
            \Log::error($e);
456
            $this->apiUserdata = null;
457
458
            return false;
459
        }
460
    }
461
462
    /**
463
     * Show rendered avatar.
464
     *
465
     * @param int
466
     * @param mixed
467
     *
468
     * @return Avatar
469
     * @throws \Throwable
470
     */
471
    public function avatarCurrentUser(int $size = 0): Avatar
472
    {
473
        $avatar = new Avatar($this->currentUserSkinImage);
474
        $avatar->renderAvatar($size);
475
476
        return $avatar;
477
    }
478
479
    /**
480
     * Default Avatar Isometric.
481
     *
482
     * @param int $size
483
     * @return IsometricAvatar
484
     * @throws \Throwable
485
     */
486
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
487
    {
488
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
489
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
490
        $isometricAvatar = new IsometricAvatar(
491
            $uuid,
492
            $timestamp
493
        );
494
        $isometricAvatar->render($size);
495
496
        return $isometricAvatar;
497
    }
498
499
    /**
500
     * Save skin image.
501
     *
502
     * @param mixed
503
     * @return bool
504
     */
505
    public function saveRemoteSkin(): bool
506
    {
507
        if (!empty($this->userdata->skin) && \mb_strlen($this->userdata->skin) > 0) {
508
            try {
509
                $skinData = $this->mojangClient->getSkin($this->userdata->skin);
510
                return SkinsStorage::save($this->userdata->uuid, $skinData);
511
            } catch (\Exception $e) {
512
                \Log::error($e);
513
                $this->error = $e->getMessage();
514
            }
515
        }
516
517
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
518
    }
519
520
    /**
521
     * Return rendered skin.
522
     *
523
     * @param int
524
     * @param string
525
     *
526
     * @return Skin
527
     * @throws \Throwable
528
     */
529
    public function renderSkinCurrentUser(int $size = 0, string $type = 'F'): Skin
530
    {
531
        $skin = new Skin($this->currentUserSkinImage);
532
        $skin->renderSkin($size, $type);
533
534
        return $skin;
535
    }
536
537
    /**
538
     * Return a Skin object of the current user.
539
     */
540
    public function skinCurrentUser(): Skin
541
    {
542
        return new Skin($this->currentUserSkinImage);
543
    }
544
545
    /**
546
     * Set force update.
547
     * @param bool $forceUpdate
548
     */
549
    public function setForceUpdate(bool $forceUpdate): void
550
    {
551
        $this->forceUpdate = $forceUpdate;
552
    }
553
554
    /**
555
     * Can I exec force update?
556
     */
557
    private function forceUpdatePossible(): bool
558
    {
559
        return ($this->forceUpdate) &&
560
            ((\time() - $this->userdata->updated_at->timestamp) > env('MIN_USERDATA_UPDATE_INTERVAL'));
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
561
    }
562
563
    /**
564
     * Use steve skin for given username.
565
     *
566
     * @param string
567
     */
568
    public function updateStats($type = 'request'): void
569
    {
570
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
571
            if ($type === 'request') {
572
                $this->accountStatsRepository->incrementRequestCounter($this->userdata->uuid);
573
            } elseif ($type === 'search') {
574
                $this->accountStatsRepository->incrementSearchCounter($this->userdata->uuid);
575
            }
576
        }
577
    }
578
579
    /**
580
     * @return bool
581
     */
582
    private function initializeUuidRequest(): bool
583
    {
584
        if ($this->requestedUuidInDb()) {
585
            // Check if UUID is in my database
586
            // Data cache still valid?
587
            if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
588
                // Nope, updating data
589
                $this->updateDbUser();
590
            }
591
592
            if (!SkinsStorage::exists($this->request)) {
593
                $this->saveRemoteSkin();
594
            }
595
596
            return true;
597
        }
598
599
        if ($this->insertNewUuid()) {
600
            return true;
601
        }
602
603
        return false;
604
    }
605
606
    /**
607
     * @return bool
608
     * @throws \Exception
609
     */
610
    private function initializeUsernameRequest(): bool
611
    {
612
        // Check DB datacache
613
        if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
614
            // Check UUID (username change/other)
615
            if ($this->convertRequestToUuid()) {
616
                if ($this->request === $this->userdata->uuid) {
617
                    // Nope, updating data
618
                    $this->request = $this->userdata->uuid;
619
                    $this->updateDbUser();
620
                } else {
621
                    // re-initialize process with the UUID if the name has been changed
622
                    return $this->initialize($this->request);
623
                }
624
            } else {
625
                $this->request = $this->userdata->uuid;
626
                $this->updateUserFailUpdate();
627
                SkinsStorage::copyAsSteve($this->request);
628
            }
629
        } else if (!SkinsStorage::exists($this->request)) {
630
            SkinsStorage::copyAsSteve($this->request);
631
        }
632
633
        return true;
634
    }
635
636
    /**
637
     * Set failed request
638
     *
639
     * @param string $errorMessage
640
     */
641
    private function setFailedRequest(string $errorMessage = ''): void
642
    {
643
        $this->userdata = null;
644
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
645
        $this->error = $errorMessage;
646
        $this->request = '';
647
    }
648
}
649