the browser-facing portion of osu!
at master 11 kB view raw
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\Http\Controllers; 7 8use App\Exceptions\ImageProcessorException; 9use App\Exceptions\ModelNotSavedException; 10use App\Libraries\Session\Store as SessionStore; 11use App\Libraries\SessionVerification; 12use App\Libraries\User\AvatarHelper; 13use App\Libraries\User\CountryChange; 14use App\Libraries\User\CountryChangeTarget; 15use App\Mail\UserEmailUpdated; 16use App\Mail\UserPasswordUpdated; 17use App\Models\Beatmap; 18use App\Models\GithubUser; 19use App\Models\OAuth\Client; 20use App\Models\UserAccountHistory; 21use App\Models\UserNotificationOption; 22use App\Transformers\CurrentUserTransformer; 23use App\Transformers\LegacyApiKeyTransformer; 24use App\Transformers\LegacyIrcKeyTransformer; 25use Auth; 26use DB; 27use Mail; 28use Request; 29 30class AccountController extends Controller 31{ 32 public function __construct() 33 { 34 $this->middleware('auth', ['except' => [ 35 'verifyLink', 36 ]]); 37 38 $this->middleware(function ($request, $next) { 39 if (Auth::check() && Auth::user()->isSilenced()) { 40 return abort(403, osu_trans('authorization.silenced')); 41 } 42 43 return $next($request); 44 }, [ 45 'except' => [ 46 'edit', 47 'reissueCode', 48 'updateCountry', 49 'updateEmail', 50 'updateNotificationOptions', 51 'updateOptions', 52 'updatePassword', 53 'verify', 54 'verifyLink', 55 ], 56 ]); 57 58 $this->middleware('verify-user', ['except' => [ 59 'updateOptions', 60 ]]); 61 62 $this->middleware('throttle:3,5', ['only' => [ 63 'reissueCode', 64 ]]); 65 66 $this->middleware('throttle:60,10', ['only' => [ 67 'updateEmail', 68 'updatePassword', 69 'verify', 70 'verifyLink', 71 ]]); 72 73 parent::__construct(); 74 } 75 76 public function avatar() 77 { 78 $user = auth()->user(); 79 80 try { 81 AvatarHelper::set($user, Request::file('avatar_file')); 82 } catch (ImageProcessorException $e) { 83 return error_popup($e->getMessage()); 84 } 85 86 return json_item($user, new CurrentUserTransformer()); 87 } 88 89 public function cover() 90 { 91 $user = \Auth::user(); 92 $params = get_params(\Request::all(), null, [ 93 'cover_file:file', 94 'cover_id:int', 95 ], ['null_missing' => true]); 96 97 if ($params['cover_file'] !== null && !$user->osu_subscriber) { 98 return error_popup(osu_trans('errors.supporter_only')); 99 } 100 101 try { 102 $user->cover()->set($params['cover_id'], $params['cover_file']); 103 $user->save(); 104 } catch (ImageProcessorException $e) { 105 return error_popup($e->getMessage()); 106 } 107 108 return json_item($user, new CurrentUserTransformer()); 109 } 110 111 public function edit() 112 { 113 $user = auth()->user(); 114 115 $blocks = $user->blocks() 116 ->orderBy('username') 117 ->get(); 118 119 $sessions = SessionStore::sessions($user->getKey()); 120 $currentSessionId = \Session::getId(); 121 122 $authorizedClients = json_collection(Client::forUser($user), 'OAuth\Client', 'user'); 123 $ownClients = json_collection($user->oauthClients()->where('revoked', false)->get(), 'OAuth\Client', ['redirect', 'secret']); 124 125 $legacyApiKey = $user->apiKeys()->available()->first(); 126 $legacyApiKeyJson = $legacyApiKey === null ? null : json_item($legacyApiKey, new LegacyApiKeyTransformer()); 127 128 $legacyIrcKey = $user->legacyIrcKey; 129 $legacyIrcKeyJson = $legacyIrcKey === null ? null : json_item($legacyIrcKey, new LegacyIrcKeyTransformer()); 130 131 $notificationOptions = $user->notificationOptions->keyBy('name'); 132 133 $githubUser = GithubUser::canAuthenticate() && $user->githubUser !== null 134 ? json_item($user->githubUser, 'GithubUser') 135 : null; 136 137 return ext_view('accounts.edit', compact( 138 'authorizedClients', 139 'blocks', 140 'currentSessionId', 141 'githubUser', 142 'legacyApiKeyJson', 143 'legacyIrcKeyJson', 144 'notificationOptions', 145 'ownClients', 146 'sessions' 147 )); 148 } 149 150 public function update() 151 { 152 $user = Auth::user(); 153 154 $params = get_params(request()->all(), 'user', [ 155 'user_from:string', 156 'user_interests:string', 157 'user_occ:string', 158 'user_sig:string', 159 'user_twitter:string', 160 'user_website:string', 161 'user_discord:string', 162 'user_style:int', 163 ]); 164 165 // setting it to null (default) is always allowed 166 if (isset($params['user_style']) && !$user->osu_subscriber) { 167 return error_popup(osu_trans('errors.supporter_only')); 168 } 169 170 try { 171 $user->fill($params)->saveOrExplode(); 172 } catch (ModelNotSavedException $e) { 173 return ModelNotSavedException::makeResponse($e, compact('user')); 174 } 175 176 return json_item($user, new CurrentUserTransformer()); 177 } 178 179 public function updateCountry() 180 { 181 $newCountry = get_string(Request::input('country_acronym')); 182 $user = Auth::user(); 183 184 if ($newCountry === null || CountryChangeTarget::get($user) !== $newCountry) { 185 abort(403, 'specified country_acronym is not allowed'); 186 } 187 188 CountryChange::handle($user, $newCountry, 'account settings'); 189 \Session::flash('popup', osu_trans('common.saved')); 190 191 return ext_view('layout.ujs-reload', [], 'js'); 192 } 193 194 public function updateEmail() 195 { 196 priv_check('UserUpdateEmail')->ensureCan(); 197 198 $params = get_params(request()->all(), 'user', ['current_password', 'user_email', 'user_email_confirmation']); 199 $user = Auth::user()->validateCurrentPassword()->validateEmailConfirmation(); 200 $previousEmail = $user->user_email; 201 202 if ($user->update($params) === true) { 203 foreach ([$previousEmail, $user->user_email] as $address) { 204 if (is_valid_email_format($address)) { 205 Mail::to($address)->locale($user->preferredLocale())->send(new UserEmailUpdated($user)); 206 } 207 } 208 209 UserAccountHistory::logUserUpdateEmail($user, $previousEmail); 210 211 return response([], 204); 212 } else { 213 return ModelNotSavedException::makeResponse(null, compact('user')); 214 } 215 } 216 217 public function updateNotificationOptions() 218 { 219 $requestParams = request()->all()['user_notification_option'] ?? []; 220 if (!is_array($requestParams)) { 221 abort(422); 222 } 223 224 DB::transaction(function () use ($requestParams) { 225 $user = auth()->user(); 226 $user 227 ->notificationOptions() 228 ->whereIn('name', array_keys($requestParams)) 229 ->select('user_id') 230 ->lockForUpdate() 231 ->get(); 232 foreach ($requestParams as $key => $value) { 233 if (!UserNotificationOption::supportsNotifications($key)) { 234 continue; 235 } 236 237 $params = get_params($value, null, ['details:any']); 238 239 $option = $user->notificationOptions()->firstOrNew(['name' => $key]); 240 // TODO: show correct field error. 241 $option->fill($params)->saveOrExplode(); 242 } 243 }); 244 245 return response(null, 204); 246 } 247 248 public function updateOptions() 249 { 250 $user = Auth::user(); 251 $params = request()->all(); 252 253 $userParams = get_params($params, 'user', [ 254 'hide_presence:bool', 255 'osu_playstyle:string[]', 256 'playmode:string', 257 'pm_friends_only:bool', 258 'user_notify:bool', 259 ]); 260 261 if (isset($userParams['playmode']) && !Beatmap::isModeValid($userParams['playmode'])) { 262 abort(422, 'invalid value specified for user[playmode]'); 263 } 264 265 $profileParams = get_params($params, 'user_profile_customization', [ 266 'audio_autoplay:bool', 267 'audio_muted:bool', 268 'audio_volume:float', 269 'beatmapset_card_size:string', 270 'beatmapset_download:string', 271 'beatmapset_show_nsfw:bool', 272 'beatmapset_title_show_original:bool', 273 'comments_show_deleted:bool', 274 'comments_sort:string', 275 'extras_order:string[]', 276 'forum_posts_show_deleted:bool', 277 'legacy_score_only:bool', 278 'profile_cover_expanded:bool', 279 'scoring_mode:string', 280 'user_list_filter:string', 281 'user_list_sort:string', 282 'user_list_view:string', 283 ]); 284 285 $profileCustomization = $user->userProfileCustomization()->createOrFirst(); 286 $user->setRelation('userProfileCustomization', $profileCustomization); 287 288 try { 289 if (!empty($userParams)) { 290 $user->fill($userParams)->saveOrExplode(); 291 } 292 293 if (!empty($profileParams)) { 294 $profileCustomization->fill($profileParams)->saveOrExplode(); 295 } 296 } catch (ModelNotSavedException $e) { 297 return ModelNotSavedException::makeResponse($e, [ 298 'user' => $user, 299 'user_profile_customization' => $profileCustomization, 300 ]); 301 } 302 303 return json_item($user, new CurrentUserTransformer()); 304 } 305 306 public function updatePassword() 307 { 308 $params = get_params(request()->all(), 'user', ['current_password', 'password', 'password_confirmation']); 309 $user = Auth::user()->validateCurrentPassword()->validatePasswordConfirmation(); 310 311 if ($user->update($params) === true) { 312 if (is_valid_email_format($user->user_email)) { 313 Mail::to($user)->send(new UserPasswordUpdated($user)); 314 } 315 316 $user->resetSessions(\Session::getId()); 317 318 return response([], 204); 319 } else { 320 return ModelNotSavedException::makeResponse(null, compact('user')); 321 } 322 } 323 324 public function verify() 325 { 326 return SessionVerification\Controller::verify(); 327 } 328 329 public function verifyLink() 330 { 331 return SessionVerification\Controller::verifyLink(); 332 } 333 334 public function reissueCode() 335 { 336 return SessionVerification\Controller::reissue(); 337 } 338}