Passed
Pull Request — master (#11)
by Joao
02:10
created

HttpRequestHandler::withAcceptCorsHeaders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 3
cp 0
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\RouteDefinitionInterface;
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
    protected $useErrorHandler = true;
25
    protected $detailedErrorHandler = false;
26
    protected $corsOrigins = [];
27
    protected $corsMethods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
28
    protected $corsHeaders = [
29
        'Authorization',
30
        'Content-Type',
31
        'Accept',
32
        'Origin',
33
        'User-Agent',
34
        'Cache-Control',
35
        'Keep-Alive',
36
        'X-Requested-With',
37
        'If-Modified-Since'
38
    ];
39
40
    protected $defaultOutputProcessor = null;
41
    protected $defaultOutputProcessorArgs = [];
42
43
    /**
44
     * @param RouteDefinitionInterface $routeDefinition
45
     * @return bool
46
     * @throws ClassNotFoundException
47
     * @throws Error404Exception
48
     * @throws Error405Exception
49
     * @throws Error520Exception
50
     * @throws InvalidClassException
51
     */
52 9
    protected function process(RouteDefinitionInterface $routeDefinition)
53
    {
54
        // Initialize ErrorHandler with default error handler
55 9
        if ($this->useErrorHandler) {
56 9
            ErrorHandler::getInstance()->register();
57
        }
58
59
        // Get HttpRequest
60 9
        $request = $this->getHttpRequest();
61
62
        // Get the URL parameters
63 9
        $httpMethod = $request->server('REQUEST_METHOD');
64 9
        $uri = parse_url($request->server('REQUEST_URI'), PHP_URL_PATH);
65 9
        $query = parse_url($request->server('REQUEST_URI'), PHP_URL_QUERY);
66 9
        $queryStr = [];
67 9
        if (!empty($query)) {
68
            parse_str($query, $queryStr);
69
        }
70
71
        // Generic Dispatcher for RestServer
72 9
        $dispatcher = $routeDefinition->getDispatcher();
73
74 9
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
75
76
        // Processing
77 9
        switch ($routeInfo[0]) {
78 9
            case Dispatcher::NOT_FOUND:
79 3
                if ($this->tryDeliveryPhysicalFile() === false) {
80 2
                    $this->prepareToOutput();
81 2
                    throw new Error404Exception("Route '$uri' not found");
82
                }
83 1
                return true;
84
85 6
            case Dispatcher::METHOD_NOT_ALLOWED:
86 2
                $outputProcessor = $this->prepareToOutput();
87 2
                if (strtoupper($httpMethod) == "OPTIONS" && !empty($this->corsOrigins)) {
0 ignored issues
show
Bug introduced by
It seems like $httpMethod can also be of type boolean; however, parameter $string of strtoupper() 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

87
                if (strtoupper(/** @scrutinizer ignore-type */ $httpMethod) == "OPTIONS" && !empty($this->corsOrigins)) {
Loading history...
88 1
                    $this->executeRequest($outputProcessor, function () {}, $request);
89
                    return;
90
                }
91 1
                throw new Error405Exception('Method not allowed');
92
93 4
            case Dispatcher::FOUND:
94
                // ... 200 Process:
95 4
                $vars = array_merge($routeInfo[2], $queryStr);
96
97
                // Get the Selected Route
98 4
                $selectedRoute = $routeInfo[1];
99
100
                // Default Handler for errors and
101 4
                $outputProcessor = $this->prepareToOutput($selectedRoute["output_processor"]);
102
103
                // Class
104 4
                $class = $selectedRoute["class"];
105 4
                $request->appendVars($vars);
106
107
                // Execute the request
108 4
                $this->executeRequest($outputProcessor, $class, $request);
109
110 2
                break;
111
112
            default:
113
                throw new Error520Exception('Unknown');
114
        }
115
    }
116
117 8
    protected function prepareToOutput($class = null)
118
    {
119 8
        if (!empty($class)) {
120 4
            $outputProcessor = BaseOutputProcessor::getFromClassName($class);
121 4
        } elseif (!empty($this->defaultOutputProcessor)) {
122 1
            $outputProcessor = BaseOutputProcessor::getFromClassName($this->defaultOutputProcessor);
123
        } else {
124 3
            $outputProcessor = BaseOutputProcessor::getFromHttpAccept();
125
        }
126 8
        $outputProcessor->writeContentType();
127 8
        if ($this->detailedErrorHandler) {
128
            ErrorHandler::getInstance()->setHandler($outputProcessor->getDetailedErrorHandler());
129
        } else {
130 8
            ErrorHandler::getInstance()->setHandler($outputProcessor->getErrorHandler());
131
        }
132
133 8
        return $outputProcessor;
134
    }
135
136 9
    protected function getHttpRequest()
137
    {
138 9
        return new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE);
139
    }
140
141
    /**
142
     * @param OutputProcessorInterface $outputProcessor
143
     * @param $class
144
     * @param HttpRequest $request
145
     * @throws ClassNotFoundException
146
     * @throws InvalidClassException
147
     */
148 5
    protected function executeRequest(OutputProcessorInterface $outputProcessor, $class, HttpRequest $request)
149
    {
150
        // Create the Request and Response methods
151 5
        $response = new HttpResponse();
152 5
        $blockExecutionBecauseOfCors = false;
153
154 5
        if (!empty($request->server('HTTP_ORIGIN'))) {
155 2
            $blockExecutionBecauseOfCors = true;
156
157
            // Allow from any origin
158 2
            if (!empty($this->corsOrigins)) {
159 1
                foreach ((array)$this->corsOrigins as $origin) {
160 1
                    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

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