Completed
Push — master ( 382c30...1fdbd5 )
by Joao
05:35
created

ServerRequestHandler::generateRoutes()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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