Passed
Push — dev ( 5c4da4...f4770d )
by Mattia
05:20
created

Core::isValidUuid()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

191
        return $this->loadDbUserdata('uuid', /** @scrutinizer ignore-type */ $uuid);
Loading history...
192
    }
193
194
    /**
195
     * Check if a username is in the database.
196
     *
197
     * @param mixed
198
     */
199
    private function nameInDb($name = false): bool
200
    {
201
        if (!$name) {
202
            $name = $this->request;
203
        }
204
205
        return $this->loadDbUserdata('username', $name);
206
    }
207
208
    /**
209
     * Insert userdata in database.
210
     *
211
     * @param void
212
     */
213
    public function insertNewUuid(): bool
214
    {
215
        if ($this->getFullUserdataApi()) {
216
            $this->userdata = new Account();
217
            $this->userdata->username = $this->apiUserdata->username;
218
            $this->userdata->uuid = $this->apiUserdata->uuid;
219
            $this->userdata->skin = ($this->apiUserdata->skin && \mb_strlen($this->apiUserdata->skin) > 1 ?
220
                $this->apiUserdata->skin : '');
221
            $this->userdata->cape = ($this->apiUserdata->cape && \mb_strlen($this->apiUserdata->cape) > 1 ?
222
                $this->apiUserdata->cape : '');
223
            $this->userdata->save();
224
225
            $this->saveRemoteSkin();
226
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->uuid);
227
228
            $accountStats = new AccountStats();
229
            $accountStats->uuid = $this->userdata->uuid;
230
            $accountStats->count_search = 0;
231
            $accountStats->count_request = 0;
232
            $accountStats->time_search = 0;
233
            $accountStats->time_request = 0;
234
            $accountStats->save();
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
            $MojangClient = new MojangClient();
252
            try {
253
                $account = $MojangClient->sendUsernameInfoRequest($this->request);
254
                $this->request = $account->uuid;
255
256
                return true;
257
            } catch (\Exception $e) {
258
                \Log::error($e);
259
260
                return false;
261
            }
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * Salva account inesistente.
269
     *
270
     * @return mixed
271
     */
272
    public function saveUnexistentAccount()
273
    {
274
        $notFound = AccountNotFound::firstOrNew(['request' => $this->request]);
275
        $notFound->request = $this->request;
276
277
        return $notFound->save();
278
    }
279
280
    /**
281
     * Check if requested string is a failed request.
282
     *
283
     * @param void
284
     */
285
    public function isUnexistentAccount(): bool
286
    {
287
        $result = AccountNotFound::find($this->request);
288
        if ($result != null) {
289
            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...
290
                $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...
291
            } else {
292
                $this->retryUnexistentCheck = false;
293
            }
294
            $this->accountNotFound = true;
295
296
            return true;
297
        }
298
        $this->accountNotFound = false;
299
300
        return false;
301
    }
302
303
    /**
304
     * Delete current request from failed cache.
305
     */
306
    public function removeFailedRequest(): bool
307
    {
308
        $result = AccountNotFound::where('request', $this->request)->delete();
309
310
        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

310
        return \count(/** @scrutinizer ignore-type */ $result) > 0;
Loading history...
311
    }
312
313
    /**
314
     * HTTP Headers for current user.
315
     *
316
     * @param $size
317
     * @param string $type
318
     */
319
    public function generateHttpCacheHeaders($size, $type = 'avatar'): array
320
    {
321
        if (isset($this->userdata->uuid) && $this->userdata->uuid !== '') {
322
            return [
323
                'Cache-Control' => 'private, max-age='.env('USERDATA_CACHE_TIME'),
324
                'Last-Modified' => \gmdate('D, d M Y H:i:s \G\M\T', $this->userdata->updated_at->timestamp),
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
325
                'Expires' => \gmdate('D, d M Y H:i:s \G\M\T', $this->userdata->updated_at->timestamp + env('USERDATA_CACHE_TIME')),
326
                'ETag' => \md5($type.$this->userdata->updated_at->timestamp.$this->userdata->uuid.$this->userdata->username.$size),
327
            ];
328
        }
329
330
        return [
331
            'Cache-Control' => 'private, max-age=7776000',
332
            'ETag' => \md5("{$type}_FFS_STOP_STEVE_SPAM_{$size}"),
333
            'Last-Modified' => \gmdate('D, d M Y H:i:s \G\M\T', \strtotime('2017-02-01 00:00')),
334
            'Expires' => \gmdate('D, d M Y H:i:s \G\M\T', \strtotime('2017-02-01 00:00')),
335
        ];
336
    }
337
338
    /**
339
     * Check requested string and initialize objects.
340
     *
341
     * @param string
342
     */
343
    public function initialize(string $string): bool
344
    {
345
        $this->dataUpdated = false;
346
        $this->request = $string;
347
        $this->normalizeRequest();
348
349
        if (!empty($this->request) && \mb_strlen($this->request) <= 32) {
350
            // TODO these checks needs optimizations
351
            // Valid UUID format? Then check if UUID is in my database
352
            if ($this->isCurrentRequestValidUuid() && $this->uuidInDb()) {
353
                // Check if UUID is in my database
354
                // Data cache still valid?
355
                if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
356
                    // Nope, updating data
357
                    $this->updateDbUser();
358
                } else {
359
                    // Check if local image exists
360
                    if (!SkinsStorage::exists($this->request)) {
361
                        $this->saveRemoteSkin();
362
                    }
363
                }
364
365
                return true;
366
            } elseif ($this->nameInDb()) {
367
                // Check DB datacache
368
                if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
369
                    // Check UUID (username change/other)
370
                    if ($this->convertRequestToUuid()) {
371
                        if ($this->request === $this->userdata->uuid) {
372
                            // Nope, updating data
373
                            $this->request = $this->userdata->uuid;
374
                            $this->updateDbUser();
375
                        } else {
376
                            // re-initialize process with the UUID if the name has been changed
377
                            return $this->initialize($this->request);
378
                        }
379
                    } else {
380
                        $this->request = $this->userdata->uuid;
381
                        $this->updateUserFailUpdate();
382
                        SkinsStorage::copyAsSteve($this->request);
383
                    }
384
                } else {
385
                    // Check if local image exists
386
                    if (!SkinsStorage::exists($this->request)) {
387
                        SkinsStorage::copyAsSteve($this->request);
388
                    }
389
                }
390
391
                return true;
392
            } else {
393
                // Account not found? time to retry to get information from Mojang?
394
                if ($this->retryUnexistentCheck || !$this->isUnexistentAccount()) {
395
                    if (!$this->isCurrentRequestValidUuid() && !$this->convertRequestToUuid()) {
396
                        $this->saveUnexistentAccount();
397
                        $this->userdata = null;
398
                        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
399
                        $this->error = 'Invalid request username';
400
                        $this->request = '';
401
402
                        return false;
403
                    }
404
405
                    // Check if the uuid is already in the database, maybe the user has changed username and the check
406
                    // nameInDb() has failed
407
                    if ($this->uuidInDb()) {
408
                        $this->updateDbUser();
409
410
                        return true;
411
                    }
412
413
                    if ($this->insertNewUuid()) {
414
                        if ($this->accountNotFound) {
415
                            $this->removeFailedRequest();
416
                        }
417
418
                        return true;
419
                    }
420
                }
421
            }
422
        }
423
424
        $this->userdata = null;
425
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
426
        $this->error = 'Account not found';
427
        $this->request = '';
428
429
        return false;
430
    }
431
432
    /**
433
     * Update current user fail count.
434
     */
435
    private function updateUserFailUpdate(): bool
436
    {
437
        if (isset($this->userdata->uuid)) {
438
            ++$this->userdata->fail_count;
439
440
            return $this->userdata->save();
441
        }
442
443
        return false;
444
    }
445
446
    /**
447
     * Update db userdata.
448
     */
449
    private function updateDbUser(): bool
450
    {
451
        if (isset($this->userdata->username) && $this->userdata->uuid != '') {
452
            // Get data from API
453
            if ($this->getFullUserdataApi()) {
454
                $originalUsername = $this->userdata->username;
455
                // Update database
456
                $this->userdata->username = $this->apiUserdata->username;
457
                $this->userdata->skin = $this->apiUserdata->skin;
458
                $this->userdata->cape = $this->apiUserdata->cape;
459
                $this->userdata->fail_count = 0;
460
                $this->userdata->save();
461
462
                // Update skin
463
                $this->saveRemoteSkin();
464
465
                // Log username change
466
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
467
                    $this->logUsernameChange($originalUsername, $this->userdata->username, $this->userdata->uuid);
468
                }
469
                $this->dataUpdated = true;
470
471
                return true;
472
            }
473
474
            $this->updateUserFailUpdate();
475
476
            if (!SkinsStorage::exists($this->userdata->uuid)) {
477
                SkinsStorage::copyAsSteve($this->userdata->uuid);
478
            }
479
        }
480
        $this->dataUpdated = false;
481
482
        return false;
483
    }
484
485
    /**
486
     * Return if data has been updated.
487
     */
488
    public function userDataUpdated(): bool
489
    {
490
        return $this->dataUpdated;
491
    }
492
493
    /**
494
     * Log the username change.
495
     *
496
     * @param $prev string Previous username
497
     * @param $new string New username
498
     * @param $uuid string User UUID
499
     */
500
    private function logUsernameChange(string $prev, string $new, string $uuid): bool
501
    {
502
        $accountNameChange = new AccountNameChange();
503
        $accountNameChange->uuid = $uuid;
504
        $accountNameChange->prev_name = $prev;
505
        $accountNameChange->new_name = $new;
506
        $accountNameChange->time_change = \time();
507
508
        return $accountNameChange->save();
509
    }
510
511
    /**
512
     * Get userdata from Mojang API.
513
     *
514
     * @param mixed
515
     */
516
    private function getFullUserdataApi(): bool
517
    {
518
        $MojangClient = new MojangClient();
519
        try {
520
            $this->apiUserdata = $MojangClient->getUuidInfo($this->request);
521
522
            return true;
523
        } catch (\Exception $e) {
524
            \Log::error($e);
525
            $this->apiUserdata = null;
526
527
            return false;
528
        }
529
    }
530
531
    /*==================================================================================================================
532
     * =AVATAR
533
     *================================================================================================================*/
534
535
    /**
536
     * Show rendered avatar.
537
     *
538
     * @param int
539
     * @param mixed
540
     *
541
     * @throws \Throwable
542
     */
543
    public function avatarCurrentUser(int $size = 0): Avatar
544
    {
545
        $avatar = new Avatar($this->currentUserSkinImage);
546
        $avatar->renderAvatar($size);
547
548
        return $avatar;
549
    }
550
551
    /**
552
     * Random avatar from saved.
553
     *
554
     * @param int
555
     *
556
     * @throws \Throwable
557
     */
558
    public function randomAvatar(int $size = 0): Avatar
559
    {
560
        $all_skin = \scandir(storage_path(env('SKINS_FOLDER')));
561
        $rand = \random_int(2, \count($all_skin));
0 ignored issues
show
Bug introduced by
It seems like $all_skin can also be of type false; 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

561
        $rand = \random_int(2, \count(/** @scrutinizer ignore-type */ $all_skin));
Loading history...
562
563
        $avatar = new Avatar(SkinsStorage::getPath($all_skin[$rand]));
564
        $avatar->renderAvatar($size);
565
566
        return $avatar;
567
    }
568
569
    /**
570
     * Default Avatar.
571
     *
572
     * @return Avatar (rendered)
573
     *
574
     * @throws \Throwable
575
     */
576
    public function defaultAvatar(int $size = 0): Avatar
577
    {
578
        $avatar = new Avatar(SkinsStorage::getPath(env('DEFAULT_USERNAME')));
579
        $avatar->renderAvatar($size);
580
581
        return $avatar;
582
    }
583
584
    /*==================================================================================================================
585
     * =ISOMETRIC_AVATAR
586
     *================================================================================================================*/
587
588
    /**
589
     * Default Avatar Isometric.
590
     *
591
     * @throws \Throwable
592
     */
593
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
594
    {
595
        // TODO: Needs refactoring
596
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
597
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
598
        $isometricAvatar = new IsometricAvatar(
599
            $uuid,
600
            $timestamp
601
        );
602
        $isometricAvatar->render($size);
603
604
        return $isometricAvatar;
605
    }
606
607
    /**
608
     * Default Avatar (Isometric).
609
     *
610
     * @return IsometricAvatar (rendered)
611
     */
612
    public function defaultIsometricAvatar(int $size = 0): IsometricAvatar
613
    {
614
        $isometricAvatar = new IsometricAvatar(
615
            env('DEFAULT_UUID'),
616
            0
617
        );
618
        $isometricAvatar->checkCacheStatus(false);
619
        $isometricAvatar->render($size);
620
621
        return $isometricAvatar;
622
    }
623
624
    /*==================================================================================================================
625
     * =SKIN
626
     *================================================================================================================*/
627
628
    /**
629
     * Save skin image.
630
     *
631
     * @param mixed
632
     */
633
    public function saveRemoteSkin(): bool
634
    {
635
        if (!empty($this->userdata->skin) && \mb_strlen($this->userdata->skin) > 0) {
636
            $mojangClient = new MojangClient();
637
            try {
638
                $skinData = $mojangClient->getSkin($this->userdata->skin);
639
640
                return SkinsStorage::save($this->userdata->uuid, $skinData);
641
            } catch (\Exception $e) {
642
                \Log::error($e);
643
                $this->error = $e->getMessage();
644
            }
645
        }
646
647
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
648
    }
649
650
    /**
651
     * Return rendered skin.
652
     *
653
     * @param int
654
     * @param string
655
     *
656
     * @throws \Throwable
657
     */
658
    public function renderSkinCurrentUser(int $size = 0, string $type = 'F'): Skin
659
    {
660
        $skin = new Skin($this->currentUserSkinImage);
661
        $skin->renderSkin($size, $type);
662
663
        return $skin;
664
    }
665
666
    /**
667
     * Return a Skin object of the current user.
668
     */
669
    public function skinCurrentUser(): Skin
670
    {
671
        return new Skin($this->currentUserSkinImage);
672
    }
673
674
    /**
675
     * Set force update.
676
     */
677
    public function setForceUpdate(bool $forceUpdate): void
678
    {
679
        $this->forceUpdate = $forceUpdate;
680
    }
681
682
    /**
683
     * Can I exec force update?
684
     */
685
    private function forceUpdatePossible(): bool
686
    {
687
        return
688
            ($this->forceUpdate) &&
689
            ((\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...
690
            ;
691
    }
692
693
    /*==================================================================================================================
694
     * =STATS
695
     *================================================================================================================*/
696
697
    /**
698
     * Use steve skin for given username.
699
     *
700
     * @param string
701
     */
702
    public function updateStats($type = 'request'): void
703
    {
704
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
705
            $AccStats = new AccountStats();
706
            if ($type === 'request') {
707
                $AccStats->incrementRequestStats($this->userdata->uuid);
708
            } elseif ($type === 'search') {
709
                $AccStats->incrementSearchStats($this->userdata->uuid);
710
            }
711
        }
712
    }
713
}
714