Passed
Push — master ( 28f5a1...3f12a7 )
by Mattia
05:11
created

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

189
        return $this->loadAccountData(/** @scrutinizer ignore-type */ $account);
Loading history...
190
    }
191
192
    /**
193
     * Insert user data in database.
194
     *
195
     * @param void
196
     *
197
     * @return bool
198
     */
199
    public function insertNewUuid(): bool
200
    {
201
        if ($this->getFullUserdataApi()) {
202
            $this->userdata = $this->accountRepository->create([
203
                'username' => $this->apiUserdata->getUsername(),
204
                'uuid' => $this->apiUserdata->getUuid(),
205
                'skin' => $this->apiUserdata->getSkin(),
206
                'cape' => $this->apiUserdata->getCape(),
207
            ]);
208
209
            $this->saveRemoteSkin();
210
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->getUuid());
211
212
            $this->accountStatsRepository->create([
213
                'uuid' => $this->userdata->uuid,
214
                'count_search' => 0,
215
                'count_request' => 0,
216
                'time_search' => 0,
217
                'time_request' => 0,
218
            ]);
219
220
            return true;
221
        }
222
223
        return false;
224
    }
225
226
    /**
227
     * Get UUID from username.
228
     *
229
     * @param string
230
     *
231
     * @return bool
232
     */
233
    private function convertRequestToUuid(): bool
234
    {
235
        if (!UserDataValidator::isValidUsername($this->request) && !UserDataValidator::isValidEmail($this->request)) {
236
            return false;
237
        }
238
239
        try {
240
            $account = $this->mojangClient->sendUsernameInfoRequest($this->request);
241
            $this->request = $account->getUuid();
242
243
            return true;
244
        } catch (\Exception $e) {
245
            Log::error($e->getFile().':'.$e->getLine().' '.$e->getMessage());
246
            Log::error($e->getTraceAsString());
247
248
            return false;
249
        }
250
    }
251
252
    /**
253
     * Salva account inesistente.
254
     *
255
     * @return mixed
256
     */
257
    public function saveUnexistentAccount()
258
    {
259
        $notFound = AccountNotFound::firstOrNew(['request' => $this->request]);
260
        $notFound->request = $this->request;
261
262
        return $notFound->save();
263
    }
264
265
    /**
266
     * Check if requested string is a failed request.
267
     *
268
     * @return bool
269
     */
270
    public function isUnexistentAccount(): bool
271
    {
272
        /** @var \App\Models\AccountNotFound $result */
273
        $result = AccountNotFound::find($this->request);
274
        if ($result !== null) {
275
            if ((\time() - $result->updated_at->timestamp) > env('USERDATA_CACHE_TIME')) {
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
276
                $result->touch();
277
                $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...
278
            } else {
279
                $this->retryUnexistentCheck = false;
280
            }
281
            $this->accountNotFound = true;
282
283
            return true;
284
        }
285
        $this->accountNotFound = false;
286
287
        return false;
288
    }
289
290
    /**
291
     * Delete current request from failed cache.
292
     */
293
    public function removeFailedRequest(): bool
294
    {
295
        $result = AccountNotFound::where('request', $this->request)->delete();
296
297
        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

297
        return \count(/** @scrutinizer ignore-type */ $result) > 0;
Loading history...
298
    }
299
300
    /**
301
     * Check requested string and initialize objects.
302
     *
303
     * @param string
304
     *
305
     * @throws \Exception
306
     *
307
     * @return bool
308
     */
309
    public function initialize(string $string): bool
310
    {
311
        $this->dataUpdated = false;
312
        $this->request = $string;
313
314
        if ($this->request === '' || \mb_strlen($this->request) > 32) {
315
            throw new \Exception('Invalid Username or UUID provided');
316
        }
317
318
        if ($this->isCurrentRequestValidUuid()) {
319
            if ($this->initializeUuidRequest()) {
320
                return true;
321
            }
322
        } elseif ($this->requestedUsernameInDb()) {
323
            return $this->initializeUsernameRequest();
324
        } elseif (!$this->isUnexistentAccount() || $this->retryUnexistentCheck) {
325
            if (!$this->isCurrentRequestValidUuid() && !$this->convertRequestToUuid()) {
326
                $this->saveUnexistentAccount();
327
                $this->setFailedRequest('Invalid requested username');
328
329
                return false;
330
            }
331
332
            // Check if the uuid is already in the database, maybe the user has changed username and the check
333
            // nameInDb() has failed
334
            if ($this->requestedUuidInDb()) {
335
                $this->updateDbUser();
336
337
                return true;
338
            }
339
340
            if ($this->insertNewUuid()) {
341
                if ($this->accountNotFound) {
342
                    $this->removeFailedRequest();
343
                }
344
345
                return true;
346
            }
347
        }
348
349
        $this->setFailedRequest('Account not found');
350
351
        return false;
352
    }
353
354
    /**
355
     * Update current user fail count.
356
     */
357
    private function updateUserFailUpdate(): bool
358
    {
359
        if (isset($this->userdata->uuid)) {
360
            ++$this->userdata->fail_count;
361
362
            return $this->userdata->save();
363
        }
364
365
        return false;
366
    }
367
368
    /**
369
     * Update db user data.
370
     */
371
    private function updateDbUser(): bool
372
    {
373
        if (isset($this->userdata->username) && $this->userdata->uuid !== '') {
374
            // Get data from API
375
            if ($this->getFullUserdataApi()) {
376
                $originalUsername = $this->userdata->username;
377
                // Update database
378
                $this->accountRepository->update([
379
                    'username' => $this->apiUserdata->getUsername(),
380
                    'skin' => $this->apiUserdata->getSkin(),
381
                    'cape' => $this->apiUserdata->getCape(),
382
                    'fail_count' => 0,
383
                ], $this->userdata->id);
384
385
                $this->userdata->touch();
386
                $this->userdata->refresh();
387
388
                // Update skin
389
                $this->saveRemoteSkin();
390
391
                // Log username change
392
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
393
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
394
                }
395
                $this->dataUpdated = true;
396
397
                return true;
398
            }
399
400
            $this->updateUserFailUpdate();
401
402
            if (!SkinsStorage::exists($this->userdata->uuid)) {
403
                SkinsStorage::copyAsSteve($this->userdata->uuid);
404
            }
405
        }
406
        $this->dataUpdated = false;
407
408
        return false;
409
    }
410
411
    /**
412
     * Return if data has been updated.
413
     */
414
    public function userDataUpdated(): bool
415
    {
416
        return $this->dataUpdated;
417
    }
418
419
    /**
420
     * Log the username change.
421
     *
422
     * @param $uuid string User UUID
423
     * @param $prev string Previous username
424
     * @param $new string New username
425
     */
426
    private function logUsernameChange(string $uuid, string $prev, string $new): void
427
    {
428
        Event::dispatch(new UsernameChangeEvent($uuid, $prev, $new));
429
    }
430
431
    /**
432
     * Get userdata from Mojang API.
433
     *
434
     * @param mixed
435
     *
436
     * @return bool
437
     */
438
    private function getFullUserdataApi(): bool
439
    {
440
        try {
441
            $this->apiUserdata = $this->mojangClient->getUuidInfo($this->request);
442
443
            return true;
444
        } catch (\Exception $e) {
445
            Log::error($e->getTraceAsString(), ['request' => $this->request]);
446
            $this->apiUserdata = null;
447
448
            return false;
449
        }
450
    }
451
452
    /**
453
     * Show rendered avatar.
454
     *
455
     * @param int
456
     * @param mixed
457
     *
458
     * @throws \Throwable
459
     *
460
     * @return Avatar
461
     */
462
    public function avatarCurrentUser(int $size = 0): Avatar
463
    {
464
        $avatar = new Avatar($this->currentUserSkinImage);
465
        $avatar->renderAvatar($size);
466
467
        return $avatar;
468
    }
469
470
    /**
471
     * Default Avatar Isometric.
472
     *
473
     * @param int $size
474
     *
475
     * @throws \Throwable
476
     *
477
     * @return IsometricAvatar
478
     */
479
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
480
    {
481
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
482
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
483
        $isometricAvatar = new IsometricAvatar(
484
            $uuid,
485
            $timestamp
486
        );
487
        $isometricAvatar->render($size);
488
489
        return $isometricAvatar;
490
    }
491
492
    /**
493
     * Save skin image.
494
     *
495
     * @param mixed
496
     *
497
     * @return bool
498
     */
499
    public function saveRemoteSkin(): bool
500
    {
501
        if (!empty($this->userdata->skin) && $this->userdata->skin !== '') {
502
            try {
503
                $skinData = $this->mojangClient->getSkin($this->userdata->skin);
504
505
                return SkinsStorage::save($this->userdata->uuid, $skinData);
506
            } catch (\Exception $e) {
507
                \Log::error($e);
508
            }
509
        }
510
511
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
512
    }
513
514
    /**
515
     * Return rendered skin.
516
     *
517
     * @param int
518
     * @param string
519
     *
520
     * @throws \Throwable
521
     *
522
     * @return Skin
523
     */
524
    public function renderSkinCurrentUser(int $size = 0, string $type = ImageSection::FRONT): Skin
525
    {
526
        $skin = new Skin($this->currentUserSkinImage);
527
        $skin->renderSkin($size, $type);
528
529
        return $skin;
530
    }
531
532
    /**
533
     * Return a Skin object of the current user.
534
     */
535
    public function skinCurrentUser(): Skin
536
    {
537
        return new Skin($this->currentUserSkinImage);
538
    }
539
540
    /**
541
     * Set force update.
542
     *
543
     * @param bool $forceUpdate
544
     */
545
    public function setForceUpdate(bool $forceUpdate): void
546
    {
547
        $this->forceUpdate = $forceUpdate;
548
    }
549
550
    /**
551
     * Can I exec force update?
552
     */
553
    private function forceUpdatePossible(): bool
554
    {
555
        return ($this->forceUpdate) &&
556
            ((\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...
557
    }
558
559
    /**
560
     * Use steve skin for given username.
561
     *
562
     * @param string
563
     */
564
    public function updateStats($type = 'request'): void
565
    {
566
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
567
            if ($type === 'request') {
568
                $this->accountStatsRepository->incrementRequestCounter($this->userdata->uuid);
569
            } elseif ($type === 'search') {
570
                $this->accountStatsRepository->incrementSearchCounter($this->userdata->uuid);
571
            }
572
        }
573
    }
574
575
    /**
576
     * @return bool
577
     */
578
    private function initializeUuidRequest(): bool
579
    {
580
        if ($this->requestedUuidInDb()) {
581
            // Check if UUID is in my database
582
            // Data cache still valid?
583
            if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
584
                // Nope, updating data
585
                $this->updateDbUser();
586
            }
587
588
            if (!SkinsStorage::exists($this->request)) {
589
                $this->saveRemoteSkin();
590
            }
591
592
            return true;
593
        }
594
595
        if ($this->insertNewUuid()) {
596
            return true;
597
        }
598
599
        return false;
600
    }
601
602
    /**
603
     * @throws \Exception
604
     *
605
     * @return bool
606
     */
607
    private function initializeUsernameRequest(): bool
608
    {
609
        // Check DB datacache
610
        if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
611
            // Check UUID (username change/other)
612
            if ($this->convertRequestToUuid()) {
613
                if ($this->request === $this->userdata->uuid) {
614
                    // Nope, updating data
615
                    $this->request = $this->userdata->uuid;
616
                    $this->updateDbUser();
617
                } else {
618
                    // re-initialize process with the UUID if the name has been changed
619
                    return $this->initialize($this->request);
620
                }
621
            } else {
622
                $this->request = $this->userdata->uuid;
623
                $this->updateUserFailUpdate();
624
                SkinsStorage::copyAsSteve($this->request);
625
            }
626
        } elseif (!SkinsStorage::exists($this->request)) {
627
            SkinsStorage::copyAsSteve($this->request);
628
        }
629
630
        return true;
631
    }
632
633
    /**
634
     * Set failed request.
635
     *
636
     * @param string $errorMessage
637
     */
638
    private function setFailedRequest(string $errorMessage = ''): void
639
    {
640
        Log::notice($errorMessage, ['request' => $this->request]);
641
        $this->userdata = null;
642
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
643
        $this->request = '';
644
    }
645
}
646