Passed
Push — master ( 10a915...0d23d6 )
by Mattia
08:28 queued 11s
created

Core::insertNewUuid()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 25
rs 9.1111
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
use App\Repositories\AccountStatsRepository;
20
21
/**
22
 * Class Core.
23
 */
24
class Core
25
{
26
    /**
27
     * Requested string.
28
     *
29
     * @var string
30
     */
31
    private $request = '';
32
33
    /**
34
     * Userdata from/to DB.
35
     *
36
     * @var Account
37
     */
38
    private $userdata;
39
40
    /**
41
     * Full userdata.
42
     *
43
     * @var MojangAccount
44
     */
45
    private $apiUserdata;
46
47
    /**
48
     * User data has been updated?
49
     *
50
     * @var bool
51
     */
52
    private $dataUpdated = false;
53
54
    /**
55
     * Set force update.
56
     *
57
     * @var bool
58
     */
59
    private $forceUpdate;
60
61
    /**
62
     * Minepic error string.
63
     *
64
     * @var string
65
     */
66
    private $error = false;
67
68
    /**
69
     * Account not found?
70
     *
71
     * @var bool
72
     */
73
    private $accountNotFound = false;
74
75
    /**
76
     * Retry for nonexistent usernames.
77
     *
78
     * @var string
79
     */
80
    private $retryUnexistentCheck = false;
81
82
    /**
83
     * Current image path.
84
     *
85
     * @var string
86
     */
87
    public $currentUserSkinImage;
88
    /**
89
     * @var AccountRepository
90
     */
91
    private $accountRepository;
92
    /**
93
     * @var MojangClient
94
     */
95
    private $mojangClient;
96
    /**
97
     * @var AccountStatsRepository
98
     */
99
    private $accountStatsRepository;
100
101
    /**
102
     * Core constructor.
103
     * @param AccountRepository $accountRepository Where user data is stored
104
     * @param AccountStatsRepository $accountStatsRepository
105
     * @param MojangClient $mojangClient Client for Mojang API
106
     */
107
    public function __construct(
108
        AccountRepository $accountRepository,
109
        AccountStatsRepository $accountStatsRepository,
110
        MojangClient $mojangClient
111
    ) {
112
        $this->accountRepository = $accountRepository;
113
        $this->mojangClient = $mojangClient;
114
        $this->accountStatsRepository = $accountStatsRepository;
115
    }
116
117
    /**
118
     * Display error.
119
     */
120
    public function error(): string
121
    {
122
        return $this->error;
123
    }
124
125
    /**
126
     * Check if is a valid UUID.
127
     */
128
    public function isCurrentRequestValidUuid(): bool
129
    {
130
        return UserDataValidator::isValidUuid($this->request);
131
    }
132
133
    /**
134
     * Normalize request.
135
     */
136
    private function normalizeRequest(): void
137
    {
138
        $this->request = \preg_replace("#\.png.*#", '', $this->request);
139
        $this->request = \preg_replace('#[^a-zA-Z0-9_]#', '', $this->request);
140
    }
141
142
    /**
143
     * Check if cache is still valid.
144
     *
145
     * @param int
146
     * @return bool
147
     */
148
    private function checkDbCache(): bool
149
    {
150
        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...
151
    }
152
153
    /**
154
     * Load saved userdata.
155
     *
156
     * @param string $type
157
     * @param string $value
158
     * @return bool
159
     */
160
    private function loadDbUserdata($type = 'uuid', $value = ''): bool
161
    {
162
        if ($type !== 'username') {
163
            $result = $this->accountRepository->findByUuid($value);
164
        } else {
165
            $result = $this->accountRepository->findLastUpdatedByUsername($value);
166
        }
167
168
        if ($result !== null) {
169
            $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...
170
            $this->currentUserSkinImage = SkinsStorage::getPath($this->userdata->uuid);
171
172
            return true;
173
        }
174
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
175
176
        return false;
177
    }
178
179
    /**
180
     * Return loaded user data.
181
     *
182
     * @return Account
183
     */
184
    public function getUserdata(): Account
185
    {
186
        return $this->userdata ?? new Account();
187
    }
188
189
    /**
190
     * Check if an UUID is in the database.
191
     *
192
     * @return bool Returns true/false
193
     */
194
    private function uuidInDb(): bool
195
    {
196
        return $this->loadDbUserdata('uuid', $this->request);
197
    }
198
199
    /**
200
     * Check if a username is in the database.
201
     *
202
     * @return bool Returns true/false
203
     */
204
    private function nameInDb(): bool
205
    {
206
        return $this->loadDbUserdata('username', $this->request);
207
    }
208
209
    /**
210
     * Insert user data in database.
211
     *
212
     * @param void
213
     * @return bool
214
     */
215
    public function insertNewUuid(): bool
216
    {
217
        if ($this->getFullUserdataApi()) {
218
            $this->userdata = $this->accountRepository->create([
219
                'username' => $this->apiUserdata->username,
220
                'uuid' => $this->apiUserdata->uuid,
221
                'skin' => $this->apiUserdata->skin && \mb_strlen($this->apiUserdata->skin) > 1 ? $this->apiUserdata->skin : '',
222
                'cape' => $this->apiUserdata->cape && \mb_strlen($this->apiUserdata->cape) > 1 ? $this->apiUserdata->cape : '',
223
            ]);
224
225
            $this->saveRemoteSkin();
226
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->uuid);
227
228
            $this->accountStatsRepository->create([
229
                'uuid' => $this->userdata->uuid,
230
                'count_search' => 0,
231
                'count_request' => 0,
232
                'time_search' => 0,
233
                'time_request' => 0,
234
            ]);
235
236
            return true;
237
        }
238
239
        return false;
240
    }
241
242
    /**
243
     * Get UUID from username.
244
     *
245
     * @param string
246
     * @return bool
247
     */
248
    private function convertRequestToUuid(): bool
249
    {
250
        if (UserDataValidator::isValidUsername($this->request) || UserDataValidator::isValidEmail($this->request)) {
251
            try {
252
                $account = $this->mojangClient->sendUsernameInfoRequest($this->request);
253
                $this->request = $account->uuid;
254
255
                return true;
256
            } catch (\Exception $e) {
257
                \Log::error($e);
258
259
                return false;
260
            }
261
        }
262
263
        return false;
264
    }
265
266
    /**
267
     * Salva account inesistente.
268
     *
269
     * @return mixed
270
     */
271
    public function saveUnexistentAccount()
272
    {
273
        $notFound = AccountNotFound::firstOrNew(['request' => $this->request]);
274
        $notFound->request = $this->request;
275
276
        return $notFound->save();
277
    }
278
279
    /**
280
     * Check if requested string is a failed request.
281
     *
282
     * @return bool
283
     */
284
    public function isUnexistentAccount(): bool
285
    {
286
        $result = AccountNotFound::find($this->request);
287
        if ($result !== null) {
288
            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...
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
     */
318
    public function initialize(string $string): bool
319
    {
320
        $this->dataUpdated = false;
321
        $this->request = $string;
322
        $this->normalizeRequest();
323
324
        if (!empty($this->request) && \mb_strlen($this->request) <= 32) {
325
            // TODO these checks needs optimizations
326
            // Valid UUID format? Then check if UUID is in my database
327
            if ($this->isCurrentRequestValidUuid()) {
328
329
                if ($this->initializeUuidRequest()) {
330
                    return true;
331
                }
332
333
            } elseif ($this->nameInDb()) {
334
                return $this->initializeUsernameRequest();
335
            } else {
336
                // Account not found? time to retry to get information from Mojang?
337
                if (!$this->isUnexistentAccount() || $this->retryUnexistentCheck) {
338
                    if (!$this->isCurrentRequestValidUuid() && !$this->convertRequestToUuid()) {
339
                        $this->saveUnexistentAccount();
340
                        $this->userdata = null;
341
                        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
342
                        $this->error = 'Invalid request username';
343
                        $this->request = '';
344
345
                        return false;
346
                    }
347
348
                    // Check if the uuid is already in the database, maybe the user has changed username and the check
349
                    // nameInDb() has failed
350
                    if ($this->uuidInDb()) {
351
                        $this->updateDbUser();
352
353
                        return true;
354
                    }
355
356
                    if ($this->insertNewUuid()) {
357
                        if ($this->accountNotFound) {
358
                            $this->removeFailedRequest();
359
                        }
360
361
                        return true;
362
                    }
363
                }
364
            }
365
        }
366
367
        $this->userdata = null;
368
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
369
        $this->error = 'Account not found';
370
        $this->request = '';
371
372
        return false;
373
    }
374
375
    /**
376
     * Update current user fail count.
377
     */
378
    private function updateUserFailUpdate(): bool
379
    {
380
        if (isset($this->userdata->uuid)) {
381
            ++$this->userdata->fail_count;
382
383
            return $this->userdata->save();
384
        }
385
386
        return false;
387
    }
388
389
    /**
390
     * Update db user data.
391
     */
392
    private function updateDbUser(): bool
393
    {
394
        if (isset($this->userdata->username) && $this->userdata->uuid != '') {
395
            // Get data from API
396
            if ($this->getFullUserdataApi()) {
397
                $originalUsername = $this->userdata->username;
398
                // Update database
399
                $this->accountRepository->update([
400
                    'username' => $this->apiUserdata->username,
401
                    'skin' => $this->apiUserdata->skin,
402
                    'cape' => $this->apiUserdata->cape,
403
                    'fail_count' => 0,
404
                ], $this->userdata->id);
405
406
                $this->userdata->refresh();
407
408
                // Update skin
409
                $this->saveRemoteSkin();
410
411
                // Log username change
412
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
413
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
414
                }
415
                $this->dataUpdated = true;
416
417
                return true;
418
            }
419
420
            $this->updateUserFailUpdate();
421
422
            if (!SkinsStorage::exists($this->userdata->uuid)) {
423
                SkinsStorage::copyAsSteve($this->userdata->uuid);
424
            }
425
        }
426
        $this->dataUpdated = false;
427
428
        return false;
429
    }
430
431
    /**
432
     * Return if data has been updated.
433
     */
434
    public function userDataUpdated(): bool
435
    {
436
        return $this->dataUpdated;
437
    }
438
439
    /**
440
     * Log the username change.
441
     *
442
     * @param $uuid string User UUID
443
     * @param $prev string Previous username
444
     * @param $new string New username
445
     */
446
    private function logUsernameChange(string $uuid, string $prev, string $new): void
447
    {
448
        \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

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