ApiRouter::index()   F
last analyzed

Complexity

Conditions 19
Paths 291

Size

Total Lines 115
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 23
Bugs 0 Features 0
Metric Value
eloc 62
c 23
b 0
f 0
dl 0
loc 115
rs 2.5458
cc 19
nc 291
nop 0

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

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

699
            $oOutput->setHeader(/** @scrutinizer ignore-type */ $sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
Loading history...
700
        }
701
702
        // --------------------------------------------------------------------------
703
704
        //  Output content
705
        $sOutputClass = static::$aOutputValidFormats[$this->outputGetFormat()];
706
        $oOutput->setContentType($sOutputClass::getContentType());
707
708
        if (array_key_exists('body', $aOut) && $aOut['body'] !== null) {
709
            $sOut = $aOut['body'];
710
        } else {
711
            unset($aOut['body']);
712
            $sOut = $sOutputClass::render($aOut);
713
        }
714
715
        $oOutput->setOutput($sOut);
716
    }
717
718
    // --------------------------------------------------------------------------
719
720
    /**
721
     * Sets CORS headers
722
     *
723
     * @return $this
724
     * @throws FactoryException
725
     */
726
    protected function setCorsHeaders(): self
727
    {
728
        /** @var Output $oOutput */
729
        $oOutput = Factory::service('Output');
730
        $oOutput
731
            ->setHeader('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN)
732
            ->setHeader('Access-Control-Allow-Credentials: ' . static::ACCESS_CONTROL_ALLOW_CREDENTIALS)
733
            ->setHeader('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS))
734
            ->setHeader('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS))
735
            ->setHeader('Access-Control-Max-Age: ' . static::ACCESS_CONTROL_MAX_AGE);
736
737
        return $this;
738
    }
739
740
    // --------------------------------------------------------------------------
741
742
    /**
743
     * Set the CORS status header
744
     *
745
     * @return $this
746
     * @throws FactoryException
747
     */
748
    protected function setCorsStatusHeader(): self
749
    {
750
        /** @var Output $oOutput */
751
        $oOutput = Factory::service('Output');
752
        $oOutput->setStatusHeader(HttpCodes::STATUS_NO_CONTENT);
753
        return $this;
754
    }
755
756
    // --------------------------------------------------------------------------
757
758
    /**
759
     * Sets the output format
760
     *
761
     * @param string $sFormat The format to use
762
     *
763
     * @return bool
764
     */
765
    public function outputSetFormat($sFormat): bool
766
    {
767
        if (static::isValidFormat($sFormat)) {
768
            $this->sOutputFormat = strtoupper($sFormat);
769
            return true;
770
        }
771
772
        return false;
773
    }
774
775
    // --------------------------------------------------------------------------
776
777
    /**
778
     * Returns the putput format
779
     *
780
     * @return string
781
     */
782
    public function outputGetFormat(): string
783
    {
784
        if (empty($this->sOutputFormat)) {
785
            $this->sOutputFormat = static::parseOutputFormatFromUri();
786
        }
787
788
        return $this->sOutputFormat;
789
    }
790
791
    // --------------------------------------------------------------------------
792
793
    /**
794
     * Parses the outputformat from the URI
795
     *
796
     * @return string
797
     */
798
    public static function parseOutputFormatFromUri(): string
799
    {
800
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
801
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
802
803
        return static::isValidFormat($sFormat)
804
            ? $sFormat
805
            : static::DEFAULT_FORMAT;
806
    }
807
808
    // --------------------------------------------------------------------------
809
810
    /**
811
     * Sets whether the status header should be sent or not
812
     *
813
     * @param bool $sendHeader Whether the header should be sent or not
814
     */
815
    public function outputSendHeader($bSendHeader): bool
816
    {
817
        $this->bOutputSendHeader = !empty($bSendHeader);
818
    }
819
820
    // --------------------------------------------------------------------------
821
822
    /**
823
     * Determines whether the format is valid
824
     *
825
     * @param string $sFormat The format to check
826
     *
827
     * @return bool
828
     */
829
    private static function isValidFormat(?string $sFormat): bool
830
    {
831
        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

831
        return in_array(strtoupper(/** @scrutinizer ignore-type */ $sFormat), array_keys(static::$aOutputValidFormats));
Loading history...
832
    }
833
834
    // --------------------------------------------------------------------------
835
836
    /**
837
     * Write a line to the API log
838
     *
839
     * @param string $sLine The line to write
840
     */
841
    public function writeLog($sLine)
842
    {
843
        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...
844
            $sLine = print_r($sLine, true);
845
        }
846
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
847
        $this->oLogger->line($sLine);
848
    }
849
850
    // --------------------------------------------------------------------------
851
852
    /**
853
     * Returns the current request method
854
     *
855
     * @return string
856
     */
857
    public function getRequestMethod(): string
858
    {
859
        return $this->sRequestMethod;
860
    }
861
862
    // --------------------------------------------------------------------------
863
864
    /**
865
     * Confirms whether the request is of the supplied type
866
     *
867
     * @param string $sMethod The request method to check
868
     *
869
     * @return bool
870
     */
871
    protected function isRequestMethod(string $sMethod): bool
872
    {
873
        return $this->getRequestMethod() === $sMethod;
874
    }
875
876
    // --------------------------------------------------------------------------
877
878
    /**
879
     * Determines whether the request is a GET request
880
     *
881
     * @return bool
882
     */
883
    public function isGetRequest(): bool
884
    {
885
        return $this->isRequestMethod(static::REQUEST_METHOD_GET);
886
    }
887
888
    // --------------------------------------------------------------------------
889
890
    /**
891
     * Determines whether the request is a PUT request
892
     *
893
     * @return bool
894
     */
895
    public function isPutRequest(): bool
896
    {
897
        return $this->isRequestMethod(static::REQUEST_METHOD_PUT);
898
    }
899
900
    // --------------------------------------------------------------------------
901
902
    /**
903
     * Determines whether the request is a POST request
904
     *
905
     * @return bool
906
     */
907
    public function isPostRequest(): bool
908
    {
909
        return $this->isRequestMethod(static::REQUEST_METHOD_POST);
910
    }
911
912
    // --------------------------------------------------------------------------
913
914
    /**
915
     * Determines whether the request is a DELETE request
916
     *
917
     * @return bool
918
     */
919
    public function isDeleteRequest(): bool
920
    {
921
        return $this->isRequestMethod(static::REQUEST_METHOD_DELETE);
922
    }
923
924
    // --------------------------------------------------------------------------
925
926
    /**
927
     * Returns the current Access Token
928
     *
929
     * @return string|null
930
     */
931
    public function getAccessToken(): ?string
932
    {
933
        return $this->sAccessToken;
934
    }
935
}
936