Passed
Pull Request — master (#11)
by Joao
01:49
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\Error404Exception;
7
use ByJG\RestServer\Exception\Error405Exception;
8
use ByJG\RestServer\Exception\Error520Exception;
9
use ByJG\RestServer\Exception\InvalidClassException;
10
use ByJG\RestServer\OutputProcessor\BaseOutputProcessor;
11
use ByJG\RestServer\OutputProcessor\OutputProcessorInterface;
12
use ByJG\RestServer\Route\RouteDefinitionInterface;
13
use Closure;
14
use FastRoute\Dispatcher;
15
use InvalidArgumentException;
16
17
class HttpRequestHandler implements RequestHandler
18
{
19
    const OK = "OK";
20
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
21
    const NOT_FOUND = "NOT FOUND";
22
23
    protected $useErrorHandler = true;
24
    protected $detailedErrorHandler = false;
25
    protected $corsOrigins = [];
26
    protected $corsMethods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
27
    protected $corsHeaders = [
28
        'Authorization',
29
        'Content-Type',
30
        'Accept',
31
        'Origin',
32
        'User-Agent',
33
        'Cache-Control',
34
        'Keep-Alive',
35
        'X-Requested-With',
36
        'If-Modified-Since'
37
    ];
38
39
    protected $defaultOutputProcessor = null;
40
    protected $defaultOutputProcessorArgs = [];
41
42
    /**
43
     * @param RouteDefinitionInterface $routeDefinition
44
     * @return bool
45
     * @throws ClassNotFoundException
46
     * @throws Error404Exception
47
     * @throws Error405Exception
48
     * @throws Error520Exception
49
     * @throws InvalidClassException
50
     */
51 7
    protected function process(RouteDefinitionInterface $routeDefinition)
52
    {
53
        // Initialize ErrorHandler with default error handler
54 7
        if ($this->useErrorHandler) {
55 7
            ErrorHandler::getInstance()->register();
56
        }
57
58
        // Get HttpRequest
59 7
        $request = $this->getHttpRequest();
60
61
        // Get the URL parameters
62 7
        $httpMethod = $request->server('REQUEST_METHOD');
63 7
        $uri = parse_url($request->server('REQUEST_URI'), PHP_URL_PATH);
64 7
        $query = parse_url($request->server('REQUEST_URI'), PHP_URL_QUERY);
65 7
        $queryStr = [];
66 7
        if (!empty($query)) {
67
            parse_str($query, $queryStr);
68
        }
69
70
        // Generic Dispatcher for RestServer
71 7
        $dispatcher = $routeDefinition->getDispatcher();
72
73 7
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
74
75
        // Processing
76 7
        switch ($routeInfo[0]) {
77 7
            case Dispatcher::NOT_FOUND:
78 3
                if ($this->tryDeliveryPhysicalFile() === false) {
79 2
                    $this->prepareToOutput();
80 2
                    throw new Error404Exception("Route '$uri' not found");
81
                }
82 1
                return true;
83
84 4
            case Dispatcher::METHOD_NOT_ALLOWED:
85 1
                $outputProcessor = $this->prepareToOutput();
86 1
                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

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

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