These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace ByJG\RestServer; |
||
4 | |||
5 | use ByJG\Cache\Psr16\NoCacheEngine; |
||
6 | use ByJG\RestServer\Exception\ClassNotFoundException; |
||
7 | use ByJG\RestServer\Exception\Error404Exception; |
||
8 | use ByJG\RestServer\Exception\Error405Exception; |
||
9 | use ByJG\RestServer\Exception\Error520Exception; |
||
10 | use ByJG\RestServer\Exception\InvalidClassException; |
||
11 | use ByJG\RestServer\Exception\OperationIdInvalidException; |
||
12 | use ByJG\RestServer\Exception\SchemaInvalidException; |
||
13 | use ByJG\RestServer\Exception\SchemaNotFoundException; |
||
14 | use ByJG\RestServer\HandleOutput\HandleOutputInterface; |
||
15 | use ByJG\RestServer\HandleOutput\HtmlHandler; |
||
16 | use ByJG\RestServer\HandleOutput\JsonHandler; |
||
17 | use ByJG\RestServer\HandleOutput\XmlHandler; |
||
18 | use FastRoute\Dispatcher; |
||
19 | use FastRoute\RouteCollector; |
||
20 | use Psr\SimpleCache\CacheInterface; |
||
21 | |||
22 | class ServerRequestHandler |
||
23 | { |
||
24 | const OK = "OK"; |
||
25 | const METHOD_NOT_ALLOWED = "NOT_ALLOWED"; |
||
26 | const NOT_FOUND = "NOT FOUND"; |
||
27 | |||
28 | protected $routes = null; |
||
29 | |||
30 | protected $defaultHandler = null; |
||
31 | |||
32 | protected $mimeTypeHandler = [ |
||
33 | "text/xml" => XmlHandler::class, |
||
34 | "application/xml" => XmlHandler::class, |
||
35 | "text/html" => HtmlHandler::class, |
||
36 | "application/json" => JsonHandler::class |
||
37 | ]; |
||
38 | |||
39 | protected $pathHandler = [ |
||
40 | |||
41 | ]; |
||
42 | |||
43 | public function getRoutes() |
||
44 | { |
||
45 | return $this->routes; |
||
46 | } |
||
47 | |||
48 | /** |
||
49 | * @param \ByJG\RestServer\RoutePattern[] $routes |
||
50 | */ |
||
51 | public function setRoutes($routes) |
||
52 | { |
||
53 | foreach ((array)$routes as $route) { |
||
54 | $this->addRoute($route); |
||
55 | } |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * @param \ByJG\RestServer\RoutePattern $route |
||
60 | */ |
||
61 | public function addRoute(RoutePattern $route) |
||
62 | { |
||
63 | if (is_null($this->routes)) { |
||
64 | $this->routes = []; |
||
65 | } |
||
66 | $this->routes[] = $route; |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * @return HandleOutputInterface |
||
71 | */ |
||
72 | public function getDefaultHandler() |
||
73 | { |
||
74 | if (empty($this->defaultHandler)) { |
||
75 | $this->defaultHandler = new JsonHandler(); |
||
76 | } |
||
77 | return $this->defaultHandler; |
||
78 | } |
||
79 | |||
80 | /** |
||
81 | * @param HandleOutputInterface $defaultHandler |
||
82 | */ |
||
83 | public function setDefaultHandler(HandleOutputInterface $defaultHandler) |
||
84 | { |
||
85 | $this->defaultHandler = $defaultHandler; |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * @throws \ByJG\RestServer\Exception\ClassNotFoundException |
||
90 | * @throws \ByJG\RestServer\Exception\Error404Exception |
||
91 | * @throws \ByJG\RestServer\Exception\Error405Exception |
||
92 | * @throws \ByJG\RestServer\Exception\Error520Exception |
||
93 | * @throws \ByJG\RestServer\Exception\InvalidClassException |
||
94 | */ |
||
95 | protected function process() |
||
96 | { |
||
97 | // Initialize ErrorHandler with default error handler |
||
98 | ErrorHandler::getInstance()->register(); |
||
99 | |||
100 | // Get the URL parameters |
||
101 | $httpMethod = $_SERVER['REQUEST_METHOD']; |
||
102 | $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); |
||
103 | parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr); |
||
104 | |||
105 | // Generic Dispatcher for RestServer |
||
106 | $dispatcher = \FastRoute\simpleDispatcher(function (RouteCollector $r) { |
||
107 | |||
108 | foreach ($this->getRoutes() as $route) { |
||
109 | $r->addRoute( |
||
110 | $route->properties('method'), |
||
111 | $route->properties('pattern'), |
||
112 | [ |
||
113 | "handler" => $route->properties('handler'), |
||
114 | "class" => $route->properties('class'), |
||
115 | "function" => $route->properties('function') |
||
116 | ] |
||
117 | ); |
||
118 | } |
||
119 | }); |
||
120 | |||
121 | $routeInfo = $dispatcher->dispatch($httpMethod, $uri); |
||
0 ignored issues
–
show
|
|||
122 | |||
123 | // Default Handler for errors |
||
124 | $this->getDefaultHandler()->writeHeader(); |
||
125 | ErrorHandler::getInstance()->setHandler($this->getDefaultHandler()->getErrorHandler()); |
||
126 | |||
127 | // Processing |
||
128 | switch ($routeInfo[0]) { |
||
129 | case Dispatcher::NOT_FOUND: |
||
130 | throw new Error404Exception("404 Route '$uri' Not found"); |
||
131 | |||
132 | case Dispatcher::METHOD_NOT_ALLOWED: |
||
133 | throw new Error405Exception('405 Method Not Allowed'); |
||
134 | |||
135 | case Dispatcher::FOUND: |
||
136 | // ... 200 Process: |
||
137 | $vars = array_merge($routeInfo[2], $queryStr); |
||
138 | |||
139 | // Instantiate the Service Handler |
||
140 | $handlerRequest = $routeInfo[1]; |
||
141 | |||
142 | // Execute the request |
||
143 | $handler = !empty($handlerRequest['handler']) ? $handlerRequest['handler'] : $this->getDefaultHandler(); |
||
144 | $this->executeRequest( |
||
145 | new $handler(), |
||
146 | $handlerRequest['class'], |
||
147 | $handlerRequest['function'], |
||
148 | $vars |
||
149 | ); |
||
150 | |||
151 | break; |
||
152 | |||
153 | default: |
||
154 | throw new Error520Exception('Unknown'); |
||
155 | } |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * @param HandleOutputInterface $handler |
||
160 | * @param string $class |
||
161 | * @param string $function |
||
162 | * @param array $vars |
||
163 | * @throws \ByJG\RestServer\Exception\ClassNotFoundException |
||
164 | * @throws \ByJG\RestServer\Exception\InvalidClassException |
||
165 | */ |
||
166 | protected function executeRequest($handler, $class, $function, $vars) |
||
167 | { |
||
168 | // Setting Default Headers and Error Handler |
||
169 | $handler->writeHeader(); |
||
170 | ErrorHandler::getInstance()->setHandler($handler->getErrorHandler()); |
||
171 | |||
172 | // Set all default values |
||
173 | foreach (array_keys($vars) as $key) { |
||
174 | $_REQUEST[$key] = $_GET[$key] = $vars[$key]; |
||
175 | } |
||
176 | |||
177 | // Create the Request and Response methods |
||
178 | $request = new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE); |
||
179 | $response = new HttpResponse(); |
||
180 | |||
181 | // Process Closure |
||
182 | if ($function instanceof \Closure) { |
||
183 | $function($response, $request); |
||
184 | echo $handler->processResponse($response); |
||
185 | return; |
||
186 | } |
||
187 | |||
188 | // Process Class::Method() |
||
189 | if (!class_exists($class)) { |
||
190 | throw new ClassNotFoundException("Class '$class' defined in the route is not found"); |
||
191 | } |
||
192 | $instance = new $class(); |
||
193 | if (!method_exists($instance, $function)) { |
||
194 | throw new InvalidClassException("There is no method '$class::$function''"); |
||
195 | } |
||
196 | $instance->$function($response, $request); |
||
197 | $handler->processResponse($response); |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Handle the ROUTE (see web/app-dist.php) |
||
202 | * |
||
203 | * @param \ByJG\RestServer\RoutePattern[]|null $routePattern |
||
204 | * @param bool $outputBuffer |
||
205 | * @param bool $session |
||
206 | * @throws \ByJG\RestServer\Exception\ClassNotFoundException |
||
207 | * @throws \ByJG\RestServer\Exception\Error404Exception |
||
208 | * @throws \ByJG\RestServer\Exception\Error405Exception |
||
209 | * @throws \ByJG\RestServer\Exception\Error520Exception |
||
210 | * @throws \ByJG\RestServer\Exception\InvalidClassException |
||
211 | */ |
||
212 | public function handle($routePattern = null, $outputBuffer = true, $session = true) |
||
213 | { |
||
214 | if ($outputBuffer) { |
||
215 | ob_start(); |
||
216 | } |
||
217 | if ($session) { |
||
218 | session_start(); |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * @var ServerRequestHandler |
||
223 | */ |
||
224 | $this->setRoutes($routePattern); |
||
0 ignored issues
–
show
It seems like
$routePattern defined by parameter $routePattern on line 212 can also be of type null ; however, ByJG\RestServer\ServerRequestHandler::setRoutes() does only seem to accept array<integer,object<ByJ...stServer\RoutePattern>> , maybe add an additional type check?
This check looks at variables that have been passed in as parameters and are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble.
Loading history...
|
|||
225 | |||
226 | // -------------------------------------------------------------------------- |
||
227 | // Check if script exists or if is itself |
||
228 | // -------------------------------------------------------------------------- |
||
229 | |||
230 | $debugBacktrace = debug_backtrace(); |
||
231 | if (!empty($_SERVER['SCRIPT_FILENAME']) |
||
232 | && file_exists($_SERVER['SCRIPT_FILENAME']) |
||
233 | && basename($_SERVER['SCRIPT_FILENAME']) !== basename($debugBacktrace[0]['file']) |
||
234 | ) { |
||
235 | $file = $_SERVER['SCRIPT_FILENAME']; |
||
236 | if (strpos($file, '.php') !== false) { |
||
237 | require_once($file); |
||
238 | } else { |
||
239 | header("Content-Type: " . $this->mimeContentType($file)); |
||
240 | |||
241 | echo file_get_contents($file); |
||
242 | } |
||
243 | return; |
||
244 | } |
||
245 | |||
246 | $this->process(); |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * Get the Mime Type based on the filename |
||
251 | * |
||
252 | * @param string $filename |
||
253 | * @return string |
||
254 | */ |
||
255 | protected function mimeContentType($filename) |
||
256 | { |
||
257 | |||
258 | $mimeTypes = array( |
||
259 | 'txt' => 'text/plain', |
||
260 | 'htm' => 'text/html', |
||
261 | 'html' => 'text/html', |
||
262 | 'php' => 'text/html', |
||
263 | 'css' => 'text/css', |
||
264 | 'js' => 'application/javascript', |
||
265 | 'json' => 'application/json', |
||
266 | 'xml' => 'application/xml', |
||
267 | 'swf' => 'application/x-shockwave-flash', |
||
268 | 'flv' => 'video/x-flv', |
||
269 | // images |
||
270 | 'png' => 'image/png', |
||
271 | 'jpe' => 'image/jpeg', |
||
272 | 'jpeg' => 'image/jpeg', |
||
273 | 'jpg' => 'image/jpeg', |
||
274 | 'gif' => 'image/gif', |
||
275 | 'bmp' => 'image/bmp', |
||
276 | 'ico' => 'image/vnd.microsoft.icon', |
||
277 | 'tiff' => 'image/tiff', |
||
278 | 'tif' => 'image/tiff', |
||
279 | 'svg' => 'image/svg+xml', |
||
280 | 'svgz' => 'image/svg+xml', |
||
281 | // archives |
||
282 | 'zip' => 'application/zip', |
||
283 | 'rar' => 'application/x-rar-compressed', |
||
284 | 'exe' => 'application/x-msdownload', |
||
285 | 'msi' => 'application/x-msdownload', |
||
286 | 'cab' => 'application/vnd.ms-cab-compressed', |
||
287 | // audio/video |
||
288 | 'mp3' => 'audio/mpeg', |
||
289 | 'qt' => 'video/quicktime', |
||
290 | 'mov' => 'video/quicktime', |
||
291 | // adobe |
||
292 | 'pdf' => 'application/pdf', |
||
293 | 'psd' => 'image/vnd.adobe.photoshop', |
||
294 | 'ai' => 'application/postscript', |
||
295 | 'eps' => 'application/postscript', |
||
296 | 'ps' => 'application/postscript', |
||
297 | // ms office |
||
298 | 'doc' => 'application/msword', |
||
299 | 'rtf' => 'application/rtf', |
||
300 | 'xls' => 'application/vnd.ms-excel', |
||
301 | 'ppt' => 'application/vnd.ms-powerpoint', |
||
302 | // open office |
||
303 | 'odt' => 'application/vnd.oasis.opendocument.text', |
||
304 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', |
||
305 | ); |
||
306 | |||
307 | $ext = strtolower(array_pop(explode('.', $filename))); |
||
0 ignored issues
–
show
|
|||
308 | if (array_key_exists($ext, $mimeTypes)) { |
||
309 | return $mimeTypes[$ext]; |
||
310 | } elseif (function_exists('finfo_open')) { |
||
311 | $finfo = finfo_open(FILEINFO_MIME); |
||
312 | $mimetype = finfo_file($finfo, $filename); |
||
313 | finfo_close($finfo); |
||
314 | return $mimetype; |
||
315 | } else { |
||
316 | return 'application/octet-stream'; |
||
317 | } |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * @param $swaggerJson |
||
322 | * @param \Psr\SimpleCache\CacheInterface|null $cache |
||
323 | * @throws \ByJG\RestServer\Exception\SchemaInvalidException |
||
324 | * @throws \ByJG\RestServer\Exception\SchemaNotFoundException |
||
325 | * @throws \ByJG\RestServer\Exception\OperationIdInvalidException |
||
326 | */ |
||
327 | public function setRoutesSwagger($swaggerJson, CacheInterface $cache = null) |
||
328 | { |
||
329 | if (!file_exists($swaggerJson)) { |
||
330 | throw new SchemaNotFoundException("Schema '$swaggerJson' not found"); |
||
331 | } |
||
332 | |||
333 | $schema = json_decode(file_get_contents($swaggerJson), true); |
||
334 | if (!isset($schema['paths'])) { |
||
335 | throw new SchemaInvalidException("Schema '$swaggerJson' is invalid"); |
||
336 | } |
||
337 | |||
338 | if (is_null($cache)) { |
||
339 | $cache = new NoCacheEngine(); |
||
340 | } |
||
341 | |||
342 | $routePattern = $cache->get('SERVERHANDLERROUTES', false); |
||
343 | if ($routePattern === false) { |
||
344 | $routePattern = $this->generateRoutes($schema); |
||
345 | $cache->set('SERVERHANDLERROUTES', $routePattern); |
||
346 | } |
||
347 | $this->setRoutes($routePattern); |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * @param $schema |
||
352 | * @return array |
||
353 | * @throws \ByJG\RestServer\Exception\OperationIdInvalidException |
||
354 | */ |
||
355 | protected function generateRoutes($schema) |
||
356 | { |
||
357 | $pathList = $this->sortPaths(array_keys($schema['paths'])); |
||
358 | |||
359 | $routes = []; |
||
360 | foreach ($pathList as $path) { |
||
361 | foreach ($schema['paths'][$path] as $method => $properties) { |
||
362 | $handler = $this->getMethodHandler($method, $path, $properties); |
||
363 | if (!isset($properties['operationId'])) { |
||
364 | throw new OperationIdInvalidException('OperationId was not found'); |
||
365 | } |
||
366 | |||
367 | $parts = explode('::', $properties['operationId']); |
||
368 | if (count($parts) !== 2) { |
||
369 | throw new OperationIdInvalidException( |
||
370 | 'OperationId needs to be in the format Namespace\\class::method' |
||
371 | ); |
||
372 | } |
||
373 | |||
374 | $routes[] = new RoutePattern( |
||
375 | strtoupper($method), |
||
376 | $path, |
||
377 | $handler, |
||
378 | $parts[1], |
||
379 | $parts[0] |
||
380 | ); |
||
381 | } |
||
382 | } |
||
383 | |||
384 | return $routes; |
||
385 | } |
||
386 | |||
387 | protected function sortPaths($pathList) |
||
388 | { |
||
389 | usort($pathList, function ($left, $right) { |
||
390 | View Code Duplication | if (strpos($left, '{') === false && strpos($right, '{') !== false) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
391 | return -16384; |
||
392 | } |
||
393 | View Code Duplication | if (strpos($left, '{') !== false && strpos($right, '{') === false) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
394 | return 16384; |
||
395 | } |
||
396 | if (strpos($left, $right) !== false) { |
||
397 | return -16384; |
||
398 | } |
||
399 | if (strpos($right, $left) !== false) { |
||
400 | return 16384; |
||
401 | } |
||
402 | return strcmp($left, $right); |
||
403 | }); |
||
404 | |||
405 | return $pathList; |
||
406 | } |
||
407 | |||
408 | /** |
||
409 | * @param $method |
||
410 | * @param $path |
||
411 | * @param $properties |
||
412 | * @return string |
||
413 | * @throws \ByJG\RestServer\Exception\OperationIdInvalidException |
||
414 | */ |
||
415 | protected function getMethodHandler($method, $path, $properties) |
||
416 | { |
||
417 | $method = strtoupper($method); |
||
418 | if (isset($this->pathHandler["$method::$path"])) { |
||
419 | return $this->pathHandler["$method::$path"]; |
||
420 | } |
||
421 | if (!isset($properties['produces'])) { |
||
422 | return get_class($this->getDefaultHandler()); |
||
423 | } |
||
424 | |||
425 | $produces = $properties['produces']; |
||
426 | if (is_array($produces)) { |
||
427 | $produces = $produces[0]; |
||
428 | } |
||
429 | |||
430 | if (!isset($this->mimeTypeHandler[$produces])) { |
||
431 | throw new OperationIdInvalidException("There is no handler for $produces"); |
||
432 | } |
||
433 | |||
434 | return $this->mimeTypeHandler[$produces]; |
||
435 | } |
||
436 | |||
437 | public function setMimeTypeHandler($mimetype, $handler) |
||
438 | { |
||
439 | $this->mimeTypeHandler[$mimetype] = $handler; |
||
440 | } |
||
441 | |||
442 | public function setPathHandler($method, $path, $handler) |
||
443 | { |
||
444 | $method = strtoupper($method); |
||
445 | $this->pathHandler["$method::$path"] = $handler; |
||
446 | } |
||
447 | } |
||
448 |
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.