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

api/controllers/ApiRouter.php (2 issues)

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

Loading history...
383
384
            if ($this->oAccessToken) {
385
                /** @var Auth\Model\User $oUserModel */
386
                $oUserModel = Factory::model('User', Auth\Constants::MODULE_SLUG);
387
                $oUserModel->setLoginData($this->oAccessToken->user_id, false);
388
389
            } else {
390
                throw new ApiException(
391
                    'Invalid access token',
392
                    $oHttpCodes::STATUS_UNAUTHORIZED
393
                );
394
            }
395
        }
396
397
        return $this;
398
    }
399
400
    // --------------------------------------------------------------------------
401
402
    /**
403
     * Discovers API controllers
404
     *
405
     * @return array
406
     * @throws ApiException
407
     * @throws NailsException
408
     */
409
    protected function discoverApiControllers(): array
410
    {
411
        $aControllerMap = [];
412
        foreach (Components::available() as $oModule) {
413
414
            $aClasses = $oModule
415
                ->findClasses('Api\\Controller')
416
                ->whichExtend(\Nails\Api\Controller\Base::class);
417
418
            $sNamespace = $oModule->slug === Components::$sAppSlug
419
                ? Components::$sAppSlug
420
                : ($oModule->data->{Constants::MODULE_SLUG}->namespace ?? null);
421
422
            if (empty($sNamespace) && count($aClasses)) {
423
                throw new ApiException(
424
                    sprintf(
425
                        'Found API controllers for module %s, but module does not define an API namespace',
426
                        $oModule->slug
427
                    )
428
                );
429
430
            } elseif (!count($aClasses)) {
431
                continue;
432
            }
433
434
            if (array_key_exists($sNamespace, $aControllerMap)) {
435
                throw new NailsException(
436
                    sprintf(
437
                        'Conflicting API namespace "%s" in use by "%s" and "%s"',
438
                        $sNamespace,
439
                        $oModule->slug,
440
                        $aControllerMap[$sNamespace]->module->slug
441
                    )
442
                );
443
            }
444
445
            $aControllerMap[$sNamespace] = (object) [
446
                'module'      => $oModule,
447
                'controllers' => [],
448
            ];
449
450
            foreach ($aClasses as $sClass) {
451
                $aControllerMap[$sNamespace]->controllers[strtolower($sClass)] = $sClass;
452
            }
453
        }
454
455
        return $aControllerMap;
456
    }
457
458
    // --------------------------------------------------------------------------
459
460
    /**
461
     * Normalises the controller class name, taking into account any defined remapping
462
     *
463
     * @param stdClass $oModule The module as created by discoverApiControllers
464
     *
465
     * @return string
466
     * @throws ApiException
467
     * @throws FactoryException
468
     */
469
    protected function normaliseControllerClass(stdClass $oModule): string
470
    {
471
        $aRemap = (array) ($oModule->module->data->{Constants::MODULE_SLUG}->{'controller-map'} ?? []);
472
        if (!empty($aRemap)) {
473
474
            $sOriginalController = $this->sClassName;
475
            $this->sClassName    = getFromArray($this->sClassName, $aRemap, $this->sClassName);
476
477
            //  This prevents users from accessing the "correct" controller, so we only have one valid route
478
            $sRemapped = array_search($sOriginalController, $aRemap);
479
            if ($sRemapped !== false) {
480
                $this->invalidApiRoute();
481
            }
482
        }
483
484
        $sController = $oModule->module->namespace . 'Api\\Controller\\' . $this->sClassName;
485
        $sController = $oModule->controllers[strtolower($sController)] ?? $sController;
486
487
        return $sController;
488
    }
489
490
    // --------------------------------------------------------------------------
491
492
    /**
493
     * Checks the controllers auth requirements
494
     *
495
     * @param string $sController The Controller class name
496
     *
497
     * @throws ApiException
498
     * @throws FactoryException
499
     */
500
    protected function checkControllerAuth(string $sController): void
501
    {
502
        /** @var HttpCodes $oHttpCodes */
503
        $oHttpCodes = Factory::service('HttpCodes');
504
        /** @var Auth\Model\User\AccessToken $oUserAccessTokenModel */
505
        $oUserAccessTokenModel = Factory::model('UserAccessToken', Auth\Constants::MODULE_SLUG);
506
507
        $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
508
        if ($mAuth !== true) {
509
510
            if (is_array($mAuth)) {
511
                throw new ApiException(
512
                    getFromArray('error', $mAuth, $oHttpCodes::getByCode($oHttpCodes::STATUS_UNAUTHORIZED)),
513
                    (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED)
514
                );
515
516
            } else {
517
                throw new ApiException(
518
                    'You must be logged in to access this resource',
519
                    $oHttpCodes::STATUS_UNAUTHORIZED
520
                );
521
            }
522
        }
523
524
        if (!empty($sController::REQUIRE_SCOPE)) {
525
            if (!$this->oAccessToken->hasScope($sController::REQUIRE_SCOPE)) {
526
                throw new ApiException(
527
                    sprintf(
528
                        'Access token with "%s" scope is required.',
529
                        $sController::REQUIRE_SCOPE
530
                    ),
531
                    $oHttpCodes::STATUS_UNAUTHORIZED
532
                );
533
            }
534
        }
535
    }
536
537
    // --------------------------------------------------------------------------
538
539
    /**
540
     * Calls the appropriate controller method
541
     *
542
     * @param \Nails\Api\Controller\Base $oController The controller instance
543
     *
544
     * @return ApiResponse
545
     * @throws ApiException
546
     * @throws FactoryException
547
     */
548
    protected function callControllerMethod(\Nails\Api\Controller\Base $oController): ApiResponse
549
    {
550
        /**
551
         * We need to look for the appropriate method; we'll look in the following order:
552
         *
553
         * - {sRequestMethod}Remap()
554
         * - {sRequestMethod}{method}()
555
         * - anyRemap()
556
         * - any{method}()
557
         *
558
         * The second parameter is whether the method is a remap method or not.
559
         */
560
        $aMethods = [
561
            [
562
                'method'   => strtolower($this->sRequestMethod) . 'Remap',
563
                'is_remap' => true,
564
            ],
565
            [
566
                'method'   => strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
567
                'is_remap' => false,
568
            ],
569
            [
570
                'method'   => 'anyRemap',
571
                'is_remap' => true,
572
            ],
573
            [
574
                'method'   => 'any' . ucfirst($this->sMethod),
575
                'is_remap' => false,
576
            ],
577
        ];
578
579
        foreach ($aMethods as $aMethod) {
580
            if (is_callable([$oController, $aMethod['method']])) {
581
                /**
582
                 * If the method we're trying to call is a remap method, then the first
583
                 * param should be the name of the method being called
584
                 */
585
                if ($aMethod['is_remap']) {
586
                    return call_user_func_array(
587
                        [
588
                            $oController,
589
                            $aMethod['method'],
590
                        ],
591
                        [$this->sMethod]);
592
593
                } else {
594
                    return call_user_func([
595
                        $oController,
596
                        $aMethod['method'],
597
                    ]);
598
                }
599
            }
600
        }
601
602
        $this->invalidApiRoute();
603
    }
604
605
    // --------------------------------------------------------------------------
606
607
    /**
608
     * Throws an invalid API route 404 exception
609
     *
610
     * @throws ApiException
611
     * @throws FactoryException
612
     */
613
    protected function invalidApiRoute(): void
614
    {
615
        /** @var HttpCodes $oHttpCodes */
616
        $oHttpCodes = Factory::service('HttpCodes');
617
618
        $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
619
        $s404Error  = sprintf(
620
            '"%s: %s/%s/%s" is not a valid API route.',
621
            $this->getRequestMethod(),
622
            strtolower($this->sModuleName),
623
            strtolower($this->sClassName),
624
            strtolower($this->sMethod)
625
        );
626
627
        throw new ApiException($s404Error, $i404Status);
628
    }
629
630
    // --------------------------------------------------------------------------
631
632
    /**
633
     * Throws an invalid API format 400 exception
634
     *
635
     * @throws ApiException
636
     */
637
    protected function invalidApiFormat(): void
638
    {
639
        /** @var HttpCodes $oHttpCodes */
640
        $oHttpCodes = Factory::service('HttpCodes');
641
642
        throw new ApiException(
643
            sprintf(
644
                '"%s" is not a valid format.',
645
                $this->sOutputFormat
646
            ),
647
            $oHttpCodes::STATUS_BAD_REQUEST
648
        );
649
    }
650
651
    // --------------------------------------------------------------------------
652
653
    /**
654
     * Sends $aOut to the browser in the desired format
655
     *
656
     * @param array $aOut The data to output to the browser
657
     */
658
    protected function output($aOut = [])
659
    {
660
        /** @var Input $oInput */
661
        $oInput = Factory::service('Input');
662
        /** @var Output $oOutput */
663
        $oOutput = Factory::service('Output');
664
        /** @var HttpCodes $oHttpCodes */
665
        $oHttpCodes = Factory::service('HttpCodes');
666
667
        //  Set cache headers
668
        $oOutput
669
            ->setHeader('Cache-Control: no-store, no-cache, must-revalidate')
670
            ->setHeader('Expires: Mon, 26 Jul 1997 05:00:00 GMT')
671
            ->setHeader('Pragma: no-cache');
672
673
        //  Set access control headers
674
        $this->setCorsHeaders();
675
676
        // --------------------------------------------------------------------------
677
678
        //  Send the correct status header, default to 200 OK
679
        if ($this->bOutputSendHeader) {
680
            $sProtocol   = $oInput->server('SERVER_PROTOCOL');
681
            $iHttpCode   = getFromArray('status', $aOut, $oHttpCodes::STATUS_OK);
682
            $sHttpString = $oHttpCodes::getByCode($iHttpCode);
683
            $oOutput->setHeader($sProtocol . ' ' . $iHttpCode . ' ' . $sHttpString);
684
        }
685
686
        // --------------------------------------------------------------------------
687
688
        //  Output content
689
        $sOutputClass = static::$aOutputValidFormats[$this->sOutputFormat];
690
        $oOutput->setContentType($sOutputClass::getContentType());
691
692
        if (array_key_exists('body', $aOut) && $aOut['body'] !== null) {
693
            $sOut = $aOut['body'];
694
        } else {
695
            unset($aOut['body']);
696
            $sOut = $sOutputClass::render($aOut);
697
        }
698
699
        $oOutput->setOutput($sOut);
700
    }
701
702
    // --------------------------------------------------------------------------
703
704
    /**
705
     * Sets CORS headers
706
     *
707
     * @return $this
708
     * @throws FactoryException
709
     */
710
    protected function setCorsHeaders(): self
711
    {
712
        /** @var Output $oOutput */
713
        $oOutput = Factory::service('Output');
714
        $oOutput
715
            ->setHeader('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN)
716
            ->setHeader('Access-Control-Allow-Credentials: ' . static::ACCESS_CONTROL_ALLOW_CREDENTIALS)
717
            ->setHeader('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS))
718
            ->setHeader('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS))
719
            ->setHeader('Access-Control-Max-Age: ' . static::ACCESS_CONTROL_MAX_AGE);
720
721
        return $this;
722
    }
723
724
    // --------------------------------------------------------------------------
725
726
    /**
727
     * Set the CORS status header
728
     *
729
     * @return $this
730
     * @throws FactoryException
731
     */
732
    protected function setCorsStatusHeader(): self
733
    {
734
        /** @var Output $oOutput */
735
        $oOutput = Factory::service('Output');
736
        $oOutput->setStatusHeader(HttpCodes::STATUS_NO_CONTENT);
737
        return $this;
738
    }
739
740
    // --------------------------------------------------------------------------
741
742
    /**
743
     * Sets the output format
744
     *
745
     * @param string $sFormat The format to use
746
     *
747
     * @return bool
748
     */
749
    public function outputSetFormat($sFormat): bool
750
    {
751
        if (static::isValidFormat($sFormat)) {
752
            $this->sOutputFormat = strtoupper($sFormat);
753
            return true;
754
        }
755
756
        return false;
757
    }
758
759
    // --------------------------------------------------------------------------
760
761
    /**
762
     * Returns the putput format
763
     *
764
     * @return string
765
     */
766
    public function outputGetFormat(): string
767
    {
768
        return $this->sOutputFormat;
769
    }
770
771
    // --------------------------------------------------------------------------
772
773
    /**
774
     * Sets whether the status header should be sent or not
775
     *
776
     * @param bool $sendHeader Whether the header should be sent or not
777
     */
778
    public function outputSendHeader($bSendHeader): bool
779
    {
780
        $this->bOutputSendHeader = !empty($bSendHeader);
781
    }
782
783
    // --------------------------------------------------------------------------
784
785
    /**
786
     * Determines whether the format is valid
787
     *
788
     * @param string $sFormat The format to check
789
     *
790
     * @return bool
791
     */
792
    private static function isValidFormat(?string $sFormat): bool
793
    {
794
        return in_array(strtoupper($sFormat), array_keys(static::$aOutputValidFormats));
795
    }
796
797
    // --------------------------------------------------------------------------
798
799
    /**
800
     * Write a line to the API log
801
     *
802
     * @param string $sLine The line to write
803
     */
804
    public function writeLog($sLine)
805
    {
806
        if (!is_string($sLine)) {
807
            $sLine = print_r($sLine, true);
808
        }
809
        $sLine = ' [' . $this->sModuleName . '->' . $this->sMethod . '] ' . $sLine;
810
        $this->oLogger->line($sLine);
811
    }
812
813
    // --------------------------------------------------------------------------
814
815
    /**
816
     * Returns the current request method
817
     *
818
     * @return string
819
     */
820
    public function getRequestMethod(): string
821
    {
822
        return $this->sRequestMethod;
823
    }
824
825
    // --------------------------------------------------------------------------
826
827
    /**
828
     * Confirms whether the request is of the supplied type
829
     *
830
     * @param string $sMethod The request method to check
831
     *
832
     * @return bool
833
     */
834
    protected function isRequestMethod(string $sMethod): bool
835
    {
836
        return $this->getRequestMethod() === $sMethod;
837
    }
838
839
    // --------------------------------------------------------------------------
840
841
    /**
842
     * Determines whether the request is a GET request
843
     *
844
     * @return bool
845
     */
846
    public function isGetRequest(): bool
847
    {
848
        return $this->isRequestMethod(static::REQUEST_METHOD_GET);
849
    }
850
851
    // --------------------------------------------------------------------------
852
853
    /**
854
     * Determines whether the request is a PUT request
855
     *
856
     * @return bool
857
     */
858
    public function isPutRequest(): bool
859
    {
860
        return $this->isRequestMethod(static::REQUEST_METHOD_PUT);
861
    }
862
863
    // --------------------------------------------------------------------------
864
865
    /**
866
     * Determines whether the request is a POST request
867
     *
868
     * @return bool
869
     */
870
    public function isPostRequest(): bool
871
    {
872
        return $this->isRequestMethod(static::REQUEST_METHOD_POST);
873
    }
874
875
    // --------------------------------------------------------------------------
876
877
    /**
878
     * Determines whether the request is a DELETE request
879
     *
880
     * @return bool
881
     */
882
    public function isDeleteRequest(): bool
883
    {
884
        return $this->isRequestMethod(static::REQUEST_METHOD_DELETE);
885
    }
886
887
    // --------------------------------------------------------------------------
888
889
    /**
890
     * Returns the current Access Token
891
     *
892
     * @return string|null
893
     */
894
    public function getAccessToken(): ?string
895
    {
896
        return $this->sAccessToken;
897
    }
898
}
899