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\Exceptions;
7
8use App\Libraries\SessionVerification;
9use Illuminate\Auth\Access\AuthorizationException as LaravelAuthorizationException;
10use Illuminate\Auth\AuthenticationException;
11use Illuminate\Database\Eloquent\ModelNotFoundException;
12use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
13use Illuminate\Http\Exceptions\HttpResponseException;
14use Illuminate\Session\TokenMismatchException;
15use Illuminate\View\ViewException;
16use Laravel\Passport\Exceptions\MissingScopeException;
17use Laravel\Passport\Exceptions\OAuthServerException as PassportOAuthServerException;
18use League\OAuth2\Server\Exception\OAuthServerException;
19use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20use Throwable;
21
22class Handler extends ExceptionHandler
23{
24 /**
25 * A list of the exception types that should not be reported.
26 *
27 * @var array
28 */
29 protected $dontReport = [
30 // laravel's
31 AuthenticationException::class,
32 LaravelAuthorizationException::class,
33 ModelNotFoundException::class,
34 TokenMismatchException::class,
35 \Illuminate\Validation\ValidationException::class,
36 \Laravel\Octane\Exceptions\DdException::class,
37 \Symfony\Component\HttpKernel\Exception\HttpException::class,
38
39 // local
40 AuthorizationException::class,
41 SilencedException::class,
42 VerificationRequiredException::class,
43
44 // oauth
45 OAuthServerException::class,
46 ];
47
48 public static function exceptionMessage($e)
49 {
50 if ($e instanceof ModelNotFoundException) {
51 return static::modelNotFoundMessage($e);
52 }
53
54 if (static::statusCode($e) >= 500) {
55 return;
56 }
57
58 return presence($e->getMessage());
59 }
60
61 public static function statusCode($e)
62 {
63 if (method_exists($e, 'getStatusCode')) {
64 return $e->getStatusCode();
65 } elseif ($e instanceof ModelNotFoundException) {
66 return 404;
67 } elseif ($e instanceof NotFoundHttpException) {
68 return 404;
69 } elseif ($e instanceof TokenMismatchException) {
70 return 403;
71 } elseif ($e instanceof AuthenticationException) {
72 return 401;
73 } elseif ($e instanceof AuthorizationException || $e instanceof MissingScopeException) {
74 return 403;
75 } elseif (static::isOAuthSessionException($e)) {
76 return 422;
77 } else {
78 return 500;
79 }
80 }
81
82 private static function isOAuthServerException($e)
83 {
84 return ($e instanceof PassportOAuthServerException) && ($e->getPrevious() instanceof OAuthServerException);
85 }
86
87 private static function isOAuthSessionException(Throwable $e): bool
88 {
89 return ($e instanceof \Exception)
90 && $e->getMessage() === 'Authorization request was not present in the session.';
91 }
92
93 private static function modelNotFoundMessage(ModelNotFoundException $e): string
94 {
95 $model = $e->getModel();
96 $modelTransKey = "models.name.{$model}";
97
98 $params = [
99 'model' => trans_exists($modelTransKey, $GLOBALS['cfg']['app']['fallback_locale'])
100 ? osu_trans($modelTransKey)
101 : trim(strtr($model, ['App\Models\\' => '']), '\\'),
102 ];
103
104 return osu_trans('models.not_found', $params);
105 }
106
107 private static function reportWithSentry(Throwable $e): void
108 {
109 $ref = log_error_sentry($e, ['http_code' => (string) static::statusCode($e)]);
110
111 if ($ref !== null) {
112 \Request::instance()->attributes->set('ref', $ref);
113 }
114 }
115
116 private static function unwrapViewException(Throwable $e): Throwable
117 {
118 if ($e instanceof ViewException) {
119 $i = 0;
120 while ($e instanceof ViewException) {
121 $e = $e->getPrevious();
122 if (++$i > 10) {
123 break;
124 }
125 }
126 }
127
128 return $e;
129 }
130
131 /**
132 * Report or log an exception.
133 *
134 * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
135 *
136 * @param \Throwable $e
137 *
138 * @return void
139 */
140 public function report(Throwable $e)
141 {
142 // immediately done if the error should not be reported
143 if ($this->shouldntReport($e)) {
144 return;
145 }
146
147 static::reportWithSentry($e);
148
149 parent::report($e);
150 }
151
152 /**
153 * Render an exception into an HTTP response.
154 *
155 * @param \Illuminate\Http\Request $request
156 * @param \Throwable $e
157 *
158 * @return \Illuminate\Http\Response
159 */
160 public function render($request, Throwable $e)
161 {
162 $e = static::unwrapViewException($e);
163
164 if (static::isOAuthServerException($e)) {
165 return parent::render($request, $e);
166 }
167
168 if ($e instanceof HttpResponseException || $e instanceof UserProfilePageLookupException) {
169 return $e->getResponse();
170 }
171
172 if ($e instanceof VerificationRequiredException) {
173 return SessionVerification\Controller::initiate();
174 }
175
176 if ($e instanceof AuthenticationException) {
177 return $this->unauthenticated($request, $e);
178 }
179
180 $statusCode = static::statusCode($e);
181
182 app('route-section')->setError($statusCode);
183
184 $isJsonRequest = is_json_request();
185
186 if ($GLOBALS['cfg']['app']['debug']) {
187 $response = parent::render($request, $e);
188 } else {
189 $message = static::exceptionMessage($e);
190
191 if ($isJsonRequest || $request->ajax()) {
192 $response = response(['error' => $message]);
193 } else {
194 $response = ext_view('layout.error', [
195 'exceptionMessage' => $message,
196 'statusCode' => $statusCode,
197 ]);
198 }
199 }
200
201 return $response->setStatusCode(static::statusCode($e));
202 }
203
204 protected function shouldntReport(Throwable $e)
205 {
206 $e = static::unwrapViewException($e);
207
208 return parent::shouldntReport($e) || static::isOAuthServerException($e) || static::isOAuthSessionException($e);
209 }
210
211 protected function unauthenticated($request, AuthenticationException $exception)
212 {
213 if (is_json_request() || $request->ajax()) {
214 return response(['authentication' => 'basic'], 401);
215 }
216
217 return ext_view('users.login', null, null, 401);
218 }
219}