Passed
Push — develop ( 7cea29...0a684c )
by Pablo
02:17
created

ApiRouter   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 847
Duplicated Lines 0 %

Importance

Changes 28
Bugs 0 Features 0
Metric Value
wmc 76
eloc 289
c 28
b 0
f 0
dl 0
loc 847
rs 2.32

28 Methods

Rating   Name   Duplication   Size   Complexity  
A detectRequestMethod() 0 7 2
A normaliseControllerClass() 0 19 3
A isPostRequest() 0 3 1
A checkControllerAuth() 0 32 5
A configureLogging() 0 9 1
A callControllerMethod() 0 55 4
A isValidFormat() 0 3 1
A outputGetFormat() 0 3 1
A detectOutputFormat() 0 28 5
A getAccessToken() 0 3 1
A isGetRequest() 0 3 1
A invalidApiRoute() 0 15 1
A writeLog() 0 7 2
A isPutRequest() 0 3 1
A output() 0 42 4
A __construct() 0 8 1
A verifyAccessToken() 0 38 5
A isDeleteRequest() 0 3 1
A setCorsStatusHeader() 0 6 1
F index() 0 114 19
A detectUriSegments() 0 19 1
B discoverApiControllers() 0 47 8
A setCorsHeaders() 0 12 1
A getRequestMethod() 0 3 1
A invalidApiFormat() 0 11 1
A outputSendHeader() 0 3 1
A isRequestMethod() 0 3 1
A outputSetFormat() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like ApiRouter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiRouter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Routes requests to the API to the appropriate controllers
5
 *
6
 * @package     Nails
7
 * @subpackage  module-api
8
 * @category    Controller
9
 * @author      Nails Dev Team
10
 * @link
11
 */
12
13
use Nails\Auth;
14
use Nails\Api\Constants;
15
use Nails\Api\Exception\ApiException;
16
use Nails\Api\Factory\ApiResponse;
17
use Nails\Common\Exception\FactoryException;
18
use Nails\Common\Exception\ModelException;
19
use Nails\Common\Exception\NailsException;
20
use Nails\Common\Exception\ValidationException;
21
use Nails\Common\Factory\HttpRequest;
0 ignored issues
show
Bug introduced by Pablo de la Peña
This use statement conflicts with another class in this namespace, HttpRequest. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
22
use Nails\Common\Factory\Logger;
23
use Nails\Common\Service\HttpCodes;
24
use Nails\Common\Service\Input;
25
use Nails\Common\Service\Output;
26
use Nails\Components;
27
use Nails\Environment;
28
use Nails\Factory;
29
30
// --------------------------------------------------------------------------
31
32
/**
33
 * Allow the app to add functionality, if needed
34
 */
35
if (class_exists('\App\Api\Controller\BaseRouter')) {
36
    abstract class BaseMiddle extends \App\Api\Controller\BaseRouter
0 ignored issues
show
Bug introduced by Pablo de la Peña
The type App\Api\Controller\BaseRouter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
    {
38
    }
39
} else {
40
    abstract class BaseMiddle extends \Nails\Common\Controller\Base
41
    {
42
    }
43
}
44
45
// --------------------------------------------------------------------------
46
47
/**
48
 * Class ApiRouter
49
 */
50
class ApiRouter extends BaseMiddle
51
{
52
    const DEFAULT_FORMAT                   = \Nails\Api\Api\Output\Json::SLUG;
53
    const REQUEST_METHOD_GET               = HttpRequest\Get::HTTP_METHOD;
54
    const REQUEST_METHOD_PUT               = HttpRequest\Put::HTTP_METHOD;
55
    const REQUEST_METHOD_POST              = HttpRequest\Post::HTTP_METHOD;
56
    const REQUEST_METHOD_DELETE            = HttpRequest\Delete::HTTP_METHOD;
57
    const REQUEST_METHOD_OPTIONS           = HttpRequest\Options::HTTP_METHOD;
58
    const ACCESS_TOKEN_HEADER              = 'X-Access-Token';
59
    const ACCESS_TOKEN_POST_PARAM          = 'accessToken';
60
    const ACCESS_TOKEN_GET_PARAM           = 'accessToken';
61
    const ACCESS_CONTROL_ALLOW_ORIGIN      = '*';
62
    const ACCESS_CONTROL_ALLOW_CREDENTIALS = 'true';
63
    const ACCESS_CONTROL_MAX_AGE           = 86400;
64
    const ACCESS_CONTROL_ALLOW_HEADERS     = ['*'];
65
    const ACCESS_CONTROL_ALLOW_METHODS     = [
66
        self::REQUEST_METHOD_GET,
67
        self::REQUEST_METHOD_PUT,
68
        self::REQUEST_METHOD_POST,
69
        self::REQUEST_METHOD_DELETE,
70
        self::REQUEST_METHOD_OPTIONS,
71
    ];
72
    const OUTPUT_FORMAT_PATTERN            = '/\.([a-z]*)$/';
73
74
    // --------------------------------------------------------------------------
75
76
    /** @var array */
77
    protected static $aOutputValidFormats;
78
79
    /** @var string */
80
    protected $sRequestMethod;
81
82
    /** @var string */
83
    protected $sModuleName;
84
85
    /** @var string */
86
    protected $sClassName;
87
88
    /** @var string */
89
    protected $sMethod;
90
91
    /** @var string */
92
    protected $sOutputFormat;
93
94
    /** @var bool */
95
    protected $bOutputSendHeader = true;
96
97
    /** @var Logger */
98
    protected $oLogger;
99
100
    /** @var string */
101
    protected $sAccessToken;
102
103
    /** @var Auth\Resource\User\AccessToken */
0 ignored issues
show
Bug introduced by Pablo de la Peña
The type Nails\Auth\Resource\User\AccessToken was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
104
    protected $oAccessToken;
105
106
    // --------------------------------------------------------------------------
107
108
    /**
109
     * ApiRouter constructor.
110
     *
111
     * @throws FactoryException
112
     */
113
    public function __construct()
114
    {
115
        parent::__construct();
116
        $this
117
            ->configureLogging()
118
            ->detectRequestMethod()
119
            ->detectOutputFormat()
120
            ->detectUriSegments();
121
    }
122
123
    // --------------------------------------------------------------------------
124
125
    /**
126
     * Route the call to the correct place
127
     */
128
    public function index()
129
    {
130
        //  Handle OPTIONS CORS pre-flight requests
131
        if ($this->sRequestMethod === static::REQUEST_METHOD_OPTIONS) {
132
133
            $this
134
                ->setCorsHeaders()
135
                ->setCorsStatusHeader();
136
            return;
137
138
        } else {
139
140
            try {
141
142
                /** @var HttpCodes $oHttpCodes */
143
                $oHttpCodes = Factory::service('HttpCodes');
144
145
                // --------------------------------------------------------------------------
146
147
                $this->verifyAccessToken();
148
149
                // --------------------------------------------------------------------------
150
151
                if (!$this->outputSetFormat($this->sOutputFormat)) {
152
                    $this->invalidApiFormat();
153
                }
154
155
                // --------------------------------------------------------------------------
156
157
                $aControllerMap = $this->discoverApiControllers();
158
                $oModule        = $aControllerMap[$this->sModuleName] ?? null;
159
160
                if (empty($oModule)) {
161
                    $this->invalidApiRoute();
162
                }
163
164
                $sController = $this->normaliseControllerClass($oModule);
165
166
                if (!class_exists($sController)) {
167
                    $this->invalidApiRoute();
168
                }
169
170
                $this->checkControllerAuth($sController);
171
172
                $oResponse = $this->callControllerMethod(
173
                    new $sController($this)
174
                );
175
176
                $aOut = [
177
                    'status' => $oResponse->getCode(),
178
                    'body'   => $oResponse->getBody(),
179
                    'data'   => $oResponse->getData(),
180
                    'meta'   => $oResponse->getMeta(),
181
                ];
182
183
            } catch (ValidationException $e) {
184
185
                $aOut = [
186
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
187
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
188
                    'details' => $e->getData() ?: [],
189
                ];
190
                if (isSuperuser()) {
191
                    $aOut['exception'] = (object) array_filter([
192
                        'type' => get_class($e),
193
                        'file' => $e->getFile(),
194
                        'line' => $e->getLine(),
195
                    ]);
196
                }
197
198
                $this->writeLog($aOut);
199
200
            } catch (ApiException $e) {
201
202
                $aOut = [
203
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
204
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
205
                    'details' => $e->getData() ?: [],
206
                ];
207
                if (isSuperuser()) {
208
                    $aOut['exception'] = (object) array_filter([
209
                        'type' => get_class($e),
210
                        'file' => $e->getFile(),
211
                        'line' => $e->getLine(),
212
                    ]);
213
                }
214
215
                $this->writeLog($aOut);
216
217
            } catch (\Exception $e) {
218
219
                /**
220
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
221
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
222
                 * developer the error quickly and with as much info as possible.
223
                 */
224
                if (Environment::is(Environment::ENV_PROD)) {
225
                    throw $e;
226
                } else {
227
                    $aOut = [
228
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
229
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
230
                        'exception' => (object) array_filter([
231
                            'type' => get_class($e),
232
                            'file' => $e->getFile(),
233
                            'line' => $e->getLine(),
234
                        ]),
235
                    ];
236
237
                    $this->writeLog($aOut);
238
                }
239
            }
240
241
            $this->output($aOut);
242
        }
243
    }
244
245
    // --------------------------------------------------------------------------
246
247
    /**
248
     * Configures API logging
249
     *
250
     * @return $this
251
     * @throws FactoryException
252
     */
253
    protected function configureLogging(): self
254
    {
255
        /** @var \Nails\Common\Resource\DateTime $oNow */
256
        $oNow = Factory::factory('DateTime');
257
        /** @var Logger oLogger */
258
        $this->oLogger = Factory::factory('Logger');
259
        $this->oLogger->setFile('api-' . $oNow->format('Y-m-d') . '.php');
260
261
        return $this;
262
    }
263
264
    // --------------------------------------------------------------------------
265
266
    /**
267
     * Detects the request method being used
268
     *
269
     * @return $this
270
     * @throws FactoryException
271
     */
272
    protected function detectRequestMethod(): self
273
    {
274
        /** @var Input $oInput */
275
        $oInput               = Factory::service('Input');
276
        $this->sRequestMethod = $oInput->server('REQUEST_METHOD') ?: static::REQUEST_METHOD_GET;
0 ignored issues
show
Documentation Bug introduced by Pablo de la Peña
It seems like $oInput->server('REQUEST...tic::REQUEST_METHOD_GET can also be of type array. However, the property $sRequestMethod is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
277
278
        return $this;
279
    }
280
281
    // --------------------------------------------------------------------------
282
283
    /**
284
     * Detects and validates the output format from the URL
285
     *
286
     * @return $this
287
     * @throws NailsException
288
     */
289
    protected function detectOutputFormat(): self
290
    {
291
        //  Look for valid output formats
292
        $aComponents = Components::available();
293
294
        //  Shift the app onto the end so it overrides any module supplied formats
295
        $oApp = array_shift($aComponents);
296
        array_push($aComponents, $oApp);
297
298
        foreach ($aComponents as $oComponent) {
299
300
            $oClasses = $oComponent
301
                ->findClasses('Api\Output')
302
                ->whichImplement(\Nails\Api\Interfaces\Output::class);
303
304
            foreach ($oClasses as $sClass) {
305
                static::$aOutputValidFormats[strtoupper($sClass::getSlug())] = $sClass;
306
            }
307
        }
308
309
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
310
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
311
312
        $this->sOutputFormat = static::isValidFormat($sFormat)
313
            ? $sFormat
314
            : static::DEFAULT_FORMAT;
315
316
        return $this;
317
    }
318
319
    // --------------------------------------------------------------------------
320
321
    /**
322
     * Extracts the module, controller and method segments from the URI
323
     *
324
     * @return $this
325
     */
326
    protected function detectUriSegments(): self
327
    {
328
        /**
329
         * In order to work out the next few parts we'll analyse the URI string manually.
330
         * We're doing this because of the optional return type at the end of the string;
331
         * it's easier to regex that quickly, remove it, then split up the segments.
332
         */
333
        $sUri = preg_replace(static::OUTPUT_FORMAT_PATTERN, '', uri_string());
334
335
        //  Remove the module prefix (i.e "api/") then explode into segments
336
        //  Using regex as some systems will report a leading slash (e.g CLI)
337
        $sUri = preg_replace('#^/?api/#', '', $sUri);
338
        $aUri = explode('/', $sUri);
339
340
        $this->sModuleName = getFromArray(0, $aUri);
341
        $this->sClassName  = getFromArray(1, $aUri, $this->sModuleName);
342
        $this->sMethod     = getFromArray(2, $aUri, 'index');
343
344
        return $this;
345
    }
346
347
    // --------------------------------------------------------------------------
348
349
    /**
350
     * Verifies the access token, if supplied. Passing the token via the header is
351
     * preferred, but fallback to the GET and POST arrays.
352
     *
353
     * @return $this
354
     * @throws ApiException
355
     * @throws NailsException
356
     * @throws ReflectionException
357
     * @throws FactoryException
358
     * @throws ModelException
359
     */
360
    protected function verifyAccessToken(): self
361
    {
362
        /** @var Input $oInput */
363
        $oInput = Factory::service('Input');
364
        /** @var HttpCodes $oHttpCodes */
365
        $oHttpCodes = Factory::service('HttpCodes');
366
        /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
367
        $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
368
369
        $sAccessToken = $oInput->header(static::ACCESS_TOKEN_HEADER);
370
371
        if (!$sAccessToken) {
372
            $sAccessToken = $oInput->post(static::ACCESS_TOKEN_POST_PARAM);
373
        }
374
375
        if (!$sAccessToken) {
376
            $sAccessToken = $oInput->get(static::ACCESS_TOKEN_GET_PARAM);
377
        }
378
379
        if ($sAccessToken) {
380
381
            $this->sAccessToken = $sAccessToken;
0 ignored issues
show
Documentation Bug introduced by Pablo de la Peña
It seems like $sAccessToken can also be of type array. However, the property $sAccessToken is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
382
            $this->oAccessToken = $oUserAccessTokenModel->getByValidToken($sAccessToken);
0 ignored issues
show
Documentation Bug introduced by Pablo de la Peña
It seems like $oUserAccessTokenModel->...lidToken($sAccessToken) of type Nails\Common\Resource is incompatible with the declared type Nails\Auth\Resource\User\AccessToken of property $oAccessToken.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
Bug introduced by Pablo de la Peña
It seems like $sAccessToken can also be of type array; however, parameter $sToken of Nails\Auth\Model\User\Ac...oken::getByValidToken() 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

382
            $this->oAccessToken = $oUserAccessTokenModel->getByValidToken(/** @scrutinizer ignore-type */ $sAccessToken);
Loading history...
383
384
            if ($this->oAccessToken) {
385
                /** @var Auth\Model\User $oUserModel */
386
                $oUserModel = Factory::model('User', Auth\Constants::MODULE_SLUG);
387
                $oUserModel->setLoginData($this->oAccessToken->user_id, false);
0 ignored issues
show
Bug introduced by Pablo de la Peña
The property user_id does not seem to exist on Nails\Common\Resource.
Loading history...
388
389
            } else {
390
                throw new ApiException(
391
                    'Invalid access token',
392
                    $oHttpCodes::STATUS_UNAUTHORIZED
393
                );
394
            }
395
        }
396
397
        return $this;
398
    }
399
400
    // --------------------------------------------------------------------------
401
402
    /**
403
     * Discovers API controllers
404
     *
405
     * @return array
406
     * @throws ApiException
407
     * @throws NailsException
408
     */
409
    protected function discoverApiControllers(): array
410
    {
411
        $aControllerMap = [];
412
        foreach (Components::available() as $oModule) {
413
414
            $aClasses = $oModule
415
                ->findClasses('Api\\Controller')
416
                ->whichExtend(\Nails\Api\Controller\Base::class);
417
418
            $sNamespace = $oModule->slug === Components::$sAppSlug
419
                ? Components::$sAppSlug
420
                : ($oModule->data->{Constants::MODULE_SLUG}->namespace ?? null);
421
422
            if (empty($sNamespace) && count($aClasses)) {
423
                throw new ApiException(
424
                    sprintf(
425
                        'Found API controllers for module %s, but module does not define an API namespace',
426
                        $oModule->slug
427
                    )
428
                );
429
430
            } elseif (!count($aClasses)) {
431
                continue;
432
            }
433
434
            if (array_key_exists($sNamespace, $aControllerMap)) {
435
                throw new NailsException(
436
                    sprintf(
437
                        'Conflicting API namespace "%s" in use by "%s" and "%s"',
438
                        $sNamespace,
439
                        $oModule->slug,
440
                        $aControllerMap[$sNamespace]->module->slug
441
                    )
442
                );
443
            }
444
445
            $aControllerMap[$sNamespace] = (object) [
446
                'module'      => $oModule,
447
                'controllers' => [],
448
            ];
449
450
            foreach ($aClasses as $sClass) {
451
                $aControllerMap[$sNamespace]->controllers[strtolower($sClass)] = $sClass;
452
            }
453
        }
454
455
        return $aControllerMap;
456
    }
457
458
    // --------------------------------------------------------------------------
459
460
    /**
461
     * Normalises the controller class name, taking into account any defined remapping
462
     *
463
     * @param stdClass $oModule The module as created by discoverApiControllers
464
     *
465
     * @return string
466
     * @throws ApiException
467
     * @throws FactoryException
468
     */
469
    protected function normaliseControllerClass(stdClass $oModule): string
470
    {
471
        $aRemap = (array) ($oModule->module->data->{Constants::MODULE_SLUG}->{'controller-map'} ?? []);
472
        if (!empty($aRemap)) {
473
474
            $sOriginalController = $this->sClassName;
475
            $this->sClassName    = getFromArray($this->sClassName, $aRemap, $this->sClassName);
476
477
            //  This prevents users from accessing the "correct" controller, so we only have one valid route
478
            $sRemapped = array_search($sOriginalController, $aRemap);
479
            if ($sRemapped !== false) {
480
                $this->invalidApiRoute();
481
            }
482
        }
483
484
        $sController = $oModule->module->namespace . 'Api\\Controller\\' . $this->sClassName;
485
        $sController = $oModule->controllers[strtolower($sController)] ?? $sController;
486
487
        return $sController;
488
    }
489
490
    // --------------------------------------------------------------------------
491
492
    /**
493
     * Checks the controllers auth requirements
494
     *
495
     * @param string $sController The Controller class name
496
     *
497
     * @throws ApiException
498
     * @throws FactoryException
499
     */
500
    protected function checkControllerAuth(string $sController): void
501
    {
502
        /** @var HttpCodes $oHttpCodes */
503
        $oHttpCodes = Factory::service('HttpCodes');
504
        /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
505
        $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
0 ignored issues
show
Unused Code introduced by Pablo de la Peña
The assignment to $oUserAccessTokenModel is dead and can be removed.
Loading history...
506
507
        $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
508
        if ($mAuth !== true) {
509
510
            if (is_array($mAuth)) {
511
                throw new ApiException(
512
                    getFromArray('error', $mAuth, $oHttpCodes::getByCode($oHttpCodes::STATUS_UNAUTHORIZED)),
0 ignored issues
show
Bug introduced by Pablo de la Peña
Are you sure the usage of $oHttpCodes::getByCode($...s::STATUS_UNAUTHORIZED) targeting Nails\Common\Service\HttpCodes::getByCode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
513
                    (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED)
514
                );
515
516
            } else {
517
                throw new ApiException(
518
                    'You must be logged in to access this resource',
519
                    $oHttpCodes::STATUS_UNAUTHORIZED
520
                );
521
            }
522
        }
523
524
        if (!empty($sController::REQUIRE_SCOPE)) {
525
            if (!$this->oAccessToken->hasScope($sController::REQUIRE_SCOPE)) {
526
                throw new ApiException(
527
                    sprintf(
528
                        'Access token with "%s" scope is required.',
529
                        $sController::REQUIRE_SCOPE
530
                    ),
531
                    $oHttpCodes::STATUS_UNAUTHORIZED
532
                );
533
            }
534
        }
535
    }
536
537
    // --------------------------------------------------------------------------
538
539
    /**
540
     * Calls the appropriate controller method
541
     *
542
     * @param \Nails\Api\Controller\Base $oController The controller instance
543
     *
544
     * @return ApiResponse
545
     * @throws ApiException
546
     * @throws FactoryException
547
     */
548
    protected function callControllerMethod(\Nails\Api\Controller\Base $oController): ApiResponse
549
    {
550
        /**
551
         * We need to look for the appropriate method; we'll look in the following order:
552
         *
553
         * - {sRequestMethod}Remap()
554
         * - {sRequestMethod}{method}()
555
         * - anyRemap()
556
         * - any{method}()
557
         *
558
         * The second parameter is whether the method is a remap method or not.
559
         */
560
        $aMethods = [
561
            [
562
                'method'   => strtolower($this->sRequestMethod) . 'Remap',
563
                'is_remap' => true,
564
            ],
565
            [
566
                'method'   => strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
567
                'is_remap' => false,
568
            ],
569
            [
570
                'method'   => 'anyRemap',
571
                'is_remap' => true,
572
            ],
573
            [
574
                'method'   => 'any' . ucfirst($this->sMethod),
575
                'is_remap' => false,
576
            ],
577
        ];
578
579
        foreach ($aMethods as $aMethod) {
580
            if (is_callable([$oController, $aMethod['method']])) {
581
                /**
582
                 * If the method we're trying to call is a remap method, then the first
583
                 * param should be the name of the method being called
584
                 */
585
                if ($aMethod['is_remap']) {
586
                    return call_user_func_array(
587
                        [
588
                            $oController,
589
                            $aMethod['method'],
590
                        ],
591
                        [$this->sMethod]);
592
593
                } else {
594
                    return call_user_func([
595
                        $oController,
596
                        $aMethod['method'],
597
                    ]);
598
                }
599
            }
600
        }
601
602
        $this->invalidApiRoute();
603
    }
604
605
    // --------------------------------------------------------------------------
606
607
    /**
608
     * Throws an invalid API route 404 exception
609
     *
610
     * @throws ApiException
611
     * @throws FactoryException
612
     */
613
    protected function invalidApiRoute(): void
614
    {
615
        /** @var HttpCodes $oHttpCodes */
616
        $oHttpCodes = Factory::service('HttpCodes');
617
618
        $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
619
        $s404Error  = sprintf(
620
            '"%s: %s/%s/%s" is not a valid API route.',
621
            $this->getRequestMethod(),
622
            strtolower($this->sModuleName),
623
            strtolower($this->sClassName),
624
            strtolower($this->sMethod)
625
        );
626
627
        throw new ApiException($s404Error, $i404Status);
628
    }
629
630
    // --------------------------------------------------------------------------
631
632
    /**
633
     * Throws an invalid API format 400 exception
634
     *
635
     * @throws ApiException
636
     */
637
    protected function invalidApiFormat(): void
638
    {
639
        /** @var HttpCodes $oHttpCodes */
640
        $oHttpCodes = Factory::service('HttpCodes');
641
642
        throw new ApiException(
643
            sprintf(
644
                '"%s" is not a valid format.',
645
                $this->sOutputFormat
646
            ),
647
            $oHttpCodes::STATUS_BAD_REQUEST
648
        );
649
    }
650
651
    // --------------------------------------------------------------------------
652
653
    /**
654
     * Sends $aOut to the browser in the desired format
655
     *
656
     * @param array $aOut The data to output to the browser
657
     */
658
    protected function output($aOut = [])
659
    {
660
        /** @var Input $oInput */
661
        $oInput = Factory::service('Input');
662
        /** @var Output $oOutput */
663
        $oOutput = Factory::service('Output');
664
        /** @var HttpCodes $oHttpCodes */
665
        $oHttpCodes = Factory::service('HttpCodes');
666
667
        //  Set cache headers
668
        $oOutput
669
            ->setHeader('Cache-Control: no-store, no-cache, must-revalidate')
670
            ->setHeader('Expires: Mon, 26 Jul 1997 05:00:00 GMT')
671
            ->setHeader('Pragma: no-cache');
672
673
        //  Set access control headers
674
        $this->setCorsHeaders();
675
676
        // --------------------------------------------------------------------------
677
678
        //  Send the correct status header, default to 200 OK
679
        if ($this->bOutputSendHeader) {
680
            $sProtocol   = $oInput->server('SERVER_PROTOCOL');
681
            $iHttpCode   = getFromArray('status', $aOut, $oHttpCodes::STATUS_OK);
682
            $sHttpString = $oHttpCodes::getByCode($iHttpCode);
0 ignored issues
show
Bug introduced by Pablo de la Peña
Are you sure the assignment to $sHttpString is correct as $oHttpCodes::getByCode($iHttpCode) targeting Nails\Common\Service\HttpCodes::getByCode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
683
            $oOutput->setHeader($sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
0 ignored issues
show
Bug introduced by Pablo de la Peña
Are you sure $sProtocol of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

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

683
            $oOutput->setHeader(/** @scrutinizer ignore-type */ $sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
Loading history...
684
        }
685
686
        // --------------------------------------------------------------------------
687
688
        //  Output content
689
        $sOutputClass = static::$aOutputValidFormats[$this->sOutputFormat];
690
        $oOutput->setContentType($sOutputClass::getContentType());
691
692
        if (array_key_exists('body', $aOut) && $aOut['body'] !== null) {
693
            $sOut = $aOut['body'];
694
        } else {
695
            unset($aOut['body']);
696
            $sOut = $sOutputClass::render($aOut);
697
        }
698
699
        $oOutput->setOutput($sOut);
700
    }
701
702
    // --------------------------------------------------------------------------
703
704
    /**
705
     * Sets CORS headers
706
     *
707
     * @return $this
708
     * @throws FactoryException
709
     */
710
    protected function setCorsHeaders(): self
711
    {
712
        /** @var Output $oOutput */
713
        $oOutput = Factory::service('Output');
714
        $oOutput
715
            ->setHeader('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN)
716
            ->setHeader('Access-Control-Allow-Credentials: ' . static::ACCESS_CONTROL_ALLOW_CREDENTIALS)
717
            ->setHeader('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS))
718
            ->setHeader('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS))
719
            ->setHeader('Access-Control-Max-Age: ' . static::ACCESS_CONTROL_MAX_AGE);
720
721
        return $this;
722
    }
723
724
    // --------------------------------------------------------------------------
725
726
    /**
727
     * Set the CORS status header
728
     *
729
     * @return $this
730
     * @throws FactoryException
731
     */
732
    protected function setCorsStatusHeader(): self
733
    {
734
        /** @var Output $oOutput */
735
        $oOutput = Factory::service('Output');
736
        $oOutput->setStatusHeader(HttpCodes::STATUS_NO_CONTENT);
737
        return $this;
738
    }
739
740
    // --------------------------------------------------------------------------
741
742
    /**
743
     * Sets the output format
744
     *
745
     * @param string $sFormat The format to use
746
     *
747
     * @return bool
748
     */
749
    public function outputSetFormat($sFormat): bool
750
    {
751
        if (static::isValidFormat($sFormat)) {
752
            $this->sOutputFormat = strtoupper($sFormat);
753
            return true;
754
        }
755
756
        return false;
757
    }
758
759
    // --------------------------------------------------------------------------
760
761
    /**
762
     * Returns the putput format
763
     *
764
     * @return string
765
     */
766
    public function outputGetFormat(): string
767
    {
768
        return $this->sOutputFormat;
769
    }
770
771
    // --------------------------------------------------------------------------
772
773
    /**
774
     * Sets whether the status header should be sent or not
775
     *
776
     * @param bool $sendHeader Whether the header should be sent or not
777
     */
778
    public function outputSendHeader($bSendHeader): bool
779
    {
780
        $this->bOutputSendHeader = !empty($bSendHeader);
781
    }
782
783
    // --------------------------------------------------------------------------
784
785
    /**
786
     * Determines whether the format is valid
787
     *
788
     * @param string $sFormat The format to check
789
     *
790
     * @return bool
791
     */
792
    private static function isValidFormat(?string $sFormat): bool
793
    {
794
        return in_array(strtoupper($sFormat), array_keys(static::$aOutputValidFormats));
0 ignored issues
show
Bug introduced by Pablo de la Peña
It seems like $sFormat can also be of type null; 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

794
        return in_array(strtoupper(/** @scrutinizer ignore-type */ $sFormat), array_keys(static::$aOutputValidFormats));
Loading history...
795
    }
796
797
    // --------------------------------------------------------------------------
798
799
    /**
800
     * Write a line to the API log
801
     *
802
     * @param string $sLine The line to write
803
     */
804
    public function writeLog($sLine)
805
    {
806
        if (!is_string($sLine)) {
0 ignored issues
show
introduced by Pablo de la Peña
The condition is_string($sLine) is always true.
Loading history...
807
            $sLine = print_r($sLine, true);
808
        }
809
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
810
        $this->oLogger->line($sLine);
811
    }
812
813
    // --------------------------------------------------------------------------
814
815
    /**
816
     * Returns the current request method
817
     *
818
     * @return string
819
     */
820
    public function getRequestMethod(): string
821
    {
822
        return $this->sRequestMethod;
823
    }
824
825
    // --------------------------------------------------------------------------
826
827
    /**
828
     * Confirms whether the request is of the supplied type
829
     *
830
     * @param string $sMethod The request method to check
831
     *
832
     * @return bool
833
     */
834
    protected function isRequestMethod(string $sMethod): bool
835
    {
836
        return $this->getRequestMethod() === $sMethod;
837
    }
838
839
    // --------------------------------------------------------------------------
840
841
    /**
842
     * Determines whether the request is a GET request
843
     *
844
     * @return bool
845
     */
846
    public function isGetRequest(): bool
847
    {
848
        return $this->isRequestMethod(static::REQUEST_METHOD_GET);
849
    }
850
851
    // --------------------------------------------------------------------------
852
853
    /**
854
     * Determines whether the request is a PUT request
855
     *
856
     * @return bool
857
     */
858
    public function isPutRequest(): bool
859
    {
860
        return $this->isRequestMethod(static::REQUEST_METHOD_PUT);
861
    }
862
863
    // --------------------------------------------------------------------------
864
865
    /**
866
     * Determines whether the request is a POST request
867
     *
868
     * @return bool
869
     */
870
    public function isPostRequest(): bool
871
    {
872
        return $this->isRequestMethod(static::REQUEST_METHOD_POST);
873
    }
874
875
    // --------------------------------------------------------------------------
876
877
    /**
878
     * Determines whether the request is a DELETE request
879
     *
880
     * @return bool
881
     */
882
    public function isDeleteRequest(): bool
883
    {
884
        return $this->isRequestMethod(static::REQUEST_METHOD_DELETE);
885
    }
886
887
    // --------------------------------------------------------------------------
888
889
    /**
890
     * Returns the current Access Token
891
     *
892
     * @return string|null
893
     */
894
    public function getAccessToken(): ?string
895
    {
896
        return $this->sAccessToken;
897
    }
898
}
899