Passed
Push — develop ( 9f9ae3...7cea29 )
by Pablo
02:27
created

ApiRouter::detectRequestMethod()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 1
nop 0
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
    protected $oAccessToken;
104
105
    // --------------------------------------------------------------------------
106
107
    /**
108
     * ApiRouter constructor.
109
     *
110
     * @throws FactoryException
111
     */
112
    public function __construct()
113
    {
114
        parent::__construct();
115
        $this
116
            ->configureLogging()
117
            ->detectRequestMethod()
118
            ->detectOutputFormat()
119
            ->detectUriSegments();
120
    }
121
122
    // --------------------------------------------------------------------------
123
124
    /**
125
     * Route the call to the correct place
126
     */
127
    public function index()
128
    {
129
        //  Handle OPTIONS CORS pre-flight requests
130
        if ($this->sRequestMethod === static::REQUEST_METHOD_OPTIONS) {
131
132
            $this
133
                ->setCorsHeaders()
134
                ->setCorsStatusHeader();
135
            return;
136
137
        } else {
138
139
            try {
140
141
                /** @var HttpCodes $oHttpCodes */
142
                $oHttpCodes = Factory::service('HttpCodes');
143
                /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
144
                $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...
145
146
                // --------------------------------------------------------------------------
147
148
                $this->verifyAccessToken();
149
150
                // --------------------------------------------------------------------------
151
152
                if (!$this->outputSetFormat($this->sOutputFormat)) {
153
                    $this->invalidApiFormat();
154
                }
155
156
                // --------------------------------------------------------------------------
157
158
                $aControllerMap = $this->discoverApiControllers();
159
                $oModule        = $aControllerMap[$this->sModuleName] ?? null;
160
161
                if (empty($oModule)) {
162
                    $this->invalidApiRoute();
163
                }
164
165
                $sController = $this->normaliseControllerClass($oModule);
166
167
                if (!class_exists($sController)) {
168
                    $this->invalidApiRoute();
169
                }
170
171
                $this->checkControllerAuth($sController);
172
173
                $oResponse = $this->callControllerMethod(
174
                    new $sController($this)
175
                );
176
177
                $aOut = [
178
                    'status' => $oResponse->getCode(),
179
                    'body'   => $oResponse->getBody(),
180
                    'data'   => $oResponse->getData(),
181
                    'meta'   => $oResponse->getMeta(),
182
                ];
183
184
            } catch (ValidationException $e) {
185
186
                $aOut = [
187
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
188
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
189
                    'details' => $e->getData() ?: [],
190
                ];
191
                if (isSuperuser()) {
192
                    $aOut['exception'] = (object) array_filter([
193
                        'type' => get_class($e),
194
                        'file' => $e->getFile(),
195
                        'line' => $e->getLine(),
196
                    ]);
197
                }
198
199
                $this->writeLog($aOut);
200
201
            } catch (ApiException $e) {
202
203
                $aOut = [
204
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
205
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
206
                    'details' => $e->getData() ?: [],
207
                ];
208
                if (isSuperuser()) {
209
                    $aOut['exception'] = (object) array_filter([
210
                        'type' => get_class($e),
211
                        'file' => $e->getFile(),
212
                        'line' => $e->getLine(),
213
                    ]);
214
                }
215
216
                $this->writeLog($aOut);
217
218
            } catch (\Exception $e) {
219
220
                /**
221
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
222
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
223
                 * developer the error quickly and with as much info as possible.
224
                 */
225
                if (Environment::is(Environment::ENV_PROD)) {
226
                    throw $e;
227
                } else {
228
                    $aOut = [
229
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
230
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
231
                        'exception' => (object) array_filter([
232
                            'type' => get_class($e),
233
                            'file' => $e->getFile(),
234
                            'line' => $e->getLine(),
235
                        ]),
236
                    ];
237
238
                    $this->writeLog($aOut);
239
                }
240
            }
241
242
            $this->output($aOut);
243
        }
244
    }
245
246
    // --------------------------------------------------------------------------
247
248
    /**
249
     * Configures API logging
250
     *
251
     * @return $this
252
     * @throws FactoryException
253
     */
254
    protected function configureLogging(): self
255
    {
256
        /** @var \Nails\Common\Resource\DateTime $oNow */
257
        $oNow = Factory::factory('DateTime');
258
        /** @var Logger oLogger */
259
        $this->oLogger = Factory::factory('Logger');
260
        $this->oLogger->setFile('api-' . $oNow->format('Y-m-d') . '.php');
261
262
        return $this;
263
    }
264
265
    // --------------------------------------------------------------------------
266
267
    /**
268
     * Detects the request method being used
269
     *
270
     * @return $this
271
     * @throws FactoryException
272
     */
273
    protected function detectRequestMethod(): self
274
    {
275
        /** @var Input $oInput */
276
        $oInput               = Factory::service('Input');
277
        $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...
278
279
        return $this;
280
    }
281
282
    // --------------------------------------------------------------------------
283
284
    /**
285
     * Detects and validates the output format from the URL
286
     *
287
     * @return $this
288
     * @throws NailsException
289
     */
290
    protected function detectOutputFormat(): self
291
    {
292
        //  Look for valid output formats
293
        $aComponents = Components::available();
294
295
        //  Shift the app onto the end so it overrides any module supplied formats
296
        $oApp = array_shift($aComponents);
297
        array_push($aComponents, $oApp);
298
299
        foreach ($aComponents as $oComponent) {
300
301
            $oClasses = $oComponent
302
                ->findClasses('Api\Output')
303
                ->whichImplement(\Nails\Api\Interfaces\Output::class);
304
305
            foreach ($oClasses as $sClass) {
306
                static::$aOutputValidFormats[strtoupper($sClass::getSlug())] = $sClass;
307
            }
308
        }
309
310
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
311
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
312
313
        $this->sOutputFormat = static::isValidFormat($sFormat)
314
            ? $sFormat
315
            : static::DEFAULT_FORMAT;
316
317
        return $this;
318
    }
319
320
    // --------------------------------------------------------------------------
321
322
    /**
323
     * Extracts the module, controller and method segments from the URI
324
     *
325
     * @return $this
326
     */
327
    protected function detectUriSegments(): self
328
    {
329
        /**
330
         * In order to work out the next few parts we'll analyse the URI string manually.
331
         * We're doing this because of the optional return type at the end of the string;
332
         * it's easier to regex that quickly, remove it, then split up the segments.
333
         */
334
        $sUri = preg_replace(static::OUTPUT_FORMAT_PATTERN, '', uri_string());
335
336
        //  Remove the module prefix (i.e "api/") then explode into segments
337
        //  Using regex as some systems will report a leading slash (e.g CLI)
338
        $sUri = preg_replace('#^/?api/#', '', $sUri);
339
        $aUri = explode('/', $sUri);
340
341
        $this->sModuleName = getFromArray(0, $aUri);
342
        $this->sClassName  = getFromArray(1, $aUri, $this->sModuleName);
343
        $this->sMethod     = getFromArray(2, $aUri, 'index');
344
345
        return $this;
346
    }
347
348
    // --------------------------------------------------------------------------
349
350
    /**
351
     * Verifies the access token, if supplied. Passing the token via the header is
352
     * preferred, but fallback to the GET and POST arrays.
353
     *
354
     * @return $this
355
     * @throws ApiException
356
     * @throws NailsException
357
     * @throws ReflectionException
358
     * @throws FactoryException
359
     * @throws ModelException
360
     */
361
    protected function verifyAccessToken(): self
362
    {
363
        /** @var Input $oInput */
364
        $oInput = Factory::service('Input');
365
        /** @var HttpCodes $oHttpCodes */
366
        $oHttpCodes = Factory::service('HttpCodes');
367
        /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
368
        $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
369
370
        $sAccessToken = $oInput->header(static::ACCESS_TOKEN_HEADER);
371
372
        if (!$sAccessToken) {
373
            $sAccessToken = $oInput->post(static::ACCESS_TOKEN_POST_PARAM);
374
        }
375
376
        if (!$sAccessToken) {
377
            $sAccessToken = $oInput->get(static::ACCESS_TOKEN_GET_PARAM);
378
        }
379
380
        if ($sAccessToken) {
381
382
            $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...
383
            $this->oAccessToken = $oUserAccessTokenModel->getByValidToken($sAccessToken);
0 ignored issues
show
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

383
            $this->oAccessToken = $oUserAccessTokenModel->getByValidToken(/** @scrutinizer ignore-type */ $sAccessToken);
Loading history...
384
385
            if ($this->oAccessToken) {
386
                /** @var Auth\Model\User $oUserModel */
387
                $oUserModel = Factory::model('User', Auth\Constants::MODULE_SLUG);
388
                $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...
389
390
            } else {
391
                throw new ApiException(
392
                    'Invalid access token',
393
                    $oHttpCodes::STATUS_UNAUTHORIZED
394
                );
395
            }
396
        }
397
398
        return $this;
399
    }
400
401
    // --------------------------------------------------------------------------
402
403
    /**
404
     * Discovers API controllers
405
     *
406
     * @return array
407
     * @throws ApiException
408
     * @throws NailsException
409
     */
410
    protected function discoverApiControllers(): array
411
    {
412
        $aControllerMap = [];
413
        foreach (Components::available() as $oModule) {
414
415
            $aClasses = $oModule
416
                ->findClasses('Api\\Controller')
417
                ->whichExtend(\Nails\Api\Controller\Base::class);
418
419
            $sNamespace = $oModule->slug === Components::$sAppSlug
420
                ? Components::$sAppSlug
421
                : ($oModule->data->{Constants::MODULE_SLUG}->namespace ?? null);
422
423
            if (empty($sNamespace) && count($aClasses)) {
424
                throw new ApiException(
425
                    sprintf(
426
                        'Found API controllers for module %s, but module does not define an API namespace',
427
                        $oModule->slug
428
                    )
429
                );
430
431
            } elseif (!count($aClasses)) {
432
                continue;
433
            }
434
435
            if (array_key_exists($sNamespace, $aControllerMap)) {
436
                throw new NailsException(
437
                    sprintf(
438
                        'Conflicting API namespace "%s" in use by "%s" and "%s"',
439
                        $sNamespace,
440
                        $oModule->slug,
441
                        $aControllerMap[$sNamespace]->module->slug
442
                    )
443
                );
444
            }
445
446
            $aControllerMap[$sNamespace] = (object) [
447
                'module'      => $oModule,
448
                'controllers' => [],
449
            ];
450
451
            foreach ($aClasses as $sClass) {
452
                $aControllerMap[$sNamespace]->controllers[strtolower($sClass)] = $sClass;
453
            }
454
        }
455
456
        return $aControllerMap;
457
    }
458
459
    // --------------------------------------------------------------------------
460
461
    /**
462
     * Normalises the controller class name, taking into account any defined remapping
463
     *
464
     * @param stdClass $oModule The module as created by discoverApiControllers
465
     *
466
     * @return string
467
     * @throws ApiException
468
     * @throws FactoryException
469
     */
470
    protected function normaliseControllerClass(stdClass $oModule): string
471
    {
472
        $aRemap = (array) ($oModule->module->data->{Constants::MODULE_SLUG}->{'controller-map'} ?? []);
473
        if (!empty($aRemap)) {
474
475
            $sOriginalController = $this->sClassName;
476
            $this->sClassName    = getFromArray($this->sClassName, $aRemap, $this->sClassName);
477
478
            //  This prevents users from accessing the "correct" controller, so we only have one valid route
479
            $sRemapped = array_search($sOriginalController, $aRemap);
480
            if ($sRemapped !== false) {
481
                $this->invalidApiRoute();
482
            }
483
        }
484
485
        $sController = $oModule->module->namespace . 'Api\\Controller\\' . $this->sClassName;
486
        $sController = $oModule->controllers[strtolower($sController)] ?? $sController;
487
488
        return $sController;
489
    }
490
491
    // --------------------------------------------------------------------------
492
493
    /**
494
     * Checks the controllers auth requirements
495
     *
496
     * @param string $sController The Controller class name
497
     *
498
     * @throws ApiException
499
     * @throws FactoryException
500
     */
501
    protected function checkControllerAuth(string $sController): void
502
    {
503
        /** @var HttpCodes $oHttpCodes */
504
        $oHttpCodes = Factory::service('HttpCodes');
505
        /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
506
        $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
507
508
        $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
509
        if ($mAuth !== true) {
510
511
            if (is_array($mAuth)) {
512
                throw new ApiException(
513
                    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...
514
                    (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED)
515
                );
516
517
            } else {
518
                throw new ApiException(
519
                    'You must be logged in to access this resource',
520
                    $oHttpCodes::STATUS_UNAUTHORIZED
521
                );
522
            }
523
        }
524
525
        if (!empty($sController::REQUIRE_SCOPE)) {
526
            if (!$oUserAccessTokenModel->hasScope($this->oAccessToken, $sController::REQUIRE_SCOPE)) {
527
                throw new ApiException(
528
                    sprintf(
529
                        'Access token with "%s" scope is required.',
530
                        $sController::REQUIRE_SCOPE
531
                    ),
532
                    $oHttpCodes::STATUS_UNAUTHORIZED
533
                );
534
            }
535
        }
536
    }
537
538
    // --------------------------------------------------------------------------
539
540
    /**
541
     * Calls the appropriate controller method
542
     *
543
     * @param \Nails\Api\Controller\Base $oController The controller instance
544
     *
545
     * @return ApiResponse
546
     * @throws ApiException
547
     * @throws FactoryException
548
     */
549
    protected function callControllerMethod(\Nails\Api\Controller\Base $oController): ApiResponse
550
    {
551
        /**
552
         * We need to look for the appropriate method; we'll look in the following order:
553
         *
554
         * - {sRequestMethod}Remap()
555
         * - {sRequestMethod}{method}()
556
         * - anyRemap()
557
         * - any{method}()
558
         *
559
         * The second parameter is whether the method is a remap method or not.
560
         */
561
        $aMethods = [
562
            [
563
                'method'   => strtolower($this->sRequestMethod) . 'Remap',
564
                'is_remap' => true,
565
            ],
566
            [
567
                'method'   => strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
568
                'is_remap' => false,
569
            ],
570
            [
571
                'method'   => 'anyRemap',
572
                'is_remap' => true,
573
            ],
574
            [
575
                'method'   => 'any' . ucfirst($this->sMethod),
576
                'is_remap' => false,
577
            ],
578
        ];
579
580
        foreach ($aMethods as $aMethod) {
581
            if (is_callable([$oController, $aMethod['method']])) {
582
                /**
583
                 * If the method we're trying to call is a remap method, then the first
584
                 * param should be the name of the method being called
585
                 */
586
                if ($aMethod['is_remap']) {
587
                    return call_user_func_array(
588
                        [
589
                            $oController,
590
                            $aMethod['method'],
591
                        ],
592
                        [$this->sMethod]);
593
594
                } else {
595
                    return call_user_func([
596
                        $oController,
597
                        $aMethod['method'],
598
                    ]);
599
                }
600
            }
601
        }
602
603
        $this->invalidApiRoute();
604
    }
605
606
    // --------------------------------------------------------------------------
607
608
    /**
609
     * Throws an invalid API route 404 exception
610
     *
611
     * @throws ApiException
612
     * @throws FactoryException
613
     */
614
    protected function invalidApiRoute(): void
615
    {
616
        /** @var HttpCodes $oHttpCodes */
617
        $oHttpCodes = Factory::service('HttpCodes');
618
619
        $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
620
        $s404Error  = sprintf(
621
            '"%s: %s/%s/%s" is not a valid API route.',
622
            $this->getRequestMethod(),
623
            strtolower($this->sModuleName),
624
            strtolower($this->sClassName),
625
            strtolower($this->sMethod)
626
        );
627
628
        throw new ApiException($s404Error, $i404Status);
629
    }
630
631
    // --------------------------------------------------------------------------
632
633
    /**
634
     * Throws an invalid API format 400 exception
635
     *
636
     * @throws ApiException
637
     */
638
    protected function invalidApiFormat(): void
639
    {
640
        /** @var HttpCodes $oHttpCodes */
641
        $oHttpCodes = Factory::service('HttpCodes');
642
643
        throw new ApiException(
644
            sprintf(
645
                '"%s" is not a valid format.',
646
                $this->sOutputFormat
647
            ),
648
            $oHttpCodes::STATUS_BAD_REQUEST
649
        );
650
    }
651
652
    // --------------------------------------------------------------------------
653
654
    /**
655
     * Sends $aOut to the browser in the desired format
656
     *
657
     * @param array $aOut The data to output to the browser
658
     */
659
    protected function output($aOut = [])
660
    {
661
        /** @var Input $oInput */
662
        $oInput = Factory::service('Input');
663
        /** @var Output $oOutput */
664
        $oOutput = Factory::service('Output');
665
        /** @var HttpCodes $oHttpCodes */
666
        $oHttpCodes = Factory::service('HttpCodes');
667
668
        //  Set cache headers
669
        $oOutput
670
            ->setHeader('Cache-Control: no-store, no-cache, must-revalidate')
671
            ->setHeader('Expires: Mon, 26 Jul 1997 05:00:00 GMT')
672
            ->setHeader('Pragma: no-cache');
673
674
        //  Set access control headers
675
        $this->setCorsHeaders();
676
677
        // --------------------------------------------------------------------------
678
679
        //  Send the correct status header, default to 200 OK
680
        if ($this->bOutputSendHeader) {
681
            $sProtocol   = $oInput->server('SERVER_PROTOCOL');
682
            $iHttpCode   = getFromArray('status', $aOut, $oHttpCodes::STATUS_OK);
683
            $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...
684
            $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

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

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