ServerRequestHandler::setDefaultHandler()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 ByJG\Util\Uri;
19
use Closure;
20
use FastRoute\Dispatcher;
21
use FastRoute\RouteCollector;
22
use Psr\SimpleCache\CacheInterface;
23
use Psr\SimpleCache\InvalidArgumentException;
24
25
class ServerRequestHandler
26
{
27
    const OK = "OK";
28
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
29
    const NOT_FOUND = "NOT FOUND";
30
31
    protected $routes = null;
32
33
    protected $defaultHandler = null;
34
35
    protected $mimeTypeHandler = [
36
        "text/xml" => XmlHandler::class,
37
        "application/xml" => XmlHandler::class,
38
        "text/html" => HtmlHandler::class,
39
        "application/json" => JsonHandler::class
40
    ];
41
42
    protected $pathHandler = [
43
44
    ];
45
46
    public function getRoutes()
47
    {
48
        return $this->routes;
49
    }
50
51
    /**
52
     * @param RoutePattern[] $routes
53
     */
54
    public function setRoutes($routes)
55
    {
56
        foreach ((array)$routes as $route) {
57
            $this->addRoute($route);
58
        }
59
    }
60
61
    /**
62
     * @param RoutePattern $route
63
     */
64
    public function addRoute(RoutePattern $route)
65
    {
66
        if (is_null($this->routes)) {
67
            $this->routes = [];
68
        }
69
        $this->routes[] = $route;
70
    }
71
72
    /**
73
     * @return HandleOutputInterface
74
     */
75
    public function getDefaultHandler()
76
    {
77
        if (empty($this->defaultHandler)) {
78
            $this->defaultHandler = new JsonHandler();
79
        }
80
        return $this->defaultHandler;
81
    }
82
83
    /**
84
     * @param HandleOutputInterface $defaultHandler
85
     */
86
    public function setDefaultHandler(HandleOutputInterface $defaultHandler)
87
    {
88
        $this->defaultHandler = $defaultHandler;
89
    }
90
91
    /**
92
     * @throws ClassNotFoundException
93
     * @throws Error404Exception
94
     * @throws Error405Exception
95
     * @throws Error520Exception
96
     * @throws InvalidClassException
97
     */
98
    protected function process()
99
    {
100
        // Initialize ErrorHandler with default error handler
101
        ErrorHandler::getInstance()->register();
102
103
        // Get the URL parameters
104
        $httpMethod = $_SERVER['REQUEST_METHOD'];
105
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
106
        parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr);
107
108
        // Generic Dispatcher for RestServer
109
        $dispatcher = \FastRoute\simpleDispatcher(function (RouteCollector $r) {
110
111
            foreach ($this->getRoutes() as $route) {
112
                $r->addRoute(
113
                    $route->properties('method'),
114
                    $route->properties('pattern'),
115
                    [
116
                        "handler" => $route->properties('handler'),
117
                        "class" => $route->properties('class'),
118
                        "function" => $route->properties('function')
119
                    ]
120
                );
121
            }
122
        });
123
124
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
0 ignored issues
show
Security Bug introduced by João Gilberto Magalhães
It seems like $uri defined by parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) on line 105 can also be of type false; however, FastRoute\Dispatcher::dispatch() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

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 returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
125
126
        // Default Handler for errors
127
        $this->getDefaultHandler()->writeHeader();
128
        ErrorHandler::getInstance()->setHandler($this->getDefaultHandler()->getErrorHandler());
129
130
        // Processing
131
        switch ($routeInfo[0]) {
132
            case Dispatcher::NOT_FOUND:
133
                throw new Error404Exception("404 Route '$uri' Not found");
134
135
            case Dispatcher::METHOD_NOT_ALLOWED:
136
                throw new Error405Exception('405 Method Not Allowed');
137
138
            case Dispatcher::FOUND:
139
                // ... 200 Process:
140
                $vars = array_merge($routeInfo[2], $queryStr);
141
142
                // Instantiate the Service Handler
143
                $handlerRequest = $routeInfo[1];
144
145
                // Execute the request
146
                $handler = !empty($handlerRequest['handler']) ? $handlerRequest['handler'] : $this->getDefaultHandler();
147
                $this->executeRequest(
148
                    new $handler(),
149
                    $handlerRequest['class'],
150
                    $handlerRequest['function'],
151
                    $vars
152
                );
153
154
                break;
155
156
            default:
157
                throw new Error520Exception('Unknown');
158
        }
159
    }
160
161
    /**
162
     * @param HandleOutputInterface $handler
163
     * @param string $class
164
     * @param string $function
165
     * @param array $vars
166
     * @throws ClassNotFoundException
167
     * @throws InvalidClassException
168
     */
169
    protected function executeRequest($handler, $class, $function, $vars)
170
    {
171
        // Setting Default Headers and Error Handler
172
        $handler->writeHeader();
173
        ErrorHandler::getInstance()->setHandler($handler->getErrorHandler());
174
175
        // Set all default values
176
        foreach (array_keys($vars) as $key) {
177
            $_REQUEST[$key] = $_GET[$key] = $vars[$key];
178
        }
179
180
        // Create the Request and Response methods
181
        $request = new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE);
182
        $response = new HttpResponse();
183
184
        // Process Closure
185
        if ($function instanceof Closure) {
186
            $function($response, $request);
187
            echo $handler->processResponse($response);
188
            return;
189
        }
190
191
        // Process Class::Method()
192
        if (!class_exists($class)) {
193
            throw new ClassNotFoundException("Class '$class' defined in the route is not found");
194
        }
195
        $instance = new $class();
196
        if (!method_exists($instance, $function)) {
197
            throw new InvalidClassException("There is no method '$class::$function''");
198
        }
199
        $instance->$function($response, $request);
200
        $handler->processResponse($response);
201
    }
202
203
    /**
204
     * Handle the ROUTE (see web/app-dist.php)
205
     *
206
     * @param RoutePattern[]|null $routePattern
207
     * @param bool $outputBuffer
208
     * @param bool $session
209
     * @return bool|void
210
     * @throws ClassNotFoundException
211
     * @throws Error404Exception
212
     * @throws Error405Exception
213
     * @throws Error520Exception
214
     * @throws InvalidClassException
215
     */
216
    public function handle($routePattern = null, $outputBuffer = true, $session = true)
217
    {
218
        if ($outputBuffer) {
219
            ob_start();
220
        }
221
        if ($session) {
222
            session_start();
223
        }
224
225
        /**
226
         * @var ServerRequestHandler
227
         */
228
        $this->setRoutes($routePattern);
0 ignored issues
show
Bug introduced by Joao Gilberto Magalhaes
It seems like $routePattern defined by parameter $routePattern on line 216 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...
229
230
        // --------------------------------------------------------------------------
231
        // Check if script exists or if is itself
232
        // --------------------------------------------------------------------------
233
234
        $debugBacktrace =  debug_backtrace();
235
        if (!empty($_SERVER['SCRIPT_FILENAME'])
236
            && file_exists($_SERVER['SCRIPT_FILENAME'])
237
            && basename($_SERVER['SCRIPT_FILENAME']) !== basename($debugBacktrace[0]['file'])
238
        ) {
239
            $file = $_SERVER['SCRIPT_FILENAME'];
240
            if (strrchr($file, '.') === ".php") {
241
                require_once($file);
242
            } else {
243
                if (!defined("RESTSERVER_TEST")) {
244
                    header("Content-Type: " . $this->mimeContentType($file));
245
                }
246
247
                echo file_get_contents($file);
248
            }
249
            return true;
250
        }
251
252
        return $this->process();
253
    }
254
255
    /**
256
     * Get the Mime Type based on the filename
257
     *
258
     * @param string $filename
259
     * @return string
260
     * @throws Error404Exception
261
     */
262
    public function mimeContentType($filename)
263
    {
264
265
        $mimeTypes = array(
266
            'txt' => 'text/plain',
267
            'htm' => 'text/html',
268
            'html' => 'text/html',
269
            'php' => 'text/html',
270
            'css' => 'text/css',
271
            'js' => 'application/javascript',
272
            'json' => 'application/json',
273
            'xml' => 'application/xml',
274
            'swf' => 'application/x-shockwave-flash',
275
            'flv' => 'video/x-flv',
276
            // images
277
            'png' => 'image/png',
278
            'jpe' => 'image/jpeg',
279
            'jpeg' => 'image/jpeg',
280
            'jpg' => 'image/jpeg',
281
            'gif' => 'image/gif',
282
            'bmp' => 'image/bmp',
283
            'ico' => 'image/vnd.microsoft.icon',
284
            'tiff' => 'image/tiff',
285
            'tif' => 'image/tiff',
286
            'svg' => 'image/svg+xml',
287
            'svgz' => 'image/svg+xml',
288
            // archives
289
            'zip' => 'application/zip',
290
            'rar' => 'application/x-rar-compressed',
291
            'exe' => 'application/x-msdownload',
292
            'msi' => 'application/x-msdownload',
293
            'cab' => 'application/vnd.ms-cab-compressed',
294
            // audio/video
295
            'mp3' => 'audio/mpeg',
296
            'qt' => 'video/quicktime',
297
            'mov' => 'video/quicktime',
298
            // adobe
299
            'pdf' => 'application/pdf',
300
            'psd' => 'image/vnd.adobe.photoshop',
301
            'ai' => 'application/postscript',
302
            'eps' => 'application/postscript',
303
            'ps' => 'application/postscript',
304
            // ms office
305
            'doc' => 'application/msword',
306
            'rtf' => 'application/rtf',
307
            'xls' => 'application/vnd.ms-excel',
308
            'ppt' => 'application/vnd.ms-powerpoint',
309
            // open office
310
            'odt' => 'application/vnd.oasis.opendocument.text',
311
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
312
        );
313
314
        if (!file_exists($filename)) {
315
            throw new Error404Exception();
316
        }
317
318
        $ext = substr(strrchr($filename, "."), 1);
319
        if (array_key_exists($ext, $mimeTypes)) {
320
            return $mimeTypes[$ext];
321
        } elseif (function_exists('finfo_open')) {
322
            $finfo = finfo_open(FILEINFO_MIME);
323
            $mimetype = finfo_file($finfo, $filename);
324
            finfo_close($finfo);
325
            return $mimetype;
326
        } else {
327
            return 'application/octet-stream';
328
        }
329
    }
330
331
    /**
332
     * @param $swaggerJson
333
     * @param CacheInterface|null $cache
334
     * @throws SchemaInvalidException
335
     * @throws SchemaNotFoundException
336
     * @throws OperationIdInvalidException
337
     * @throws InvalidArgumentException
338
     */
339
    public function setRoutesSwagger($swaggerJson, CacheInterface $cache = null)
340
    {
341
        if (!file_exists($swaggerJson)) {
342
            throw new SchemaNotFoundException("Schema '$swaggerJson' not found");
343
        }
344
345
        $schema = json_decode(file_get_contents($swaggerJson), true);
346
        if (!isset($schema['paths'])) {
347
            throw new SchemaInvalidException("Schema '$swaggerJson' is invalid");
348
        }
349
350
        if (is_null($cache)) {
351
            $cache = new NoCacheEngine();
352
        }
353
354
        $routePattern = $cache->get('SERVERHANDLERROUTES', false);
355
        if ($routePattern === false) {
356
            $routePattern = $this->generateRoutes($schema);
357
            $cache->set('SERVERHANDLERROUTES', $routePattern);
358
        }
359
        $this->setRoutes($routePattern);
360
    }
361
362
    /**
363
     * @param $schema
364
     * @return array
365
     * @throws OperationIdInvalidException
366
     */
367
    protected function generateRoutes($schema)
368
    {
369
        $basePath = isset($schema["basePath"]) ? $schema["basePath"] : "";
370
        if (empty($basePath) && isset($schema["servers"])) {
371
            $uri = new Uri($schema["servers"][0]["url"]);
372
            $basePath = $uri->getPath();
373
        }
374
375
        $pathList = $this->sortPaths(array_keys($schema['paths']));
376
377
        $routes = [];
378
        foreach ($pathList as $path) {
379
            foreach ($schema['paths'][$path] as $method => $properties) {
380
                $handler = $this->getMethodHandler($method, $basePath . $path, $properties);
381
                if (!isset($properties['operationId'])) {
382
                    throw new OperationIdInvalidException('OperationId was not found');
383
                }
384
385
                $parts = explode('::', $properties['operationId']);
386
                if (count($parts) !== 2) {
387
                    throw new OperationIdInvalidException(
388
                        'OperationId needs to be in the format Namespace\\class::method'
389
                    );
390
                }
391
392
                $routes[] = new RoutePattern(
393
                    strtoupper($method),
394
                    $basePath . $path,
395
                    $handler,
396
                    $parts[1],
397
                    $parts[0]
398
                );
399
            }
400
        }
401
402
        return $routes;
403
    }
404
405
    protected function sortPaths($pathList)
406
    {
407
        usort($pathList, function ($left, $right) {
408 View Code Duplication
            if (strpos($left, '{') === false && strpos($right, '{') !== false) {
0 ignored issues
show
Duplication introduced by Joao Gilberto Magalhães
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...
409
                return -16384;
410
            }
411 View Code Duplication
            if (strpos($left, '{') !== false && strpos($right, '{') === false) {
0 ignored issues
show
Duplication introduced by Joao Gilberto Magalhães
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...
412
                return 16384;
413
            }
414
            if (strpos($left, $right) !== false) {
415
                return -16384;
416
            }
417
            if (strpos($right, $left) !== false) {
418
                return 16384;
419
            }
420
            return strcmp($left, $right);
421
        });
422
423
        return $pathList;
424
    }
425
426
    /**
427
     * @param $method
428
     * @param $path
429
     * @param $properties
430
     * @return string
431
     * @throws OperationIdInvalidException
432
     */
433
    protected function getMethodHandler($method, $path, $properties)
434
    {
435
        $method = strtoupper($method);
436
        if (isset($this->pathHandler["$method::$path"])) {
437
            return $this->pathHandler["$method::$path"];
438
        }
439
        if (!isset($properties['produces'])) {
440
            return get_class($this->getDefaultHandler());
441
        }
442
443
        $produces = $properties['produces'];
444
        if (is_array($produces)) {
445
            $produces = $produces[0];
446
        }
447
448
        if (!isset($this->mimeTypeHandler[$produces])) {
449
            throw new OperationIdInvalidException("There is no handler for $produces");
450
        }
451
452
        return $this->mimeTypeHandler[$produces];
453
    }
454
455
    public function setMimeTypeHandler($mimetype, $handler)
456
    {
457
        $this->mimeTypeHandler[$mimetype] = $handler;
458
    }
459
460
    public function setPathHandler($method, $path, $handler)
461
    {
462
        $method = strtoupper($method);
463
        $this->pathHandler["$method::$path"] = $handler;
464
    }
465
}
466