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\Libraries;
7
8use App\Models\Beatmap;
9use App\Models\Beatmapset;
10use App\Models\RankHighest;
11use App\Models\User;
12use App\Models\UserBadge;
13use App\Models\UsernameChangeHistory;
14use Carbon\Carbon;
15use DB;
16use Illuminate\Support\Collection;
17
18class UsernameValidation
19{
20 public static function validateAvailability(string $username): ValidationErrors
21 {
22 $errors = new ValidationErrors('user');
23
24 if (($availableDate = User::checkWhenUsernameAvailable($username)) > Carbon::now()) {
25 $remaining = Carbon::now()->diff($availableDate, false);
26
27 // the times are +1 to round up the interval; e.g. 5 days, 2 hours will show 6 days
28 if ($remaining->days + 1 >= User::INACTIVE_DAYS) {
29 //no need to mention the inactivity period of the account is actively in use.
30 $errors->add('username', '.username_in_use');
31 } elseif ($remaining->days > 0) {
32 $errors->add(
33 'username',
34 '.username_available_in',
35 ['duration' => osu_trans_choice('common.count.days', $remaining->days + 1)]
36 );
37 } elseif ($remaining->h > 0) {
38 $errors->add(
39 'username',
40 '.username_available_in',
41 ['duration' => osu_trans_choice('common.count.hours', $remaining->h + 1)]
42 );
43 } else {
44 $errors->add('username', '.username_available_soon');
45 }
46 }
47
48 return $errors;
49 }
50
51 public static function validateUsername($username)
52 {
53 $errors = new ValidationErrors('user');
54
55 if (($username ?? '') !== trim($username)) {
56 $errors->add('username', '.username_no_spaces');
57 }
58
59 if (strlen($username) < 3) {
60 $errors->add('username', '.username_too_short');
61 }
62
63 if (strlen($username) > 15) {
64 $errors->add('username', '.username_too_long');
65 }
66
67 if (strpos($username, ' ') !== false || !preg_match('#^[A-Za-z0-9-\[\]_ ]+$#u', $username)) {
68 $errors->add('username', '.username_invalid_characters');
69 }
70
71 if (strpos($username, '_') !== false && strpos($username, ' ') !== false) {
72 $errors->add('username', '.username_no_space_userscore_mix');
73 }
74
75 foreach (model_pluck(DB::table('phpbb_disallow'), 'disallow_username') as $check) {
76 if (preg_match('#^'.str_replace('%', '.*?', preg_quote($check, '#')).'$#i', $username)) {
77 $errors->add('username', '.username_not_allowed');
78 break;
79 }
80 }
81
82 return $errors;
83 }
84
85 public static function validateUsersOfUsername(string $username): ValidationErrors
86 {
87 $errors = new ValidationErrors('user');
88 $userIds = static::usersOfUsername($username)->pluck('user_id');
89
90 // Check if any of the users have been ranked in the top 100
91 $highestRank = RankHighest::whereIn('user_id', $userIds)->min('rank');
92 if ($highestRank !== null && $highestRank <= 100) {
93 return $errors->add('username', '.username_locked');
94 }
95
96 // Check if any of the users have badges
97 if (UserBadge::whereIn('user_id', $userIds)->exists()) {
98 return $errors->add('username', '.username_locked');
99 }
100
101 // Check if any of the users have beatmaps or beatmapsets with
102 // leaderboards enabled
103 if (
104 Beatmap::scoreable()->whereIn('user_id', $userIds)->exists() ||
105 Beatmapset::scoreable()->whereIn('user_id', $userIds)->exists()
106 ) {
107 return $errors->add('username', '.username_locked');
108 }
109
110 return $errors;
111 }
112
113 private static function usersOfUsername(string $username): Collection
114 {
115 $userIds = UsernameChangeHistory::where('username_last', $username)->pluck('user_id');
116 $users = User::whereIn('user_id', $userIds)->get();
117 $existing = User::findByUsernameForInactive($username);
118 if ($existing !== null) {
119 $users->push($existing);
120 }
121
122 return $users;
123 }
124}