Passed
Push — develop ( 14940b...eb9fb5 )
by Pablo
06:14 queued 04:12
created

ApiRouter::setCorsStatusHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
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 6
rs 10
cc 1
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\NailsException;
18
use Nails\Common\Exception\ValidationException;
19
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...
20
use Nails\Common\Factory\Logger;
21
use Nails\Common\Service\HttpCodes;
22
use Nails\Common\Service\Input;
23
use Nails\Common\Service\Output;
24
use Nails\Components;
25
use Nails\Environment;
26
use Nails\Factory;
27
28
// --------------------------------------------------------------------------
29
30
/**
31
 * Allow the app to add functionality, if needed
32
 */
33
if (class_exists('\App\Api\Controller\BaseRouter')) {
34
    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...
35
    {
36
    }
37
} else {
38
    abstract class BaseMiddle extends \Nails\Common\Controller\Base
39
    {
40
    }
41
}
42
43
// --------------------------------------------------------------------------
44
45
/**
46
 * Class ApiRouter
47
 */
48
class ApiRouter extends BaseMiddle
49
{
50
    const DEFAULT_FORMAT                   = \Nails\Api\Api\Output\Json::SLUG;
51
    const REQUEST_METHOD_GET               = HttpRequest\Get::HTTP_METHOD;
52
    const REQUEST_METHOD_PUT               = HttpRequest\Put::HTTP_METHOD;
53
    const REQUEST_METHOD_POST              = HttpRequest\Post::HTTP_METHOD;
54
    const REQUEST_METHOD_DELETE            = HttpRequest\Delete::HTTP_METHOD;
55
    const REQUEST_METHOD_OPTIONS           = HttpRequest\Options::HTTP_METHOD;
56
    const ACCESS_TOKEN_HEADER              = 'X-Access-Token';
57
    const ACCESS_TOKEN_POST_PARAM          = 'accessToken';
58
    const ACCESS_TOKEN_GET_PARAM           = 'accessToken';
59
    const ACCESS_CONTROL_ALLOW_ORIGIN      = '*';
60
    const ACCESS_CONTROL_ALLOW_CREDENTIALS = 'true';
61
    const ACCESS_CONTROL_MAX_AGE           = 86400;
62
    const ACCESS_CONTROL_ALLOW_HEADERS     = ['*'];
63
    const ACCESS_CONTROL_ALLOW_METHODS     = [
64
        self::REQUEST_METHOD_GET,
65
        self::REQUEST_METHOD_PUT,
66
        self::REQUEST_METHOD_POST,
67
        self::REQUEST_METHOD_DELETE,
68
        self::REQUEST_METHOD_OPTIONS,
69
    ];
70
    const OUTPUT_FORMAT_PATTERN            = '/\.([a-z]*)$/';
71
72
    // --------------------------------------------------------------------------
73
74
    /** @var array */
75
    protected static $aOutputValidFormats;
76
77
    // --------------------------------------------------------------------------
78
79
    /** @var string */
80
    private $sRequestMethod;
81
82
    /** @var string */
83
    private $sModuleName;
84
85
    /** @var string */
86
    private $sClassName;
87
88
    /** @var string */
89
    private $sMethod;
90
91
    /** @var string */
92
    private $sOutputFormat;
93
94
    /** @var bool */
95
    private $bOutputSendHeader = true;
96
97
    /** @var Logger */
98
    private $oLogger;
99
100
    /** @var string */
101
    private $sAccessToken;
102
103
    // --------------------------------------------------------------------------
104
105
    /**
106
     * ApiRouter constructor.
107
     *
108
     * @throws \Nails\Common\Exception\FactoryException
109
     */
110
    public function __construct()
111
    {
112
        parent::__construct();
113
114
        // --------------------------------------------------------------------------
115
116
        //  Work out the request method
117
        /** @var Input $oInput */
118
        $oInput               = Factory::service('Input');
119
        $this->sRequestMethod = $oInput->server('REQUEST_METHOD');
120
        $this->sRequestMethod = $this->sRequestMethod ? $this->sRequestMethod : static::REQUEST_METHOD_GET;
121
122
        /**
123
         * In order to work out the next few parts we'll analyse the URI string manually.
124
         * We're doing this because of the optional return type at the end of the string;
125
         * it's easier to regex that quickly, remove it, then split up the segments.
126
         */
127
128
        //  Look for valid output formats
129
        $aComponents = Components::available();
130
131
        //  Shift the app onto the end so it overrides any module supplied formats
132
        $oApp = array_shift($aComponents);
133
        array_push($aComponents, $oApp);
134
135
        foreach ($aComponents as $oComponent) {
136
137
            $oClasses = $oComponent
138
                ->findClasses('Api\Output')
139
                ->whichImplement(\Nails\Api\Interfaces\Output::class);
140
141
            foreach ($oClasses as $sClass) {
142
                static::$aOutputValidFormats[strtoupper($sClass::getSlug())] = $sClass;
143
            }
144
        }
145
146
        $this->sOutputFormat = static::detectOutputFormat();
147
        $sUri                = preg_replace(static::OUTPUT_FORMAT_PATTERN, '', uri_string());
148
149
        //  Remove the module prefix (i.e "api/") then explode into segments
150
        //  Using regex as some systems will report a leading slash (e.g CLI)
151
        $sUri = preg_replace('#/?api/#', '', $sUri);
152
        $aUri = explode('/', $sUri);
153
154
        //  Work out the sModuleName, sClassName and method
155
        $this->sModuleName = getFromArray(0, $aUri, null);
156
        $this->sClassName  = ucfirst(getFromArray(1, $aUri, $this->sModuleName));
157
        $this->sMethod     = getFromArray(2, $aUri, 'index');
158
159
        //  Configure logging
160
        /** @var \Nails\Common\Resource\DateTime $oNow */
161
        $oNow = Factory::factory('DateTime');
162
        /** @var Logger oLogger */
163
        $this->oLogger = Factory::factory('Logger');
164
        $this->oLogger->setFile('api-' . $oNow->format('Y-m-d') . '.php');
165
    }
166
167
    // --------------------------------------------------------------------------
168
169
    /**
170
     * Route the call to the correct place
171
     */
172
    public function index()
173
    {
174
        //  Handle OPTIONS CORS pre-flight requests
175
        if ($this->sRequestMethod === static::REQUEST_METHOD_OPTIONS) {
176
177
            $this
178
                ->setCorsHeaders()
179
                ->setCorsStatusHeader();
180
            return;
181
182
        } else {
183
184
            try {
185
                /**
186
                 * If an access token has been passed then verify it
187
                 *
188
                 * Passing the token via the header is preferred, but fallback to the GET
189
                 * and POST arrays.
190
                 */
191
192
                /** @var Input $oInput */
193
                $oInput = Factory::service('Input');
194
                /** @var HttpCodes $oHttpCodes */
195
                $oHttpCodes = Factory::service('HttpCodes');
196
                /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
197
                $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
198
199
                $sAccessToken = $oInput->header(static::ACCESS_TOKEN_HEADER);
200
201
                if (!$sAccessToken) {
202
                    $sAccessToken = $oInput->post(static::ACCESS_TOKEN_POST_PARAM);
203
                }
204
205
                if (!$sAccessToken) {
206
                    $sAccessToken = $oInput->get(static::ACCESS_TOKEN_GET_PARAM);
207
                }
208
209
                if ($sAccessToken) {
210
211
                    $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...
212
                    $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

212
                    $oAccessToken       = $oUserAccessTokenModel->getByValidToken(/** @scrutinizer ignore-type */ $sAccessToken);
Loading history...
213
214
                    if ($oAccessToken) {
0 ignored issues
show
introduced by Pablo de la Peña
$oAccessToken is of type Nails\Common\Resource, thus it always evaluated to true.
Loading history...
215
                        /** @var Auth\Model\User $oUserModel */
216
                        $oUserModel = Factory::model('User', Auth\Constants::MODULE_SLUG);
217
                        $oUserModel->setLoginData($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...
218
                    } else {
219
                        throw new ApiException(
220
                            'Invalid access token',
221
                            $oHttpCodes::STATUS_UNAUTHORIZED
222
                        );
223
                    }
224
                }
225
226
                // --------------------------------------------------------------------------
227
228
                if (!$this->outputSetFormat($this->sOutputFormat)) {
229
                    throw new ApiException(
230
                        '"' . $this->sOutputFormat . '" is not a valid format.',
231
                        $oHttpCodes::STATUS_BAD_REQUEST
232
                    );
233
                }
234
235
                // --------------------------------------------------------------------------
236
237
                //  Register API modules
238
                $aNamespaces = [
239
                    'app' => (object) [
240
                        'namespace' => 'App\\',
241
                    ],
242
                ];
243
                foreach (Components::modules() as $oModule) {
244
                    if (!empty($oModule->data->{Constants::MODULE_SLUG}->namespace)) {
245
                        $sNamespace = $oModule->data->{Constants::MODULE_SLUG}->namespace;
246
                        if (array_key_exists($sNamespace, $aNamespaces)) {
247
                            throw new NailsException(
248
                                sprintf(
249
                                    'Conflicting API namespace "%s" in use by "%s" and "%s"',
250
                                    $sNamespace,
251
                                    $oModule->slug,
252
                                    $aNamespaces[$sNamespace]->slug
253
                                )
254
                            );
255
                        }
256
                        $aNamespaces[$sNamespace] = $oModule;
257
                    }
258
                }
259
260
                $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
261
                $s404Error  = sprintf(
262
                    '"%s: %s/%s/%s" is not a valid API route.',
263
                    $this->getRequestMethod(),
264
                    strtolower($this->sModuleName),
265
                    strtolower($this->sClassName),
266
                    strtolower($this->sMethod)
267
                );
268
269
                if (!array_key_exists($this->sModuleName, $aNamespaces)) {
270
                    throw new ApiException($s404Error, $i404Status);
271
                }
272
273
                $oNamespace          = $aNamespaces[$this->sModuleName];
274
                $sOriginalController = $this->sClassName;
275
276
                //  Do we need to remap the controller?
277
                if (!empty($oNamespace->data->{Constants::MODULE_SLUG}->{'controller-map'})) {
278
279
                    $aMap             = (array) $oNamespace->data->{Constants::MODULE_SLUG}->{'controller-map'};
280
                    $this->sClassName = getFromArray($this->sClassName, $aMap, $this->sClassName);
281
282
                    //  This prevents users from accessing the "correct" controller, so we only have one valid route
283
                    $sRemapped = array_search($sOriginalController, $aMap);
284
                    if ($sRemapped !== false) {
285
                        $this->sClassName = $sRemapped;
286
                    }
287
                }
288
289
                $sController = $oNamespace->namespace . 'Api\\Controller\\' . $this->sClassName;
290
291
                if (!class_exists($sController)) {
292
                    throw new ApiException($s404Error, $i404Status);
293
                }
294
295
                $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
296
                if ($mAuth !== true) {
297
298
                    if (is_array($mAuth)) {
299
                        $sError  = 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...
300
                        $iStatus = (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED);
301
                    } else {
302
                        $sError  = 'You must be logged in to access this resource';
303
                        $iStatus = $oHttpCodes::STATUS_UNAUTHORIZED;
304
                    }
305
306
                    throw new ApiException($sError, $iStatus);
307
                }
308
309
                if (!empty($sController::REQUIRE_SCOPE)) {
310
                    if (!$oUserAccessTokenModel->hasScope($oAccessToken, $sController::REQUIRE_SCOPE)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by Pablo de la Peña
The variable $oAccessToken does not seem to be defined for all execution paths leading up to this point.
Loading history...
311
                        throw new ApiException(
312
                            'Access token with "' . $sController::REQUIRE_SCOPE . '" scope is required.',
313
                            $oHttpCodes::STATUS_UNAUTHORIZED
314
                        );
315
                    }
316
                }
317
318
                //  New instance of the controller
319
                $oInstance = new $sController($this);
320
321
                /**
322
                 * We need to look for the appropriate method; we'll look in the following order:
323
                 *
324
                 * - {sRequestMethod}Remap()
325
                 * - {sRequestMethod}{method}()
326
                 * - anyRemap()
327
                 * - any{method}()
328
                 *
329
                 * The second parameter is whether the method is a remap method or not.
330
                 */
331
                $bDidFindRoute = false;
332
                $aMethods      = [
333
                    [
334
                        strtolower($this->sRequestMethod) . 'Remap',
335
                        true,
336
                    ],
337
                    [
338
                        strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
339
                        false,
340
                    ],
341
                    [
342
                        'anyRemap',
343
                        true,
344
                    ],
345
                    [
346
                        'any' . ucfirst($this->sMethod),
347
                        false,
348
                    ],
349
                ];
350
351
                foreach ($aMethods as $aMethodName) {
352
353
                    $sMethod  = getFromArray(0, $aMethodName);
354
                    $bIsRemap = (bool) getFromArray(1, $aMethodName);
355
                    if (is_callable([$oInstance, $sMethod])) {
356
357
                        $bDidFindRoute = true;
358
359
                        /**
360
                         * If the method we're trying to call is a remap method, then the first
361
                         * param should be the name of the method being called
362
                         */
363
                        if ($bIsRemap) {
364
                            $oResponse = call_user_func_array([$oInstance, $sMethod], [$this->sMethod]);
365
                        } else {
366
                            $oResponse = call_user_func([$oInstance, $sMethod]);
367
                        }
368
                        break;
369
                    }
370
                }
371
372
                if (!$bDidFindRoute) {
373
                    throw new ApiException(
374
                        $s404Error,
375
                        $i404Status
376
                    );
377
                }
378
379
                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...
380
                    //  This is a misconfiguration error, which we want to bubble up to the error handler
381
                    throw new NailsException(
382
                        'Return object must be an instance of \Nails\Api\Factory\ApiResponse',
383
                        $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
384
                    );
385
                }
386
387
                $aOut = [
388
                    'status' => $oResponse->getCode(),
389
                    'body'   => $oResponse->getBody(),
390
                    'data'   => $oResponse->getData(),
391
                    'meta'   => $oResponse->getMeta(),
392
                ];
393
394
            } catch (ValidationException $e) {
395
396
                $aOut = [
397
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
0 ignored issues
show
Comprehensibility Best Practice introduced by Pablo de la Peña
The variable $oHttpCodes does not seem to be defined for all execution paths leading up to this point.
Loading history...
398
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
399
                    'details' => $e->getData() ?: [],
400
                ];
401
                if (isSuperuser()) {
402
                    $aOut['exception'] = (object) array_filter([
403
                        'type' => get_class($e),
404
                        'file' => $e->getFile(),
405
                        'line' => $e->getLine(),
406
                    ]);
407
                }
408
409
                $this->writeLog($aOut);
410
411
            } catch (ApiException $e) {
412
413
                $aOut = [
414
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
415
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
416
                    'details' => $e->getData() ?: [],
417
                ];
418
                if (isSuperuser()) {
419
                    $aOut['exception'] = (object) array_filter([
420
                        'type' => get_class($e),
421
                        'file' => $e->getFile(),
422
                        'line' => $e->getLine(),
423
                    ]);
424
                }
425
426
                $this->writeLog($aOut);
427
428
            } catch (\Exception $e) {
429
                /**
430
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
431
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
432
                 * developer the error quickly and with as much info as possible.
433
                 */
434
                if (Environment::is(Environment::ENV_PROD)) {
435
                    throw $e;
436
                } else {
437
                    $aOut = [
438
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
439
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
440
                        'exception' => (object) array_filter([
441
                            'type' => get_class($e),
442
                            'file' => $e->getFile(),
443
                            'line' => $e->getLine(),
444
                        ]),
445
                    ];
446
447
                    $this->writeLog($aOut);
448
                }
449
            }
450
451
            $this->output($aOut);
452
        }
453
    }
454
455
    // --------------------------------------------------------------------------
456
457
    /**
458
     * Sends $aOut to the browser in the desired format
459
     *
460
     * @param array $aOut The data to output to the browser
461
     */
462
    protected function output($aOut = [])
463
    {
464
        /** @var Input $oInput */
465
        $oInput = Factory::service('Input');
466
        /** @var Output $oOutput */
467
        $oOutput = Factory::service('Output');
468
        /** @var HttpCodes $oHttpCodes */
469
        $oHttpCodes = Factory::service('HttpCodes');
470
471
        //  Set cache headers
472
        $oOutput
473
            ->setHeader('Cache-Control: no-store, no-cache, must-revalidate')
474
            ->setHeader('Expires: Mon, 26 Jul 1997 05:00:00 GMT')
475
            ->setHeader('Pragma: no-cache');
476
477
        //  Set access control headers
478
        $this->setCorsHeaders();
479
480
        // --------------------------------------------------------------------------
481
482
        //  Send the correct status header, default to 200 OK
483
        if ($this->bOutputSendHeader) {
484
            $sProtocol   = $oInput->server('SERVER_PROTOCOL');
485
            $iHttpCode   = getFromArray('status', $aOut, $oHttpCodes::STATUS_OK);
486
            $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...
487
            $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

487
            $oOutput->setHeader(/** @scrutinizer ignore-type */ $sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
Loading history...
488
        }
489
490
        // --------------------------------------------------------------------------
491
492
        //  Output content
493
        $sOutputClass = static::$aOutputValidFormats[$this->sOutputFormat];
494
        $oOutput->setContentType($sOutputClass::getContentType());
495
496
        if (array_key_exists('body', $aOut) && $aOut['body'] !== null) {
497
            $sOut = $aOut['body'];
498
        } else {
499
            unset($aOut['body']);
500
            $sOut = $sOutputClass::render($aOut);
501
        }
502
503
        $oOutput->setOutput($sOut);
504
    }
505
506
    // --------------------------------------------------------------------------
507
508
    /**
509
     * Sets CORS headers
510
     *
511
     * @return $this
512
     * @throws \Nails\Common\Exception\FactoryException
513
     */
514
    protected function setCorsHeaders(): self
515
    {
516
        /** @var Output $oOutput */
517
        $oOutput = Factory::service('Output');
518
        $oOutput
519
            ->setHeader('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN)
520
            ->setHeader('Access-Control-Allow-Credentials: ' . static::ACCESS_CONTROL_ALLOW_CREDENTIALS)
521
            ->setHeader('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS))
522
            ->setHeader('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS))
523
            ->setHeader('Access-Control-Max-Age: ' . static::ACCESS_CONTROL_MAX_AGE);
524
525
        return $this;
526
    }
527
528
    // --------------------------------------------------------------------------
529
530
    /**
531
     * Set the CORS status header
532
     *
533
     * @return $this
534
     * @throws \Nails\Common\Exception\FactoryException
535
     */
536
    protected function setCorsStatusHeader(): self
537
    {
538
        /** @var Output $oOutput */
539
        $oOutput = Factory::service('Output');
540
        $oOutput->setStatusHeader(HttpCodes::STATUS_NO_CONTENT);
541
        return $this;
542
    }
543
544
    // --------------------------------------------------------------------------
545
546
    /**
547
     * Sets the output format
548
     *
549
     * @param string $sFormat The format to use
550
     *
551
     * @return bool
552
     */
553
    public function outputSetFormat($sFormat): bool
554
    {
555
        if (static::isValidFormat($sFormat)) {
556
            $this->sOutputFormat = strtoupper($sFormat);
557
            return true;
558
        }
559
560
        return false;
561
    }
562
563
    // --------------------------------------------------------------------------
564
565
    /**
566
     * Returns the putput format
567
     *
568
     * @return string
569
     */
570
    public function outputGetFormat(): string
571
    {
572
        return $this->sOutputFormat;
573
    }
574
575
    // --------------------------------------------------------------------------
576
577
    /**
578
     * Detects the putput frmat from the URI
579
     *
580
     * @return string
581
     */
582
    public static function detectOutputFormat(): string
583
    {
584
        preg_match(static::OUTPUT_FORMAT_PATTERN, uri_string(), $aMatches);
585
        $sFormat = !empty($aMatches[1]) ? strtoupper($aMatches[1]) : null;
586
587
        return static::isValidFormat($sFormat)
588
            ? $sFormat
589
            : static::DEFAULT_FORMAT;
590
    }
591
592
    // --------------------------------------------------------------------------
593
594
    /**
595
     * Sets whether the status header should be sent or not
596
     *
597
     * @param bool $sendHeader Whether the header should be sent or not
598
     */
599
    public function outputSendHeader($sendHeader): bool
600
    {
601
        $this->bOutputSendHeader = !empty($sendHeader);
602
    }
603
604
    // --------------------------------------------------------------------------
605
606
    /**
607
     * Determines whether the format is valid
608
     *
609
     * @param string $sFormat The format to check
610
     *
611
     * @return bool
612
     */
613
    private static function isValidFormat(?string $sFormat): bool
614
    {
615
        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

615
        return in_array(strtoupper(/** @scrutinizer ignore-type */ $sFormat), array_keys(static::$aOutputValidFormats));
Loading history...
616
    }
617
618
    // --------------------------------------------------------------------------
619
620
    /**
621
     * Write a line to the API log
622
     *
623
     * @param string $sLine The line to write
624
     */
625
    public function writeLog($sLine)
626
    {
627
        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...
628
            $sLine = print_r($sLine, true);
629
        }
630
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
631
        $this->oLogger->line($sLine);
632
    }
633
634
    // --------------------------------------------------------------------------
635
636
    /**
637
     * Returns the current request method
638
     *
639
     * @return string
640
     */
641
    public function getRequestMethod(): string
642
    {
643
        return $this->sRequestMethod;
644
    }
645
646
    // --------------------------------------------------------------------------
647
648
    /**
649
     * Confirms whether the request is of the supplied type
650
     *
651
     * @param string $sMethod The request method to check
652
     *
653
     * @return bool
654
     */
655
    protected function isRequestMethod(string $sMethod): bool
656
    {
657
        return $this->getRequestMethod() === $sMethod;
658
    }
659
660
    // --------------------------------------------------------------------------
661
662
    /**
663
     * Determines whether the request is a GET request
664
     *
665
     * @return bool
666
     */
667
    public function isGetRequest(): bool
668
    {
669
        return $this->isRequestMethod(static::REQUEST_METHOD_GET);
670
    }
671
672
    // --------------------------------------------------------------------------
673
674
    /**
675
     * Determines whether the request is a PUT request
676
     *
677
     * @return bool
678
     */
679
    public function isPutRequest(): bool
680
    {
681
        return $this->isRequestMethod(static::REQUEST_METHOD_PUT);
682
    }
683
684
    // --------------------------------------------------------------------------
685
686
    /**
687
     * Determines whether the request is a POST request
688
     *
689
     * @return bool
690
     */
691
    public function isPostRequest(): bool
692
    {
693
        return $this->isRequestMethod(static::REQUEST_METHOD_POST);
694
    }
695
696
    // --------------------------------------------------------------------------
697
698
    /**
699
     * Determines whether the request is a DELETE request
700
     *
701
     * @return bool
702
     */
703
    public function isDeleteRequest(): bool
704
    {
705
        return $this->isRequestMethod(static::REQUEST_METHOD_DELETE);
706
    }
707
708
    // --------------------------------------------------------------------------
709
710
    /**
711
     * Returns the current Access Token
712
     *
713
     * @return string|null
714
     */
715
    public function getAccessToken(): ?string
716
    {
717
        return $this->sAccessToken;
718
    }
719
}
720