Test Failed
Pull Request — master (#11)
by Joao
01:49
created

HttpRequestHandler::prepareToOutput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace ByJG\RestServer;
4
5
use ByJG\RestServer\Exception\ClassNotFoundException;
6
use ByJG\RestServer\Exception\Error401Exception;
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\OutputProcessor\BaseOutputProcessor;
12
use ByJG\RestServer\OutputProcessor\OutputProcessorInterface;
13
use ByJG\RestServer\Route\RouteListInterface;
14
use Closure;
15
use FastRoute\Dispatcher;
16
use InvalidArgumentException;
17
18
class HttpRequestHandler implements RequestHandler
19
{
20
    const OK = "OK";
21
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
22
    const NOT_FOUND = "NOT FOUND";
23
24
    const CORS_OK = 'CORS_OK';
25
    const CORS_FAILED = 'CORS_FAILED';
26
    const CORS_OPTIONS = 'CORS_OPTIONS';
27
28
    protected $useErrorHandler = true;
29
    protected $detailedErrorHandler = false;
30
    protected $disableCors = false;
31
    protected $corsOrigins = ['.*'];
32
    protected $corsMethods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
33
    protected $corsHeaders = [
34
        'Authorization',
35
        'Content-Type',
36
        'Accept',
37
        'Origin',
38
        'User-Agent',
39
        'Cache-Control',
40
        'Keep-Alive',
41
        'X-Requested-With',
42
        'If-Modified-Since'
43
    ];
44
45
    protected $defaultOutputProcessor = null;
46
    protected $defaultOutputProcessorArgs = [];
47
48
    /**
49
     * @param RouteListInterface $routeDefinition
50
     * @return bool
51
     * @throws ClassNotFoundException
52
     * @throws Error404Exception
53
     * @throws Error405Exception
54
     * @throws Error520Exception
55
     * @throws InvalidClassException
56
     */
57
    protected function process(RouteListInterface $routeDefinition)
58
    {
59
        // Initialize ErrorHandler with default error handler
60
        if ($this->useErrorHandler) {
61
            ErrorHandler::getInstance()->register();
62
        }
63
64
        // Create the Request and Response methods
65
        $request = $this->getHttpRequest();
66
        $response = new HttpResponse();
67
68
        // Get the URL parameters
69
        $httpMethod = $request->server('REQUEST_METHOD');
70
        $uri = parse_url($request->server('REQUEST_URI'), PHP_URL_PATH);
71
        $query = parse_url($request->server('REQUEST_URI'), PHP_URL_QUERY);
72
        $queryStr = [];
73
        if (!empty($query)) {
74
            parse_str($query, $queryStr);
75
        }
76
77
        // Generic Dispatcher for RestServer
78
        $dispatcher = $routeDefinition->getDispatcher();
79
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
80
81
        $corsStatus = $this->validateCors($response, $request);
82
        if ($corsStatus != self::CORS_OK) {
83
            $corsOutputProcessor = $this->initializeProcessor($response, $request);
84
85
            if ($corsStatus == self::CORS_OPTIONS) {
86
                $corsOutputProcessor->writeHeader($response);
87
                return;
88
            } elseif ($corsStatus == self::CORS_FAILED) {
89
                throw new Error401Exception("CORS verification failed. Request Blocked.");
90
            }
91
        }
92
93
        // Processing
94
        switch ($routeInfo[0]) {
95
            case Dispatcher::NOT_FOUND:
96
                if ($this->tryDeliveryPhysicalFile() === false) {
97
                    $this->initializeProcessor($response, $request);
98
                    throw new Error404Exception("Route '$uri' not found");
99
                }
100
                return true;
101
102
            case Dispatcher::METHOD_NOT_ALLOWED:
103
                $this->initializeProcessor($response, $request);
104
                throw new Error405Exception('Method not allowed');
105
106
            case Dispatcher::FOUND:
107
                // ... 200 Process:
108
                $vars = array_merge($routeInfo[2], $queryStr);
109
110
                // Get the Selected Route
111
                $selectedRoute = $routeInfo[1];
112
113
                // Default Handler for errors and
114
                $outputProcessor = $this->initializeProcessor($response, $request, $selectedRoute["output_processor"]);
115
116
                // Class
117
                $class = $selectedRoute["class"];
118
                $request->appendVars($vars);
119
120
                // Execute the request
121
                $this->executeRequest($outputProcessor, $class, $response, $request);
122
123
                break;
124
125
            default:
126
                throw new Error520Exception('Unknown');
127
        }
128
    }
129
130
    protected function initializeProcessor(HttpResponse $response, HttpRequest $request, $class = null)
131
    {
132
        if (!empty($class)) {
133
            $outputProcessor = BaseOutputProcessor::getFromClassName($class);
134
        } elseif (!empty($this->defaultOutputProcessor)) {
135
            $outputProcessor = BaseOutputProcessor::getFromClassName($this->defaultOutputProcessor);
136
        } else {
137
            $outputProcessor = BaseOutputProcessor::getFromHttpAccept();
138
        }
139
        $outputProcessor->writeContentType();
140
        if ($this->detailedErrorHandler) {
141
            ErrorHandler::getInstance()->setHandler($outputProcessor->getDetailedErrorHandler());
142
        } else {
143
            ErrorHandler::getInstance()->setHandler($outputProcessor->getErrorHandler());
144
        }
145
146
        ErrorHandler::getInstance()->setOutputProcessor($outputProcessor, $response);
147
        
148
        return $outputProcessor;
149
    }
150
151
    protected function getHttpRequest()
152
    {
153
        return new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE);
154
    }
155
156
    /**
157
     * Undocumented function
158
     *
159
     * @param HttpResponse $response
160
     * @param HttpRequest $request
161
     * @return string
162
     */
163
    protected function validateCors(HttpResponse $response, HttpRequest $request)
164
    {
165
        $corsStatus = self::CORS_OK;
166
167
        if ($this->disableCors) {
168
            return self::CORS_OK;
169
        }
170
171
        if (!empty($request->server('HTTP_ORIGIN'))) {
172
            $corsStatus = self::CORS_FAILED;
173
174
            foreach ((array)$this->corsOrigins as $origin) {
175
                if (preg_match("~^.*//$origin$~", $request->server('HTTP_ORIGIN'))) {
0 ignored issues
show
Bug introduced by
It seems like $request->server('HTTP_ORIGIN') can also be of type boolean; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
                if (preg_match("~^.*//$origin$~", /** @scrutinizer ignore-type */ $request->server('HTTP_ORIGIN'))) {
Loading history...
176
                    $response->addHeader("Access-Control-Allow-Origin", $request->server('HTTP_ORIGIN'));
177
                    $response->addHeader('Access-Control-Allow-Credentials', 'true');
178
                    $response->addHeader('Access-Control-Max-Age', '86400');    // cache for 1 day
179
180
                    // Access-Control headers are received during OPTIONS requests
181
                    if ($request->server('REQUEST_METHOD') == 'OPTIONS') {
182
                        $response->addHeader("Access-Control-Allow-Methods", implode(",", array_merge(['OPTIONS'], $this->corsMethods)));
183
                        $response->addHeader("Access-Control-Allow-Headers", implode(",", $this->corsHeaders));
184
                        return self::CORS_OPTIONS;
185
                    }
186
                    $corsStatus = self::CORS_OK;
187
                    break;
188
                }
189
            }
190
        }
191
        return $corsStatus;
192
    }
193
194
    /**
195
     * @param OutputProcessorInterface $outputProcessor
196
     * @param $class
197
     * @param HttpRequest $request
198
     * @throws ClassNotFoundException
199
     * @throws InvalidClassException
200
     */
201
    protected function executeRequest(OutputProcessorInterface $outputProcessor, $class, HttpResponse $response, HttpRequest $request)
202
    {
203
        // Process Closure
204
        if ($class instanceof Closure) {
205
            $class($response, $request);
206
            $outputProcessor->processResponse($response);
207
            return;
208
        }
209
210
        // Process Class::Method()
211
        $function = $class[1];
212
        $class =  $class[0];
213
        if (!class_exists($class)) {
214
            throw new ClassNotFoundException("Class '$class' defined in the route is not found");
215
        }
216
        $instance = new $class();
217
        if (!method_exists($instance, $function)) {
218
            throw new InvalidClassException("There is no method '$class::$function''");
219
        }
220
        $instance->$function($response, $request);
221
        $outputProcessor->processResponse($response);
222
    }
223
224
    /**
225
     * Handle the ROUTE (see web/app-dist.php)
226
     *
227
     * @param RouteListInterface $routeDefinition
228
     * @param bool $outputBuffer
229
     * @param bool $session
230
     * @return bool|void
231
     * @throws ClassNotFoundException
232
     * @throws Error404Exception
233
     * @throws Error405Exception
234
     * @throws Error520Exception
235
     * @throws InvalidClassException
236
     */
237
    public function handle(RouteListInterface $routeDefinition, $outputBuffer = true, $session = false)
238
    {
239
        if ($outputBuffer) {
240
            ob_start();
241
        }
242
        if ($session) {
243
            session_start();
244
        }
245
246
        // --------------------------------------------------------------------------
247
        // Check if script exists or if is itself
248
        // --------------------------------------------------------------------------
249
        return $this->process($routeDefinition);
250
    }
251
252
    /**
253
     * @return bool
254
     * @throws Error404Exception
255
     */
256
    protected function tryDeliveryPhysicalFile()
257
    {
258
        $file = $_SERVER['SCRIPT_FILENAME'];
259
        if (!empty($file) && file_exists($file)) {
260
            $mime = $this->mimeContentType($file);
261
262
            if ($mime === false) {
0 ignored issues
show
introduced by
The condition $mime === false is always false.
Loading history...
263
                return false;
264
            }
265
266
            if (!defined("RESTSERVER_TEST")) {
267
                header("Content-Type: $mime");
268
            }
269
            echo file_get_contents($file);
270
            return true;
271
        }
272
273
        return false;
274
    }
275
276
    /**
277
     * Get the Mime Type based on the filename
278
     *
279
     * @param string $filename
280
     * @return string
281
     * @throws Error404Exception
282
     */
283 4
    protected function mimeContentType($filename)
284
    {
285
        $prohibitedTypes = [
286
            "php",
287
            "vb",
288
            "cs",
289
            "rb",
290
            "py",
291
            "py3",
292
            "lua"
293
        ];
294
295
        $mimeTypes = [
296
            'txt' => 'text/plain',
297
            'htm' => 'text/html',
298
            'html' => 'text/html',
299
            'css' => 'text/css',
300
            'js' => 'application/javascript',
301
            'json' => 'application/json',
302
            'xml' => 'application/xml',
303
            'swf' => 'application/x-shockwave-flash',
304
            'flv' => 'video/x-flv',
305
            // images
306
            'png' => 'image/png',
307
            'jpe' => 'image/jpeg',
308
            'jpeg' => 'image/jpeg',
309
            'jpg' => 'image/jpeg',
310
            'gif' => 'image/gif',
311
            'bmp' => 'image/bmp',
312
            'ico' => 'image/vnd.microsoft.icon',
313
            'tiff' => 'image/tiff',
314
            'tif' => 'image/tiff',
315
            'svg' => 'image/svg+xml',
316
            'svgz' => 'image/svg+xml',
317
            // archives
318
            'zip' => 'application/zip',
319
            'rar' => 'application/x-rar-compressed',
320
            'exe' => 'application/x-msdownload',
321
            'msi' => 'application/x-msdownload',
322
            'cab' => 'application/vnd.ms-cab-compressed',
323
            // audio/video
324
            'mp3' => 'audio/mpeg',
325
            'qt' => 'video/quicktime',
326
            'mov' => 'video/quicktime',
327
            // adobe
328
            'pdf' => 'application/pdf',
329
            'psd' => 'image/vnd.adobe.photoshop',
330
            'ai' => 'application/postscript',
331
            'eps' => 'application/postscript',
332
            'ps' => 'application/postscript',
333
            // ms office
334
            'doc' => 'application/msword',
335
            'rtf' => 'application/rtf',
336
            'xls' => 'application/vnd.ms-excel',
337
            'ppt' => 'application/vnd.ms-powerpoint',
338
            // open office
339
            'odt' => 'application/vnd.oasis.opendocument.text',
340
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
341
        ];
342
343 4
        if (!file_exists($filename)) {
344 1
            throw new Error404Exception();
345
        }
346
347 3
        $ext = substr(strrchr($filename, "."), 1);
348 3
        if (!in_array($ext, $prohibitedTypes)) {
349 3
            if (array_key_exists($ext, $mimeTypes)) {
350 3
                return $mimeTypes[$ext];
351
            } elseif (function_exists('finfo_open')) {
352
                $finfo = finfo_open(FILEINFO_MIME);
353
                $mimetype = finfo_file($finfo, $filename);
354
                finfo_close($finfo);
355
                return $mimetype;
356
            }
357
        }
358
359
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
360
    }
361
362
    public function withCorsDisabled()
363
    {
364
        $this->disableCors = true;
365
        return $this;
366
    }
367
368
    public function withErrorHandlerDisabled()
369
    {
370
        $this->useErrorHandler = false;
371
        return $this;
372
    }
373
374
    public function withDetailedErrorHandler()
375
    {
376
        $this->detailedErrorHandler = true;
377
        return $this;
378
    }
379
380
    public function withCorsOrigins($origins)
381
    {
382
        $this->corsOrigins = $origins;
383
        return $this;
384
    }
385
386
    public function withAcceptCorsHeaders($headers)
387
    {
388
        $this->corsHeaders = $headers;
389
        return $this;
390
    }
391
392
    public function withAcceptCorsMethods($methods)
393
    {
394
        $this->corsMethods = $methods;
395
        return $this;
396
    }
397
398
    public function withDefaultOutputProcessor($processor, $args = [])
399
    {
400
        if (!($processor instanceof \Closure)) {
401
            if (!is_string($processor)) {
402
                throw new InvalidArgumentException("Default processor needs to class name of an OutputProcessor");
403
            }
404
            if (!is_subclass_of($processor, BaseOutputProcessor::class)) {
405
                throw new InvalidArgumentException("Needs to be a class of " . BaseOutputProcessor::class);
406
            }
407
        }
408
409
        $this->defaultOutputProcessor = $processor;
410
411
        return $this;
412
    }
413
}
414