This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | /** |
||
4 | * Minotaur |
||
5 | * |
||
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
||
7 | * use this file except in compliance with the License. You may obtain a copy of |
||
8 | * the License at |
||
9 | * |
||
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
11 | * |
||
12 | * Unless required by applicable law or agreed to in writing, software |
||
13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
||
14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
||
15 | * License for the specific language governing permissions and limitations under |
||
16 | * the License. |
||
17 | * |
||
18 | * @copyright 2015-2017 Appertly |
||
19 | * @license Apache-2.0 |
||
20 | */ |
||
21 | namespace Minotaur\Http; |
||
22 | |||
23 | use Psr\Http\Message\ServerRequestInterface as Request; |
||
24 | use Psr\Http\Message\ResponseInterface as Response; |
||
25 | use Caridea\Http\ProblemDetails; |
||
26 | |||
27 | /** |
||
28 | * A pretty basic contingency plan. |
||
29 | * |
||
30 | * Under normal circumstances, this class will simply return the response given |
||
31 | * by the `$next` function. In the event that an Exception occurred in the |
||
32 | * `$next` function, this class will craft a new response containing details |
||
33 | * about the error itself. |
||
34 | */ |
||
35 | class Rescuer implements \Minotaur\Route\Plugin |
||
36 | { |
||
37 | /** |
||
38 | * @var bool Whether to include exception information in responses |
||
39 | */ |
||
40 | protected $debug; |
||
41 | /** |
||
42 | * @var string The class name of the XHP to render |
||
43 | */ |
||
44 | protected $xhpClass; |
||
45 | |||
46 | /** |
||
47 | * Convenient map of HTTP status codes to human-readable explanations |
||
48 | */ |
||
49 | protected const MESSAGES = [ |
||
50 | 403 => "You are not allowed to perform this action.", |
||
51 | 404 => "We don't have anything at this URL. Double-check the URL you requested.", |
||
52 | 405 => "You can't use that HTTP method for this URL. Check the Allow response header for the ones you can.", |
||
53 | 406 => "We don't have any content available in the MIME type you specified in your Accept header. Try specifying additional MIME types.", |
||
54 | 422 => "There was a problem with the data you submitted. Review the messages for each field and try again.", |
||
55 | 423 => "This data is locked. You have permission, but it is no longer allowed to be changed.", |
||
56 | 500 => "It looks like we have a problem on our end! Our staff has been notified. Please try again later." |
||
57 | ]; |
||
58 | |||
59 | /** |
||
60 | * Creates a new Contingency. |
||
61 | * |
||
62 | * The following options are available: |
||
63 | * * `debug` – Whether to include exception stack trace information (*should be `false` in production!*). Defaults to `false`. |
||
64 | * * `xhpClass` – The class name of XHP to render (must be `xhp_class_name` format). Defaults to `xhp__labrys__error_page`. |
||
65 | * |
||
66 | * @param array<string,mixed> $options The options |
||
67 | */ |
||
68 | 10 | public function __construct(array $options = []) |
|
69 | { |
||
70 | 10 | $this->debug = (bool)($options['debug'] ?? false); |
|
71 | 10 | $c = (string)($options['xhpClass'] ?? 'labrys_error_page'); |
|
72 | 10 | if (!is_subclass_of($c, \Minotaur\Tags\Primitive::class)) { |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
73 | throw new \InvalidArgumentException("Class given in xhpClass option '$c' does not extend \Minotaur\Tags\Node"); |
||
74 | } |
||
75 | 10 | $this->xhpClass = $c; |
|
76 | 10 | } |
|
77 | |||
78 | /** |
||
79 | * Gets the plugin priority; larger means first. |
||
80 | * |
||
81 | * @return - The plugin priority |
||
0 ignored issues
–
show
The doc-type
- could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types. ![]() |
|||
82 | */ |
||
83 | public function getPriority(): int |
||
84 | { |
||
85 | return PHP_INT_MAX; |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * Middleware request–response handling. |
||
90 | * |
||
91 | * @param $request - The server request |
||
92 | * @param $response - The response |
||
93 | * @param callable $next - The next layer. (function (Request,Response): Response) |
||
94 | * @return - The response |
||
0 ignored issues
–
show
The doc-type
- could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types. ![]() |
|||
95 | */ |
||
96 | 10 | public function __invoke(Request $request, Response $response, callable $next): Response |
|
97 | { |
||
98 | try { |
||
99 | 10 | return $next($request, $response); |
|
100 | 10 | } catch (\Exception $e) { |
|
101 | 10 | return $this->process($request, $response, $e); |
|
102 | } |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Handles an exception. |
||
107 | * |
||
108 | * This is your chance for logging, changing the HTTP status header, and |
||
109 | * rendering some kind of message for the client. |
||
110 | * |
||
111 | * @param $request - The request |
||
112 | * @param $response - The response |
||
113 | * @param $e - The exception to process |
||
114 | * @return - The new response |
||
0 ignored issues
–
show
The doc-type
- could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types. ![]() |
|||
115 | */ |
||
116 | 10 | public function process(Request $request, Response $response, \Exception $e) : Response |
|
117 | { |
||
118 | 10 | if ($e instanceof \Minotaur\Route\Exception\Unroutable) { |
|
119 | 3 | $response = $response->withStatus($e->getCode(), $e->getMessage()); |
|
120 | 3 | foreach ($e->getHeaders() as $k => $v) { |
|
121 | 3 | $response = $response->withHeader($k, $v); |
|
122 | } |
||
123 | 7 | } elseif ($e instanceof \Caridea\Acl\Exception\Forbidden) { |
|
124 | 1 | $response = $response->withStatus(403, 'Forbidden'); |
|
125 | 6 | } elseif ($e instanceof \Caridea\Dao\Exception\Unretrievable || |
|
126 | 6 | $e instanceof \Caridea\Acl\Exception\Unloadable) { |
|
127 | 1 | $response = $response->withStatus(404, 'Not Found'); |
|
128 | 5 | } elseif ($e instanceof \Caridea\Dao\Exception\Conflicting || |
|
129 | 5 | $e instanceof \Caridea\Dao\Exception\Duplicative) { |
|
130 | 2 | $response = $response->withStatus(409, 'Conflict'); |
|
131 | 3 | } elseif ($e instanceof \Caridea\Validate\Exception\Invalid) { |
|
132 | 1 | $response = $response->withStatus(422, 'Unprocessable Entity'); |
|
133 | 2 | } elseif ($e instanceof \Caridea\Dao\Exception\Locked) { |
|
134 | 1 | $response = $response->withStatus(423, 'Locked'); |
|
135 | } else { |
||
136 | 1 | $response = $response->withStatus(500, 'Internal Server Error'); |
|
137 | } |
||
138 | 10 | $values = $this->getValues($request, $e); |
|
139 | 10 | $types = new \Caridea\Http\AcceptTypes($request->getServerParams()); |
|
140 | 10 | switch ($types->preferred(['application/json', ProblemDetails::MIME_TYPE_JSON, 'text/html'])) { |
|
141 | /* HH_IGNORE_ERROR[4110]: Not sure why hh_client has a problem with this */ |
||
142 | 10 | case ProblemDetails::MIME_TYPE_JSON: |
|
0 ignored issues
–
show
case statements should be defined using a colon.
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces.
There is no need for braces, since each case is terminated by the next There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages. switch ($expr) {
case "A": { //wrong
doSomething();
break;
}
case "B"; //wrong
doSomething();
break;
case "C": //right
doSomething();
break;
}
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig. ![]() |
|||
143 | /* HH_IGNORE_ERROR[4110]: Not sure why hh_client has a problem with this */ |
||
144 | 10 | case 'application/json': |
|
145 | 8 | $response = $response->withHeader('Content-Type', ProblemDetails::MIME_TYPE_JSON); |
|
146 | 8 | $response->getBody()->write((string)$this->renderJson($values)); |
|
147 | 8 | break; |
|
148 | default: |
||
149 | 2 | $response = $response->withHeader('Content-Type', 'text/html'); |
|
150 | 2 | $response->getBody()->write((string)$this->renderHtml($values)); |
|
151 | } |
||
152 | 10 | return $response; |
|
153 | } |
||
154 | |||
155 | /** |
||
156 | * Assembles the values from the Request and Exception. |
||
157 | * |
||
158 | * @param $request - The request |
||
159 | * @param $e - The Exception |
||
160 | * @return array<string,mixed> The assembled values |
||
161 | */ |
||
162 | 10 | protected function getValues(Request $request, \Exception $e): array |
|
0 ignored issues
–
show
|
|||
163 | { |
||
164 | 10 | $values = []; |
|
165 | 10 | $extra = ['success' => false]; |
|
166 | 10 | if ($this->debug) { |
|
167 | 10 | $extra['exception'] = $this->getStackTrace($e); |
|
168 | } |
||
169 | 10 | if ($e instanceof \Minotaur\Route\Exception\Unroutable) { |
|
170 | 3 | $code = $e->getCode(); |
|
171 | 3 | $values['title'] = $e->getMessage(); |
|
172 | 3 | $values['status'] = $code; |
|
173 | 3 | $values['detail'] = self::MESSAGES[$code]; |
|
174 | 7 | } elseif ($e instanceof \Caridea\Acl\Exception\Forbidden) { |
|
175 | 1 | $values['title'] = 'Access Denied'; |
|
176 | 1 | $values['status'] = 403; |
|
177 | 1 | $values['detail'] = self::MESSAGES[403]; |
|
178 | 6 | } elseif ($e instanceof \Caridea\Dao\Exception\Unretrievable || |
|
179 | 6 | $e instanceof \Caridea\Acl\Exception\Unloadable) { |
|
180 | 1 | $values['title'] = 'Resource Not Found'; |
|
181 | 1 | $values['status'] = 404; |
|
182 | 1 | $values['detail'] = self::MESSAGES[404]; |
|
183 | 5 | } elseif ($e instanceof \Caridea\Dao\Exception\Duplicative) { |
|
184 | 1 | $values['title'] = 'Constraint Violation'; |
|
185 | 1 | $values['status'] = 409; |
|
186 | 1 | $values['detail'] = 'The data you submitted violates unique constraints. Most likely, this is a result of an existing record with similar data. Double-check existing records and try again.'; |
|
187 | 4 | } elseif ($e instanceof \Caridea\Dao\Exception\Conflicting) { |
|
188 | 1 | $values['title'] = 'Concurrent Modification'; |
|
189 | 1 | $values['status'] = 409; |
|
190 | 1 | $values['detail'] = 'Someone else saved changes to this same data while you were editing. Try your request again using the latest copy of the record.'; |
|
191 | 3 | } elseif ($e instanceof \Caridea\Validate\Exception\Invalid) { |
|
192 | 1 | $values['title'] = 'Data Validation Failure'; |
|
193 | 1 | $values['status'] = 422; |
|
194 | 1 | $values['detail'] = self::MESSAGES[422]; |
|
195 | 1 | $errors = []; |
|
196 | 1 | foreach ($e->getErrors() as $field => $code) { |
|
197 | 1 | $errors[] = ['field' => $field, 'code' => $code]; |
|
198 | } |
||
199 | 1 | $extra['errors'] = $errors; |
|
200 | 2 | } elseif ($e instanceof \Caridea\Dao\Exception\Locked) { |
|
201 | 1 | $values['title'] = 'Resource Locked'; |
|
202 | 1 | $values['status'] = 423; |
|
203 | 1 | $values['detail'] = self::MESSAGES[423]; |
|
204 | } else { |
||
205 | 1 | $values['title'] = 'Internal Server Error'; |
|
206 | 1 | $values['status'] = 500; |
|
207 | 1 | $values['detail'] = self::MESSAGES[500]; |
|
208 | } |
||
209 | 10 | $values['extra'] = $extra; |
|
210 | 10 | return $values; |
|
211 | } |
||
212 | |||
213 | /** |
||
214 | * Gets an exception stack trace as a string, including nested exceptions. |
||
215 | * |
||
216 | * @param $e - The exception |
||
217 | * @return array<string,string> The full stack trace |
||
218 | */ |
||
219 | 10 | private function getStackTrace(\Exception $e): array |
|
220 | { |
||
221 | $details = [ |
||
222 | 10 | 'class' => get_class($e), |
|
223 | 10 | 'message' => $e->getMessage(), |
|
224 | 10 | 'stack' => $e->getTraceAsString() |
|
225 | ]; |
||
226 | 10 | if ($e->getPrevious() !== null) { |
|
227 | 1 | $details['previous'] = $this->getStackTrace($e->getPrevious()); |
|
228 | } |
||
229 | 10 | return $details; |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * Returns the ProblemDetails to render. |
||
234 | * |
||
235 | * @param array<string,mixed> $values The values |
||
236 | * @return ProblemDetails The JSON response |
||
237 | */ |
||
238 | 8 | protected function renderJson(array $values): ProblemDetails |
|
239 | { |
||
240 | 8 | $extra = $values['extra'] ?? []; |
|
241 | 8 | return new ProblemDetails( |
|
242 | 8 | null, |
|
243 | 8 | (string) $values['title'], |
|
244 | 8 | (int) $values['status'], |
|
245 | 8 | (string) $values['detail'], |
|
246 | 8 | null, |
|
247 | 8 | $extra |
|
248 | ); |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Returns the HTML to render. |
||
253 | * |
||
254 | * @param array<string,mixed> $values The values |
||
255 | * @return \Minotaur\Tags\Node The HTML response |
||
256 | */ |
||
257 | 2 | protected function renderHtml(array $values): \Minotaur\Tags\Node |
|
258 | { |
||
259 | 2 | return \Minotaur\Tags\fcomposited($this->xhpClass, ['values' => $values]); |
|
260 | } |
||
261 | } |
||
262 |