1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Models;
7
8use App\Exceptions\InvariantException;
9use Exception;
10
11/**
12 * @property Build $build
13 * @property mixed $disk_md5
14 * @property mixed $mac_md5
15 * @property mixed $osu_md5
16 * @property \Carbon\Carbon $timestamp
17 * @property mixed $unique_md5
18 * @property int $user_id
19 * @property int $verified
20 */
21class UserClient extends Model
22{
23 const CREATED_AT = 'timestamp';
24
25 public $incrementing = false;
26 public $timestamps = false;
27
28 protected $casts = [
29 'timestamp' => 'datetime',
30 'verified' => 'boolean',
31 ];
32 protected $primaryKey = ':composite';
33 protected $primaryKeys = ['user_id', 'osu_md5', 'unique_md5'];
34 protected $table = 'osu_user_security';
35
36 public static function lookupOrNew($userId, $hash)
37 {
38 $splitHash = static::splitHash($hash);
39
40 return static::firstOrNew([
41 'user_id' => $userId,
42 'unique_md5' => $splitHash['unique'],
43 'osu_md5' => $splitHash['osu'],
44 ], [
45 'mac_md5' => $splitHash['mac'],
46 'disk_md5' => $splitHash['disk'],
47 ]);
48 }
49
50 public static function markVerified($userId, $hash)
51 {
52 $client = static::lookupOrNew($userId, $hash);
53
54 try {
55 $client->fill(['verified' => true])->save();
56 } catch (Exception $e) {
57 if (is_sql_unique_exception($e)) {
58 static::markVerified($userId, $hash);
59 return;
60 }
61
62 throw $e;
63 }
64 }
65
66 public static function splitHash($hash)
67 {
68 $hashes = explode(':', $hash);
69
70 if (count($hashes) < 5) {
71 throw new InvariantException('invalid client hash format');
72 }
73
74 return array_map(function ($value) {
75 if (!ctype_xdigit($value) || strlen($value) !== 32) {
76 throw new InvariantException('invalid md5 hash inside client hash');
77 }
78
79 return hex2bin($value);
80 }, [
81 'osu' => $hashes[0],
82 'mac' => $hashes[2],
83 'unique' => $hashes[3],
84 'disk' => $hashes[4],
85 ]);
86 }
87
88 public function build()
89 {
90 return $this->belongsTo(Build::class, 'osu_md5', 'hash');
91 }
92
93 public function isLatest()
94 {
95 if ($this->build === null) {
96 return false;
97 }
98
99 $latestBuild = Build::select('build_id')
100 ->where([
101 'test_build' => false,
102 'stream_id' => $this->build->stream_id,
103 ])->last();
104
105 return $this->build->getKey() === optional($latestBuild)->getKey();
106 }
107}