Passed
Pull Request — master (#11)
by Joao
01:43
created

HttpRequestHandler::executeRequest()   B

Complexity

Conditions 10
Paths 27

Size

Total Lines 55
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 33.5571

Importance

Changes 0
Metric Value
cc 10
eloc 33
c 0
b 0
f 0
nc 27
nop 3
dl 0
loc 55
ccs 13
cts 34
cp 0.3824
crap 33.5571
rs 7.6666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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($request->server('HTTP_ORIGIN'))) {
154
            $blockExecutionBecauseOfCors = true;
155
156
            // Allow from any origin
157
            if (!empty($this->corsOrigins)) {
158
                foreach ((array)$this->corsOrigins as $origin) {
159
                    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

159
                    if (preg_match("~^.*//$origin$~", /** @scrutinizer ignore-type */ $request->server('HTTP_ORIGIN'))) {
Loading history...
160
                        $response->addHeader("Access-Control-Allow-Origin", $request->server('HTTP_ORIGIN'));
161
                        $response->addHeader('Access-Control-Allow-Credentials', 'true');
162
                        $response->addHeader('Access-Control-Max-Age', '86400');    // cache for 1 day
163
164
                        // Access-Control headers are received during OPTIONS requests
165
                        if ($request->server('REQUEST_METHOD') == 'OPTIONS') {
166
                            $response->addHeader("Access-Control-Allow-Methods", implode(",", array_merge(['OPTIONS'], $this->corsMethods)));
167
                            $response->addHeader("Access-Control-Allow-Headers", implode(",", $this->corsHeaders));
168
                            $outputProcessor->processResponse($response);
169
                            return;
170
                        }
171
                        $blockExecutionBecauseOfCors = false;
172
                        break;
173
                    }
174
                }
175
            }
176
        }
177
178 3
        if ($blockExecutionBecauseOfCors) {
179
            $outputProcessor->processResponse($response);
180
            return;
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 7
    public function handle(RouteDefinitionInterface $routeDefinition, $outputBuffer = true, $session = false)
218
    {
219 7
        if ($outputBuffer) {
220
            ob_start();
221
        }
222 7
        if ($session) {
223
            session_start();
224
        }
225
226
        // --------------------------------------------------------------------------
227
        // Check if script exists or if is itself
228
        // --------------------------------------------------------------------------
229 7
        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
    public function withCorsOrigins($origins)
353
    {
354
        $this->corsOrigins = $origins;
355
        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
    public function withDefaultOutputProcessor($processor, $args = [])
371
    {
372
        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
        $this->defaultOutputProcessor = $processor;
382
383
        return $this;
384
    }
385
}
386