Passed
Push — dev ( c55022...971015 )
by Mattia
05:44
created

Core::insertNewUuid()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 17
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 25
rs 9.0777
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\Models\AccountStats;
18
use App\Repositories\AccountRepository;
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
     * @var AccountRepository
89
     */
90
    private $accountRepository;
91
    /**
92
     * @var MojangClient
93
     */
94
    private $mojangClient;
95
96
    /**
97
     * Core constructor.
98
     * @param AccountRepository $accountRepository Where user data is stored
99
     * @param MojangClient $mojangClient Client for Mojang API
100
     */
101
    public function __construct(
102
        AccountRepository $accountRepository,
103
        MojangClient $mojangClient
104
    ) {
105
        $this->accountRepository = $accountRepository;
106
        $this->mojangClient = $mojangClient;
107
    }
108
109
    /**
110
     * Display error.
111
     */
112
    public function error(): string
113
    {
114
        return $this->error;
115
    }
116
117
    /**
118
     * Check if is a valid UUID.
119
     */
120
    public function isCurrentRequestValidUuid(): bool
121
    {
122
        return UserDataValidator::isValidUuid($this->request);
123
    }
124
125
    /**
126
     * Normalize request.
127
     */
128
    private function normalizeRequest(): void
129
    {
130
        $this->request = \preg_replace("#\.png.*#", '', $this->request);
131
        $this->request = \preg_replace('#[^a-zA-Z0-9_]#', '', $this->request);
132
    }
133
134
    /**
135
     * Check if chache is still valid.
136
     *
137
     * @param int
138
     * @return bool
139
     */
140
    private function checkDbCache(): bool
141
    {
142
        return (\time() - $this->userdata->updated_at->timestamp) < env('USERDATA_CACHE_TIME');
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
143
    }
144
145
    /**
146
     * Load saved userdata.
147
     *
148
     * @param string $type
149
     * @param string $value
150
     * @return bool
151
     */
152
    private function loadDbUserdata($type = 'uuid', $value = ''): bool
153
    {
154
        if ($type !== 'username') {
155
            $result = $this->accountRepository->findByUuid($value);
156
        } else {
157
            $result = $this->accountRepository->findLastUpdatedByUsername($value);
158
        }
159
160
        if ($result !== null) {
161
            $this->userdata = $result;
0 ignored issues
show
Documentation Bug introduced by
It seems like $result can also be of type Illuminate\Database\Eloquent\Builder. However, the property $userdata is declared as type App\Models\Account. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
162
            $this->currentUserSkinImage = SkinsStorage::getPath($this->userdata->uuid);
163
164
            return true;
165
        }
166
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
167
168
        return false;
169
    }
170
171
    /**
172
     * Return loaded user data.
173
     *
174
     * @return Account
175
     */
176
    public function getUserdata(): ?Account
177
    {
178
        return $this->userdata;
179
    }
180
181
    /**
182
     * Get loaded user data and stats (array).
183
     */
184
    public function getFullUserdata(): array
185
    {
186
        $userstats = AccountStats::find($this->userdata->uuid);
187
188
        return [$this->userdata, $userstats];
189
    }
190
191
    /**
192
     * Check if an UUID is in the database.
193
     */
194
    private function uuidInDb(?string $uuid = null): bool
195
    {
196
        if ($uuid === null) {
197
            $uuid = $this->request;
198
        }
199
200
        return $this->loadDbUserdata('uuid', $uuid);
201
    }
202
203
    /**
204
     * Check if a username is in the database.
205
     */
206
    private function nameInDb(?string $name = null): bool
207
    {
208
        if ($name === null) {
209
            $name = $this->request;
210
        }
211
212
        return $this->loadDbUserdata('username', $name);
213
    }
214
215
    /**
216
     * Insert user data in database.
217
     *
218
     * @param void
219
     * @return bool
220
     */
221
    public function insertNewUuid(): bool
222
    {
223
        if ($this->getFullUserdataApi()) {
224
            $this->userdata = $this->accountRepository->create([
225
                'username' => $this->apiUserdata->username,
226
                'uuid' => $this->apiUserdata->uuid,
227
                'skin' => $this->apiUserdata->skin && \mb_strlen($this->apiUserdata->skin) > 1 ? $this->apiUserdata->skin : '',
228
                'cape' => $this->apiUserdata->cape && \mb_strlen($this->apiUserdata->cape) > 1 ? $this->apiUserdata->cape : '',
229
            ]);
230
231
            $this->saveRemoteSkin();
232
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->uuid);
233
234
            $accountStats = new AccountStats();
235
            $accountStats->uuid = $this->userdata->uuid;
236
            $accountStats->count_search = 0;
237
            $accountStats->count_request = 0;
238
            $accountStats->time_search = 0;
239
            $accountStats->time_request = 0;
240
            $accountStats->save();
241
242
            return true;
243
        }
244
245
        return false;
246
    }
247
248
    /**
249
     * Get UUID from username.
250
     *
251
     * @param string
252
     * @return bool
253
     */
254
    private function convertRequestToUuid(): bool
255
    {
256
        if (UserDataValidator::isValidUsername($this->request) || UserDataValidator::isValidEmail($this->request)) {
257
            try {
258
                $account = $this->mojangClient->sendUsernameInfoRequest($this->request);
259
                $this->request = $account->uuid;
260
261
                return true;
262
            } catch (\Exception $e) {
263
                \Log::error($e);
264
265
                return false;
266
            }
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * Salva account inesistente.
274
     *
275
     * @return mixed
276
     */
277
    public function saveUnexistentAccount()
278
    {
279
        $notFound = AccountNotFound::firstOrNew(['request' => $this->request]);
280
        $notFound->request = $this->request;
281
282
        return $notFound->save();
283
    }
284
285
    /**
286
     * Check if requested string is a failed request.
287
     *
288
     * @return bool
289
     */
290
    public function isUnexistentAccount(): bool
291
    {
292
        $result = AccountNotFound::find($this->request);
293
        if ($result != null) {
294
            if ((\time() - $result->updated_at->timestamp) > env('USERDATA_CACHE_TIME')) {
0 ignored issues
show
Bug introduced by
The property updated_at does not seem to exist on App\Models\AccountNotFound. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
295
                $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...
296
            } else {
297
                $this->retryUnexistentCheck = false;
298
            }
299
            $this->accountNotFound = true;
300
301
            return true;
302
        }
303
        $this->accountNotFound = false;
304
305
        return false;
306
    }
307
308
    /**
309
     * Delete current request from failed cache.
310
     */
311
    public function removeFailedRequest(): bool
312
    {
313
        $result = AccountNotFound::where('request', $this->request)->delete();
314
315
        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

315
        return \count(/** @scrutinizer ignore-type */ $result) > 0;
Loading history...
316
    }
317
318
    /**
319
     * Check requested string and initialize objects.
320
     *
321
     * @param string
322
     * @return bool
323
     */
324
    public function initialize(string $string): bool
325
    {
326
        $this->dataUpdated = false;
327
        $this->request = $string;
328
        $this->normalizeRequest();
329
330
        if (!empty($this->request) && \mb_strlen($this->request) <= 32) {
331
            // TODO these checks needs optimizations
332
            // Valid UUID format? Then check if UUID is in my database
333
            if ($this->isCurrentRequestValidUuid() && $this->uuidInDb()) {
334
                // Check if UUID is in my database
335
                // Data cache still valid?
336
                if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
337
                    // Nope, updating data
338
                    $this->updateDbUser();
339
                } else {
340
                    // Check if local image exists
341
                    if (!SkinsStorage::exists($this->request)) {
342
                        $this->saveRemoteSkin();
343
                    }
344
                }
345
346
                return true;
347
            } elseif ($this->nameInDb()) {
348
                // Check DB datacache
349
                if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
350
                    // Check UUID (username change/other)
351
                    if ($this->convertRequestToUuid()) {
352
                        if ($this->request === $this->userdata->uuid) {
353
                            // Nope, updating data
354
                            $this->request = $this->userdata->uuid;
355
                            $this->updateDbUser();
356
                        } else {
357
                            // re-initialize process with the UUID if the name has been changed
358
                            return $this->initialize($this->request);
359
                        }
360
                    } else {
361
                        $this->request = $this->userdata->uuid;
362
                        $this->updateUserFailUpdate();
363
                        SkinsStorage::copyAsSteve($this->request);
364
                    }
365
                } else {
366
                    // Check if local image exists
367
                    if (!SkinsStorage::exists($this->request)) {
368
                        SkinsStorage::copyAsSteve($this->request);
369
                    }
370
                }
371
372
                return true;
373
            } else {
374
                // Account not found? time to retry to get information from Mojang?
375
                if ($this->retryUnexistentCheck || !$this->isUnexistentAccount()) {
376
                    if (!$this->isCurrentRequestValidUuid() && !$this->convertRequestToUuid()) {
377
                        $this->saveUnexistentAccount();
378
                        $this->userdata = null;
379
                        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
380
                        $this->error = 'Invalid request username';
381
                        $this->request = '';
382
383
                        return false;
384
                    }
385
386
                    // Check if the uuid is already in the database, maybe the user has changed username and the check
387
                    // nameInDb() has failed
388
                    if ($this->uuidInDb()) {
389
                        $this->updateDbUser();
390
391
                        return true;
392
                    }
393
394
                    if ($this->insertNewUuid()) {
395
                        if ($this->accountNotFound) {
396
                            $this->removeFailedRequest();
397
                        }
398
399
                        return true;
400
                    }
401
                }
402
            }
403
        }
404
405
        $this->userdata = null;
406
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
407
        $this->error = 'Account not found';
408
        $this->request = '';
409
410
        return false;
411
    }
412
413
    /**
414
     * Update current user fail count.
415
     */
416
    private function updateUserFailUpdate(): bool
417
    {
418
        if (isset($this->userdata->uuid)) {
419
            ++$this->userdata->fail_count;
420
421
            return $this->userdata->save();
422
        }
423
424
        return false;
425
    }
426
427
    /**
428
     * Update db user data.
429
     */
430
    private function updateDbUser(): bool
431
    {
432
        if (isset($this->userdata->username) && $this->userdata->uuid != '') {
433
            // Get data from API
434
            if ($this->getFullUserdataApi()) {
435
                $originalUsername = $this->userdata->username;
436
                // Update database
437
                $this->accountRepository->update([
438
                    'username' => $this->apiUserdata->username,
439
                    'skin' => $this->apiUserdata->skin,
440
                    'cape' => $this->apiUserdata->cape,
441
                    'fail_count' => 0,
442
                ], $this->userdata->id);
443
444
                $this->userdata->refresh();
445
446
                // Update skin
447
                $this->saveRemoteSkin();
448
449
                // Log username change
450
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
451
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
452
                }
453
                $this->dataUpdated = true;
454
455
                return true;
456
            }
457
458
            $this->updateUserFailUpdate();
459
460
            if (!SkinsStorage::exists($this->userdata->uuid)) {
461
                SkinsStorage::copyAsSteve($this->userdata->uuid);
462
            }
463
        }
464
        $this->dataUpdated = false;
465
466
        return false;
467
    }
468
469
    /**
470
     * Return if data has been updated.
471
     */
472
    public function userDataUpdated(): bool
473
    {
474
        return $this->dataUpdated;
475
    }
476
477
    /**
478
     * Log the username change.
479
     *
480
     * @param $uuid string User UUID
481
     * @param $prev string Previous username
482
     * @param $new string New username
483
     */
484
    private function logUsernameChange(string $uuid, string $prev, string $new): void
485
    {
486
        \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

486
        \Event::/** @scrutinizer ignore-call */ 
487
                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...
487
    }
488
489
    /**
490
     * Get userdata from Mojang API.
491
     *
492
     * @param mixed
493
     * @return bool
494
     */
495
    private function getFullUserdataApi(): bool
496
    {
497
        try {
498
            $this->apiUserdata = $this->mojangClient->getUuidInfo($this->request);
499
500
            return true;
501
        } catch (\Exception $e) {
502
            \Log::error($e);
503
            $this->apiUserdata = null;
504
505
            return false;
506
        }
507
    }
508
509
    /**
510
     * Show rendered avatar.
511
     *
512
     * @param int
513
     * @param mixed
514
     *
515
     * @return Avatar
516
     * @throws \Throwable
517
     */
518
    public function avatarCurrentUser(int $size = 0): Avatar
519
    {
520
        $avatar = new Avatar($this->currentUserSkinImage);
521
        $avatar->renderAvatar($size);
522
523
        return $avatar;
524
    }
525
526
    /**
527
     * Default Avatar Isometric.
528
     *
529
     * @param int $size
530
     * @return IsometricAvatar
531
     * @throws \Throwable
532
     */
533
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
534
    {
535
        // TODO: Needs refactoring
536
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
537
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
538
        $isometricAvatar = new IsometricAvatar(
539
            $uuid,
540
            $timestamp
541
        );
542
        $isometricAvatar->render($size);
543
544
        return $isometricAvatar;
545
    }
546
547
    /**
548
     * Save skin image.
549
     *
550
     * @param mixed
551
     * @return bool
552
     */
553
    public function saveRemoteSkin(): bool
554
    {
555
        if (!empty($this->userdata->skin) && \mb_strlen($this->userdata->skin) > 0) {
556
            $mojangClient = new MojangClient();
557
            try {
558
                $skinData = $mojangClient->getSkin($this->userdata->skin);
559
560
                return SkinsStorage::save($this->userdata->uuid, $skinData);
561
            } catch (\Exception $e) {
562
                \Log::error($e);
563
                $this->error = $e->getMessage();
564
            }
565
        }
566
567
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
568
    }
569
570
    /**
571
     * Return rendered skin.
572
     *
573
     * @param int
574
     * @param string
575
     *
576
     * @return Skin
577
     * @throws \Throwable
578
     */
579
    public function renderSkinCurrentUser(int $size = 0, string $type = 'F'): Skin
580
    {
581
        $skin = new Skin($this->currentUserSkinImage);
582
        $skin->renderSkin($size, $type);
583
584
        return $skin;
585
    }
586
587
    /**
588
     * Return a Skin object of the current user.
589
     */
590
    public function skinCurrentUser(): Skin
591
    {
592
        return new Skin($this->currentUserSkinImage);
593
    }
594
595
    /**
596
     * Set force update.
597
     */
598
    public function setForceUpdate(bool $forceUpdate): void
599
    {
600
        $this->forceUpdate = $forceUpdate;
601
    }
602
603
    /**
604
     * Can I exec force update?
605
     */
606
    private function forceUpdatePossible(): bool
607
    {
608
        return ($this->forceUpdate) &&
609
            ((\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...
610
    }
611
612
    /**
613
     * Use steve skin for given username.
614
     *
615
     * @param string
616
     */
617
    public function updateStats($type = 'request'): void
618
    {
619
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
620
            $AccStats = new AccountStats();
621
            if ($type === 'request') {
622
                $AccStats->incrementRequestStats($this->userdata->uuid);
623
            } elseif ($type === 'search') {
624
                $AccStats->incrementSearchStats($this->userdata->uuid);
625
            }
626
        }
627
    }
628
}
629