api/controllers/ApiRouter.php
public function index()
    {
        //  Handle OPTIONS CORS pre-flight requests
        if ($this->sRequestMethod === 'OPTIONS') {

            $oOutput = Factory::service('Output');
            $oOutput->set_header('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN);
            $oOutput->set_header('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS));
            $oOutput->set_header('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS));
            return;

        } else {

            try {
                /**
                 * If an access token has been passed then verify it
                 *
                 * Passing the token via the header is preferred, but fallback to the GET
                 * and POST arrays.
                 */

                $oInput                = Factory::service('Input');
                $oHttpCodes            = Factory::service('HttpCodes');
                $oUserAccessTokenModel = Factory::model('UserAccessToken', 'nails/module-auth');
                $sAccessToken          = $oInput->header('X-Access-Token');

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->post('accessToken');
                }

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->get('accessToken');
                }

                if ($sAccessToken) {
                    $oAccessToken = $oUserAccessTokenModel->getByValidToken($sAccessToken);
                    if ($oAccessToken) {
                        $oUserModel = Factory::model('User', 'nails/module-auth');
                        $oUserModel->setLoginData($oAccessToken->user_id, false);
                    } else {
                        throw new ApiException(
                            'Invalid access token',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                // --------------------------------------------------------------------------

                if (!$this->outputSetFormat($this->sOutputFormat)) {
                    throw new ApiException(
                        '"' . $this->sOutputFormat . '" is not a valid format.',
                        $oHttpCodes::STATUS_BAD_REQUEST
                    );
                }

                // --------------------------------------------------------------------------

                //  Register API modules
                $aNamespaces = [
                    'app' => (object) [
                        'namespace' => 'App\\',
                    ],
                ];
                foreach (Components::modules() as $oModule) {
                    if (!empty($oModule->data->{'nails/module-api'}->namespace)) {
                        $sNamespace = $oModule->data->{'nails/module-api'}->namespace;
                        if (array_key_exists($sNamespace, $aNamespaces)) {
                            throw new NailsException(
                                'Conflicting API namespace "' . $sNamespace . '" in use by ' .
                                '"' . $oModule->slug . '" and "' . $aNamespaces[$sNamespace]->slug . '"'
                            );
                        }
                        $aNamespaces[$sNamespace] = $oModule;
                    }
                }

                $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
                $s404Error  = '"' . strtolower($this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod) . '" is not a valid API route.';

                if (!array_key_exists($this->sModuleName, $aNamespaces)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $oNamespace          = $aNamespaces[$this->sModuleName];
                $sOriginalController = $this->sClassName;

                //  Do we need to remap the controller?
                if (!empty($oNamespace->data->{'nails/module-api'}->{'controller-map'})) {

                    $aMap             = (array) $oNamespace->data->{'nails/module-api'}->{'controller-map'};
                    $this->sClassName = getFromArray($this->sClassName, $aMap, $this->sClassName);

                    //  This prevents users from accessing the "correct" controller, so we only have one valid route
                    $sRemapped = array_search($sOriginalController, $aMap);
                    if ($sRemapped !== false) {
                        $this->sClassName = $sRemapped;
                    }
                }

                $sController = $oNamespace->namespace . 'Api\\Controller\\' . $this->sClassName;

                if (!class_exists($sController)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
                if ($mAuth !== true) {

                    if (is_array($mAuth)) {
                        $sError  = getFromArray('error', $mAuth, $oHttpCodes::getByCode($oHttpCodes::STATUS_UNAUTHORIZED));
                        $iStatus = (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED);
                    } else {
                        $sError  = 'You must be logged in to access this resource';
                        $iStatus = $oHttpCodes::STATUS_UNAUTHORIZED;
                    }

                    throw new ApiException($sError, $iStatus);
                }

                if (!empty($sController::REQUIRE_SCOPE)) {
                    if (!$oUserAccessTokenModel->hasScope($oAccessToken, $sController::REQUIRE_SCOPE)) {
                        throw new ApiException(
                            'Access token with "' . $sController::REQUIRE_SCOPE . '" scope is required.',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                //  New instance of the controller
                $oInstance = new $sController($this);

                /**
                 * We need to look for the appropriate method; we'll look in the following order:
                 *
                 * - {sRequestMethod}Remap()
                 * - {sRequestMethod}{method}()
                 * - anyRemap()
                 * - any{method}()
                 *
                 * The second parameter is whether the method is a remap method or not.
                 */
                $bDidFindRoute = false;
                $aMethods      = [
                    [
                        strtolower($this->sRequestMethod) . 'Remap',
                        true,
                    ],
                    [
                        strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
                        false,
                    ],
                    [
                        'anyRemap',
                        true,
                    ],
                    [
                        'any' . ucfirst($this->sMethod),
                        false,
                    ],
                ];

                foreach ($aMethods as $aMethodName) {

                    $sMethod  = getFromArray(0, $aMethodName);
                    $bIsRemap = (bool) getFromArray(1, $aMethodName);
                    if (is_callable([$oInstance, $sMethod])) {

                        $bDidFindRoute = true;

                        /**
                         * If the method we're trying to call is a remap method, then the first
                         * param should be the name of the method being called
                         */
                        if ($bIsRemap) {
                            $oResponse = call_user_func_array([$oInstance, $sMethod], [$this->sMethod]);
                        } else {
                            $oResponse = call_user_func([$oInstance, $sMethod]);
                        }
                        break;
                    }
                }

                if (!$bDidFindRoute) {
                    throw new ApiException(
                        '"' . $this->sRequestMethod . ': ' . $this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod . '" is not a valid API route.',
                        $oHttpCodes::STATUS_NOT_FOUND
                    );
                }

                if (!($oResponse instanceof ApiResponse)) {
                    //  This is a misconfiguration error, which we want to bubble up to the error handler
                    throw new NailsException(
                        'Return object must be an instance of \Nails\Api\Factory\ApiResponse',
                        $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
                    );
                }

                $aOut = [
                    'status' => $oHttpCodes::STATUS_OK,
                    'data'   => $oResponse->getData(),
                    'meta'   => $oResponse->getMeta(),
                ];

            } catch (ValidationException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (ApiException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (\Exception $e) {
                /**
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
                 * developer the error quickly and with as much info as possible.
                 */
                if (Environment::is(Environment::ENV_PROD)) {
                    throw $e;
                } else {
                    $aOut = [
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
                        'exception' => (object) array_filter([
                            'type' => get_class($e),
                            'file' => $e->getFile(),
                            'line' => $e->getLine(),
                        ]),
                    ];

                    $this->writeLog($aOut);
                }
            }

            $this->output($aOut);
        }
    }
public function index()
    {
        //  Handle OPTIONS CORS pre-flight requests
        if ($this->sRequestMethod === 'OPTIONS') {

            $oOutput = Factory::service('Output');
            $oOutput->set_header('Access-Control-Allow-Origin: ' . static::ACCESS_CONTROL_ALLOW_ORIGIN);
            $oOutput->set_header('Access-Control-Allow-Headers: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_HEADERS));
            $oOutput->set_header('Access-Control-Allow-Methods: ' . implode(', ', static::ACCESS_CONTROL_ALLOW_METHODS));
            return;

        } else {

            try {
                /**
                 * If an access token has been passed then verify it
                 *
                 * Passing the token via the header is preferred, but fallback to the GET
                 * and POST arrays.
                 */

                $oInput                = Factory::service('Input');
                $oHttpCodes            = Factory::service('HttpCodes');
                $oUserAccessTokenModel = Factory::model('UserAccessToken', 'nails/module-auth');
                $sAccessToken          = $oInput->header('X-Access-Token');

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->post('accessToken');
                }

                if (!$sAccessToken) {
                    $sAccessToken = $oInput->get('accessToken');
                }

                if ($sAccessToken) {
                    $oAccessToken = $oUserAccessTokenModel->getByValidToken($sAccessToken);
                    if ($oAccessToken) {
                        $oUserModel = Factory::model('User', 'nails/module-auth');
                        $oUserModel->setLoginData($oAccessToken->user_id, false);
                    } else {
                        throw new ApiException(
                            'Invalid access token',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                // --------------------------------------------------------------------------

                if (!$this->outputSetFormat($this->sOutputFormat)) {
                    throw new ApiException(
                        '"' . $this->sOutputFormat . '" is not a valid format.',
                        $oHttpCodes::STATUS_BAD_REQUEST
                    );
                }

                // --------------------------------------------------------------------------

                //  Register API modules
                $aNamespaces = [
                    'app' => (object) [
                        'namespace' => 'App\\',
                    ],
                ];
                foreach (Components::modules() as $oModule) {
                    if (!empty($oModule->data->{'nails/module-api'}->namespace)) {
                        $sNamespace = $oModule->data->{'nails/module-api'}->namespace;
                        if (array_key_exists($sNamespace, $aNamespaces)) {
                            throw new NailsException(
                                'Conflicting API namespace "' . $sNamespace . '" in use by ' .
                                '"' . $oModule->slug . '" and "' . $aNamespaces[$sNamespace]->slug . '"'
                            );
                        }
                        $aNamespaces[$sNamespace] = $oModule;
                    }
                }

                $i404Status = $oHttpCodes::STATUS_NOT_FOUND;
                $s404Error  = '"' . strtolower($this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod) . '" is not a valid API route.';

                if (!array_key_exists($this->sModuleName, $aNamespaces)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $oNamespace          = $aNamespaces[$this->sModuleName];
                $sOriginalController = $this->sClassName;

                //  Do we need to remap the controller?
                if (!empty($oNamespace->data->{'nails/module-api'}->{'controller-map'})) {

                    $aMap             = (array) $oNamespace->data->{'nails/module-api'}->{'controller-map'};
                    $this->sClassName = getFromArray($this->sClassName, $aMap, $this->sClassName);

                    //  This prevents users from accessing the "correct" controller, so we only have one valid route
                    $sRemapped = array_search($sOriginalController, $aMap);
                    if ($sRemapped !== false) {
                        $this->sClassName = $sRemapped;
                    }
                }

                $sController = $oNamespace->namespace . 'Api\\Controller\\' . $this->sClassName;

                if (!class_exists($sController)) {
                    throw new ApiException($s404Error, $i404Status);
                }

                $mAuth = $sController::isAuthenticated($this->sRequestMethod, $this->sMethod);
                if ($mAuth !== true) {

                    if (is_array($mAuth)) {
                        $sError  = getFromArray('error', $mAuth, $oHttpCodes::getByCode($oHttpCodes::STATUS_UNAUTHORIZED));
                        $iStatus = (int) getFromArray('status', $mAuth, $oHttpCodes::STATUS_UNAUTHORIZED);
                    } else {
                        $sError  = 'You must be logged in to access this resource';
                        $iStatus = $oHttpCodes::STATUS_UNAUTHORIZED;
                    }

                    throw new ApiException($sError, $iStatus);
                }

                if (!empty($sController::REQUIRE_SCOPE)) {
                    if (!$oUserAccessTokenModel->hasScope($oAccessToken, $sController::REQUIRE_SCOPE)) {
                        throw new ApiException(
                            'Access token with "' . $sController::REQUIRE_SCOPE . '" scope is required.',
                            $oHttpCodes::STATUS_UNAUTHORIZED
                        );
                    }
                }

                //  New instance of the controller
                $oInstance = new $sController($this);

                /**
                 * We need to look for the appropriate method; we'll look in the following order:
                 *
                 * - {sRequestMethod}Remap()
                 * - {sRequestMethod}{method}()
                 * - anyRemap()
                 * - any{method}()
                 *
                 * The second parameter is whether the method is a remap method or not.
                 */
                $bDidFindRoute = false;
                $aMethods      = [
                    [
                        strtolower($this->sRequestMethod) . 'Remap',
                        true,
                    ],
                    [
                        strtolower($this->sRequestMethod) . ucfirst($this->sMethod),
                        false,
                    ],
                    [
                        'anyRemap',
                        true,
                    ],
                    [
                        'any' . ucfirst($this->sMethod),
                        false,
                    ],
                ];

                foreach ($aMethods as $aMethodName) {

                    $sMethod  = getFromArray(0, $aMethodName);
                    $bIsRemap = (bool) getFromArray(1, $aMethodName);
                    if (is_callable([$oInstance, $sMethod])) {

                        $bDidFindRoute = true;

                        /**
                         * If the method we're trying to call is a remap method, then the first
                         * param should be the name of the method being called
                         */
                        if ($bIsRemap) {
                            $oResponse = call_user_func_array([$oInstance, $sMethod], [$this->sMethod]);
                        } else {
                            $oResponse = call_user_func([$oInstance, $sMethod]);
                        }
                        break;
                    }
                }

                if (!$bDidFindRoute) {
                    throw new ApiException(
                        '"' . $this->sRequestMethod . ': ' . $this->sModuleName . '/' . $this->sClassName . '/' . $this->sMethod . '" is not a valid API route.',
                        $oHttpCodes::STATUS_NOT_FOUND
                    );
                }

                if (!($oResponse instanceof ApiResponse)) {
                    //  This is a misconfiguration error, which we want to bubble up to the error handler
                    throw new NailsException(
                        'Return object must be an instance of \Nails\Api\Factory\ApiResponse',
                        $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR
                    );
                }

                $aOut = [
                    'status' => $oHttpCodes::STATUS_OK,
                    'data'   => $oResponse->getData(),
                    'meta'   => $oResponse->getMeta(),
                ];

            } catch (ValidationException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_BAD_REQUEST,
                    'error'   => $e->getMessage() ?: 'An unkown validation error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (ApiException $e) {

                $aOut = [
                    'status'  => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                    'error'   => $e->getMessage() ?: 'An unkown error occurred',
                    'details' => $e->getData() ?: [],
                ];
                if (isSuperuser()) {
                    $aOut['exception'] = (object) array_filter([
                        'type' => get_class($e),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                    ]);
                }

                $this->writeLog($aOut);

            } catch (\Exception $e) {
                /**
                 * When running in PRODUCTION we want the global error handler to catch exceptions so that they
                 * can be handled proeprly and reported if necessary. In other environments we want to show the
                 * developer the error quickly and with as much info as possible.
                 */
                if (Environment::is(Environment::ENV_PROD)) {
                    throw $e;
                } else {
                    $aOut = [
                        'status'    => $e->getCode() ?: $oHttpCodes::STATUS_INTERNAL_SERVER_ERROR,
                        'error'     => $e->getMessage() ?: 'An unkown error occurred',
                        'exception' => (object) array_filter([
                            'type' => get_class($e),
                            'file' => $e->getFile(),
                            'line' => $e->getLine(),
                        ]),
                    ];

                    $this->writeLog($aOut);
                }
            }

            $this->output($aOut);
        }
    }
Route the call to the correct place
@return void