Passed
Push — master ( e0d87d...28f5a1 )
by Mattia
04:18
created

Core::error()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
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;
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
     * Normalize request.
126
     */
127
    private function normalizeRequest(): void
128
    {
129
        $this->request = \preg_replace("#\.png.*#", '', $this->request);
130
        $this->request = \preg_replace('#[^a-zA-Z0-9_]#', '', $this->request);
131
    }
132
133
    /**
134
     * Check if cache is still valid.
135
     *
136
     * @param int
137
     *
138
     * @return bool
139
     */
140
    private function checkDbCache(): bool
141
    {
142
        $accountUpdatedAtTimestamp = $this->userdata->updated_at->timestamp ?? 0;
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
143
144
        return (\time() - $accountUpdatedAtTimestamp) < env('USERDATA_CACHE_TIME');
145
    }
146
147
    /**
148
     * Load saved Account information.
149
     *
150
     * @param Account|null $account
151
     *
152
     * @return bool
153
     */
154
    private function loadAccountData(?Account $account): bool
155
    {
156
        if ($account !== null) {
157
            $this->userdata = $account;
158
            $this->currentUserSkinImage = SkinsStorage::getPath($this->userdata->uuid);
159
160
            return true;
161
        }
162
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
163
164
        return false;
165
    }
166
167
    /**
168
     * Return loaded user data.
169
     *
170
     * @return Account
171
     */
172
    public function getUserdata(): Account
173
    {
174
        return $this->userdata ?? new Account();
175
    }
176
177
    /**
178
     * Check if an UUID is in the database.
179
     *
180
     * @return bool Returns true/false
181
     */
182
    private function requestedUuidInDb(): bool
183
    {
184
        $account = $this->accountRepository->findByUuid($this->request);
185
186
        return $this->loadAccountData($account);
187
    }
188
189
    /**
190
     * Check if a username is in the database.
191
     *
192
     * @return bool Returns true/false
193
     */
194
    private function requestedUsernameInDb(): bool
195
    {
196
        $account = $this->accountRepository->findLastUpdatedByUsername($this->request);
197
198
        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

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

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