Passed
Push — develop ( eb9fb5...9f9ae3 )
by Pablo
02:48
created

ApiRouter::checkControllerAuth()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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

622
            $oOutput->setHeader(/** @scrutinizer ignore-type */ $sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
Loading history...
623
        }
624
625
        // --------------------------------------------------------------------------
626
627
        //  Output content
628
        $sOutputClass = static::$aOutputValidFormats[$this->sOutputFormat];
629
        $oOutput->setContentType($sOutputClass::getContentType());
630
631
        if (array_key_exists('body', $aOut) && $aOut['body'] !== null) {
632
            $sOut = $aOut['body'];
633
        } else {
634
            unset($aOut['body']);
635
            $sOut = $sOutputClass::render($aOut);
636
        }
637
638
        $oOutput->setOutput($sOut);
639
    }
640
641
    // --------------------------------------------------------------------------
642
643
    /**
644
     * Sets CORS headers
645
     *
646
     * @return $this
647
     * @throws FactoryException
648
     */
649
    protected function setCorsHeaders(): self
650
    {
651
        /** @var Output $oOutput */
652
        $oOutput = Factory::service('Output');
653
        $oOutput
654
            ->setHeader('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN)
655
            ->setHeader('Access-Control-Allow-Credentials: ' . static::ACCESS_CONTROL_ALLOW_CREDENTIALS)
656
            ->setHeader('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS))
657
            ->setHeader('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS))
658
            ->setHeader('Access-Control-Max-Age: ' . static::ACCESS_CONTROL_MAX_AGE);
659
660
        return $this;
661
    }
662
663
    // --------------------------------------------------------------------------
664
665
    /**
666
     * Set the CORS status header
667
     *
668
     * @return $this
669
     * @throws FactoryException
670
     */
671
    protected function setCorsStatusHeader(): self
672
    {
673
        /** @var Output $oOutput */
674
        $oOutput = Factory::service('Output');
675
        $oOutput->setStatusHeader(HttpCodes::STATUS_NO_CONTENT);
676
        return $this;
677
    }
678
679
    // --------------------------------------------------------------------------
680
681
    /**
682
     * Sets the output format
683
     *
684
     * @param string $sFormat The format to use
685
     *
686
     * @return bool
687
     */
688
    public function outputSetFormat($sFormat): bool
689
    {
690
        if (static::isValidFormat($sFormat)) {
691
            $this->sOutputFormat = strtoupper($sFormat);
692
            return true;
693
        }
694
695
        return false;
696
    }
697
698
    // --------------------------------------------------------------------------
699
700
    /**
701
     * Returns the putput format
702
     *
703
     * @return string
704
     */
705
    public function outputGetFormat(): string
706
    {
707
        return $this->sOutputFormat;
708
    }
709
710
    // --------------------------------------------------------------------------
711
712
    /**
713
     * Detects the putput frmat from the URI
714
     *
715
     * @return string
716
     */
717
    public static function detectOutputFormat(): string
718
    {
719
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
720
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
721
722
        return static::isValidFormat($sFormat)
723
            ? $sFormat
724
            : static::DEFAULT_FORMAT;
725
    }
726
727
    // --------------------------------------------------------------------------
728
729
    /**
730
     * Sets whether the status header should be sent or not
731
     *
732
     * @param bool $sendHeader Whether the header should be sent or not
733
     */
734
    public function outputSendHeader($sendHeader): bool
735
    {
736
        $this->bOutputSendHeader = !empty($sendHeader);
737
    }
738
739
    // --------------------------------------------------------------------------
740
741
    /**
742
     * Determines whether the format is valid
743
     *
744
     * @param string $sFormat The format to check
745
     *
746
     * @return bool
747
     */
748
    private static function isValidFormat(?string $sFormat): bool
749
    {
750
        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

750
        return in_array(strtoupper(/** @scrutinizer ignore-type */ $sFormat), array_keys(static::$aOutputValidFormats));
Loading history...
751
    }
752
753
    // --------------------------------------------------------------------------
754
755
    /**
756
     * Write a line to the API log
757
     *
758
     * @param string $sLine The line to write
759
     */
760
    public function writeLog($sLine)
761
    {
762
        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...
763
            $sLine = print_r($sLine, true);
764
        }
765
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
766
        $this->oLogger->line($sLine);
767
    }
768
769
    // --------------------------------------------------------------------------
770
771
    /**
772
     * Returns the current request method
773
     *
774
     * @return string
775
     */
776
    public function getRequestMethod(): string
777
    {
778
        return $this->sRequestMethod;
779
    }
780
781
    // --------------------------------------------------------------------------
782
783
    /**
784
     * Confirms whether the request is of the supplied type
785
     *
786
     * @param string $sMethod The request method to check
787
     *
788
     * @return bool
789
     */
790
    protected function isRequestMethod(string $sMethod): bool
791
    {
792
        return $this->getRequestMethod() === $sMethod;
793
    }
794
795
    // --------------------------------------------------------------------------
796
797
    /**
798
     * Determines whether the request is a GET request
799
     *
800
     * @return bool
801
     */
802
    public function isGetRequest(): bool
803
    {
804
        return $this->isRequestMethod(static::REQUEST_METHOD_GET);
805
    }
806
807
    // --------------------------------------------------------------------------
808
809
    /**
810
     * Determines whether the request is a PUT request
811
     *
812
     * @return bool
813
     */
814
    public function isPutRequest(): bool
815
    {
816
        return $this->isRequestMethod(static::REQUEST_METHOD_PUT);
817
    }
818
819
    // --------------------------------------------------------------------------
820
821
    /**
822
     * Determines whether the request is a POST request
823
     *
824
     * @return bool
825
     */
826
    public function isPostRequest(): bool
827
    {
828
        return $this->isRequestMethod(static::REQUEST_METHOD_POST);
829
    }
830
831
    // --------------------------------------------------------------------------
832
833
    /**
834
     * Determines whether the request is a DELETE request
835
     *
836
     * @return bool
837
     */
838
    public function isDeleteRequest(): bool
839
    {
840
        return $this->isRequestMethod(static::REQUEST_METHOD_DELETE);
841
    }
842
843
    // --------------------------------------------------------------------------
844
845
    /**
846
     * Returns the current Access Token
847
     *
848
     * @return string|null
849
     */
850
    public function getAccessToken(): ?string
851
    {
852
        return $this->sAccessToken;
853
    }
854
}
855