From 01964ac1992f1cd7a65a869be27a2cda33ac2d39 Mon Sep 17 00:00:00 2001 From: Ralf Date: Tue, 1 Aug 2023 16:07:55 +0200 Subject: [PATCH 1/2] update php 7 > 8.1+, dependencies, phpunit10, psr12, bugfixing and code cleanup --- .gitignore | 6 + backend/class/api/rest.php | 369 +++++------ backend/class/api/rest/accesskey.php | 46 +- backend/class/api/rest/accesstoken.php | 47 +- backend/class/app.php | 575 +++++++++--------- backend/class/auth/accesskey.php | 85 +-- backend/class/auth/accesstoken.php | 108 ++-- backend/class/context/restApiContext.php | 186 +++--- .../context/restApiContext/apiEndpoint.php | 249 ++++---- backend/class/context/restContext.php | 154 ++--- .../class/context/restContextInterface.php | 102 ++-- backend/class/context/restcrud.php | 373 ++++++------ backend/class/credential/accesskey.php | 45 +- backend/class/credential/accesstoken.php | 68 ++- backend/class/helper/context.php | 399 ++++++++++++ .../class/model/exposesRemoteApiInterface.php | 14 +- backend/class/model/schemeless/remote.php | 94 +-- .../model/schemeless/remote/restcontext.php | 91 +-- .../model/schemeless/remote/restcrud.php | 56 +- backend/class/request/json.php | 179 +++--- backend/class/response/json.php | 320 +++++----- .../structure/credential/accesskey.php | 22 +- .../structure/credential/accesstoken.php | 24 +- composer.json | 55 +- config/context/restcrud.json | 16 +- frontend/template/json/template.php | 2 + phpcs.xml | 26 +- phpunit.xml | 22 + psalm.xml | 14 + tests/autoload.php | 38 ++ 30 files changed, 2145 insertions(+), 1640 deletions(-) create mode 100644 .gitignore create mode 100644 backend/class/helper/context.php create mode 100644 phpunit.xml create mode 100644 psalm.xml create mode 100644 tests/autoload.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d60c21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/* +/vendor/ +.phpunit.result.cache +.phpunit.cache +.phpunit +/tmp diff --git a/backend/class/api/rest.php b/backend/class/api/rest.php index dfdc261..e02c90b 100644 --- a/backend/class/api/rest.php +++ b/backend/class/api/rest.php @@ -1,7 +1,16 @@ Is used to handle HTTP(s) requests for retrieving and sending data from the foreign service - * @var \curl + * Is used to handle HTTP(s) requests for retrieving and sending data from the foreign service + * @var CurlHandle */ - protected $curlHandler; - + protected CurlHandle $curlHandler; /** * What is the request's special secret string? - *
Many codename API services are relying on a second authentication factor. - *
By definition the second factor is dependent from the concret topic of the request - *
The given salt is filled with the requested key's name. - *
So every different key has a different salt. + * Many codename API services are relying on a second authentication factor. + * By definition the second factor is dependent from the concrete topic of the request + * The given salt is filled with the requested key's name. + * So every different key has a different salt. * @internal Will not be transferred unencrypted * @var string */ - protected $salt = ''; - + protected string $salt = ''; /** - * Contains the API serive provider's response to the given request - *
After retrieving a response from the foreign host, it will be stored here. + * Contains the API service provider's response to the given request + * After retrieving a response from the foreign host, it will be stored here. * @var array */ - protected $response = ''; - + protected mixed $response = ''; /** * Contains the API type. - *
Typically is defined using the name of the foreign service in upper-case characters + * Typically, is defined using the name of the foreign service in upper-case characters * @example YOURAPITYPE * @var string */ - protected $type = ''; - + protected string $type = ''; /** * Contains POST data for the request - *
Typically all headers for authentication and data retrieval + * Typically all headers for authentication and data retrieval * @var array */ - protected $data = array(); - - /** - * Contains a list of fields that must not be sent via the POST request - *
Most of the given fields may irritate the foreign service as they are based on the core core - * @internal This is since these fields are request arguments responsible for app routing. - * @var array - */ - public $forbiddenpostfields = array('app', 'context', 'view', 'action', 'callback', 'template', 'lang'); + protected array $data = []; /** * Create instance * @param array $data - * @return \codename\rest\api\rest + * @throws ReflectionException + * @throws exception */ - public function __CONSTRUCT(array $data) { - - $this->errorstack = new \codename\core\errorstack($this->type); + public function __construct(array $data) + { + $this->errorstack = new errorstack($this->type); /* // TODO: Validate! if(count($errors = app::getValidator('structure_api_codename')->validate($data)) > 0) { @@ -109,18 +111,6 @@ public function __CONSTRUCT(array $data) { return $this; } - /** - * [createServiceProvider description] - * @param array $data [description] - * @return \codename\core\value\structure\api\codename\serviceprovider [description] - */ - protected function createServiceProvider(array $data) : \codename\core\value\structure\api\codename\serviceprovider { - return new \codename\core\value\structure\api\codename\serviceprovider([ - 'host' => $data['host'], - 'port' => $data['port'] - ]); - } - /** * return a credential object * this must be implemented for each kind of rest-client @@ -128,166 +118,195 @@ protected function createServiceProvider(array $data) : \codename\core\value\str * * type checking should also be done here * - * @param array $data [description] - * @return \codename\core\credential + * @param array $data [description] + * @return credential */ - protected abstract function createAuthenticationCredential(array $data) : \codename\core\credential; + abstract protected function createAuthenticationCredential(array $data): credential; /** - * Returns the cacheGroup for this instance - * @return string + * [createServiceProvider description] + * @param array $data [description] + * @return serviceprovider [description] + * @throws ReflectionException + * @throws exception */ - protected function getCachegroup() : string { - return 'API_' . $this->type . '_' . $this->getIdentifier(); + protected function createServiceProvider(array $data): serviceprovider + { + return new serviceprovider([ + 'host' => $data['host'], + 'port' => $data['port'], + ]); } /** - * get an identifier for the current system/app/user - * may be either a user id, accesskey or something else. - * @return string [description] + * [get description] + * @param string $uri [description] + * @param array $params [description] + * @return mixed [type] [description] + * @throws ReflectionException + * @throws exception */ - protected abstract function getIdentifier() : string; + public function get(string $uri, array $params = []): mixed + { + return $this->request($uri, 'GET', $params); + } /** * Mapper for the request function. - *
This method will concatenate the URL and return the (void) result of doRequest($url). + * This method will concatenate the URL and return the (void) result of doRequest($url). * @param string $url * @param string $method - * @param array $params + * @param array $params * @return mixed + * @throws ReflectionException + * @throws exception */ - public function request(string $url, string $method = 'GET', array $params = []) { + public function request(string $url, string $method = 'GET', array $params = []): mixed + { return $this->doRequest($this->serviceprovider->getUrl() . $url, $method, $params); } /** - * [get description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * {@inheritDoc} */ - public function get(string $uri, array $params = []) { - return $this->request($uri, 'GET', $params); - // return $this->doRequest($this->serviceprovider->getUrl() . $uri, 'GET', $params); + protected function doRequest(string $url, string $method = '', array $params = []): mixed + { + $this->errorstack->reset(); + return parent::doRequest($url, $method, $params); } /** * [put description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * @param string $uri [description] + * @param array $params [description] + * @return mixed [type] [description] + * @throws ReflectionException + * @throws exception */ - public function put(string $uri, array $params = []) { - return $this->request($uri, 'PUT', $params); - // return $this->doRequest($this->serviceprovider->getUrl() . $uri, 'PUT', $params); + public function put(string $uri, array $params = []): mixed + { + return $this->request($uri, 'PUT', $params); } /** * [post description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * @param string $uri [description] + * @param array $params [description] + * @return mixed [type] [description] + * @throws ReflectionException + * @throws exception */ - public function post(string $uri, array $params = []) { - return $this->request($uri, 'POST', $params); - // return $this->doRequest($this->serviceprovider->getUrl() . $uri, 'POST', $params); + public function post(string $uri, array $params = []): mixed + { + return $this->request($uri, 'POST', $params); } /** * [patch description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * @param string $uri [description] + * @param array $params [description] + * @return mixed [type] [description] + * @throws ReflectionException + * @throws exception */ - public function patch(string $uri, array $params = []) { - return $this->request($uri, 'PATCH', $params); - return $this->doRequest($this->serviceprovider->getUrl() . $uri, 'PATCH', $params); + public function patch(string $uri, array $params = []): mixed + { + return $this->request($uri, 'PATCH', $params); } /** * [delete description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * @param string $uri [description] + * @param array $params [description] + * @return void [type] [description] */ - public function delete(string $uri, array $params = []) { - + public function delete(string $uri, array $params = []): void + { } /** * [options description] - * @param string $uri [description] - * @param array $params [description] - * @return [type] [description] + * @param string $uri [description] + * @param array $params [description] + * @return void [type] [description] */ - public function options(string $uri, array $params = []) { - + public function options(string $uri, array $params = []): void + { } - // not implemented at the moment - // public function connect(string $uri, array $params = []) { - // } - - // not implemented at the moment - // public function trace(string $uri, array $params = []) { - // } - - /** * Sets data for the request to be sent. - *
Will erialize arrays as JSON. + * Will serialize arrays as JSON. * @param array $data * @return void */ - public function setData(array $data) { - foreach($data as $key => $value) { - if(is_array($value)) { - if((count($value) > 0) && (reset($value) instanceof \CURLFile) ) { - // add the CURLFile as a POST content - $this->addData($key, $value); - continue; + public function setData(array $data): void + { + foreach ($data as $key => $value) { + if (is_array($value)) { + if ((count($value) > 0) && (reset($value) instanceof CURLFile)) { + // add the CURLFile as a POST content + $this->addData($key, $value); + continue; } else { - $value = json_encode($value); + $value = json_encode($value); } } - $this->addData($key, $value); + $this->addData($key, $value); } - return; } /** * Adds another key to the data array of this instance. - *
Will check for the forbiddenpostfields here and do nothing if the field's $name is forbidden + * Will check for the forbiddenpostfields here and do nothing if the field's $name is forbidden * @param string $name * @param mixed|null $value * @return void */ - public function addData(string $name, $value) { - if(in_array($name, $this->forbiddenpostfields)) { + public function addData(string $name, mixed $value): void + { + if (in_array($name, $this->forbiddenpostfields)) { return; } $this->data[$name] = $value; - return; } /** * Returns the errorstack of the API instance - * @return \codename\core\errorstack + * @return errorstack */ - public function getErrorstack() : \codename\core\errorstack { + public function getErrorstack(): errorstack + { return $this->errorstack; } + /** + * Returns the cacheGroup for this instance + * @return string + */ + protected function getCacheGroup(): string + { + return 'API_' . $this->type . '_' . $this->getIdentifier(); + } + + /** + * get an identifier for the current system/app/user + * may be either a user id, accesskey or something else. + * @return string [description] + */ + abstract protected function getIdentifier(): string; + /** * Hashes the type, app, secret and salt of this instance and returns the hash value * @return string **/ - protected function makeHash() : string { - if(strlen($this->salt) == 0) { + protected function makeHash(): string + { + if (strlen($this->salt) == 0) { $this->errorstack->addError('setup', 'SERVICE_SALT_NOT_FOUND'); print_r($this->errorstack->getErrors()); } - if(strlen($this->type) == 0) { + if (strlen($this->type) == 0) { $this->errorstack->addError('setup', 'TYPE_NOT_FOUND'); print_r($this->errorstack->getErrors()); } @@ -299,79 +318,74 @@ protected function makeHash() : string { * @param string $version * @param string $endpoint * @return bool + * @throws ReflectionException + * @throws exception */ - protected function doAPIRequest(string $version, string $endpoint) : bool { + protected function doAPIRequest(string $version, string $endpoint): bool + { return $this->doRequest($this->serviceprovider->getUrl() . '/' . $version . '/' . $endpoint); } /** - * [getAuthenticationHeaders description] - * @return array [description] - */ - protected abstract function getAuthenticationHeaders() : array; - - /** - * @inheritDoc + * {@inheritDoc} */ - protected function prepareRequest(string $url, string $method, array $params = []) + protected function prepareRequest(string $url, string $method, array $params = []): void { - parent::prepareRequest($url, $method, $params); + parent::prepareRequest($url, $method, $params); - // convert key => value array to - // key: value string elements - // $authenticationHeaders = []; - foreach($this->getAuthenticationHeaders() as $key => $value) { - // $authenticationHeaders[] = "{$key}: {$value}"; - $this->setHeader($key, $value); - } - - // curl_setopt($this->curlHandler, CURLOPT_HTTPHEADER, $authenticationHeaders); + // convert key => value array to + // key: value string elements + // $authenticationHeaders = []; + foreach ($this->getAuthenticationHeaders() as $key => $value) { + $this->setHeader($key, $value); + } } /** - * @inheritDoc + * [getAuthenticationHeaders description] + * @return array [description] */ - protected function doRequest(string $url, string $method = '', array $params = []) { - $this->errorstack->reset(); - return parent::doRequest($url, $method, $params); - } + abstract protected function getAuthenticationHeaders(): array; /** * If data exist, this function will write the data as POST fields to the curlHandler * @return void */ - protected function sendData() { - if(count($this->data) > 0) { + protected function sendData(): void + { + if (count($this->data) > 0) { curl_setopt($this->curlHandler, CURLOPT_POST, 1); - foreach($this->data as $key => &$value) { - if(is_array($value)) { - if(count($value) > 0 && !(reset($value) instanceof \CURLFile)) { - $value = json_encode($value); + foreach ($this->data as &$value) { + if (is_array($value)) { + if (count($value) > 0 && !(reset($value) instanceof CURLFile)) { + $value = json_encode($value); } } } curl_setopt($this->curlHandler, CURLOPT_POST, count($this->data)); curl_setopt($this->curlHandler, CURLOPT_POSTFIELDS, $this->data); } - return; } /** * Decodes the response and validates it - *
Uses validators (\codename\core\validator\structure\api\response) to check the response content - *
Will return false on any error. - *
Will output cURL errors on development environments + * Uses validators (\codename\core\validator\structure\api\response) to check the response content + * Will return false on any error. + * Will output cURL errors on development environments * @param string $response * @return mixed + * @throws ReflectionException + * @throws exception */ - protected function decodeResponse(string $response) { + protected function decodeResponse(string $response): mixed + { app::getLog('debug')->debug('CORE_BACKEND_CLASS_API_CODENAME_DECODERESPONSE::START ($response = ' . $response . ')'); - if(defined('CORE_ENVIRONMENT') && CORE_ENVIRONMENT == 'dev') { + if (defined('CORE_ENVIRONMENT') && CORE_ENVIRONMENT == 'dev') { print_r(curl_error($this->curlHandler)); } - if(strlen($response) == 0) { + if (strlen($response) == 0) { $this->response = null; app::getLog('errormessage')->warning('CORE_BACKEND_CLASS_API_CODENAME_DECODERESPONSE::RESPONSE_EMPTY ($response = ' . $response . ')'); $this->errorstack->addError('', 'RESPONSE_EMPTY', $response); @@ -380,23 +394,16 @@ protected function decodeResponse(string $response) { $response = app::object2array(json_decode($response)); - if(is_null($response)) { - $this->response = null; - // response null after DESERIALIZATION (!) (e.g. invalid format) - $this->errorstack->addError('', 'RESPONSE_NULL', $response); - return false; - } - - if(count($errors = app::getValidator('structure_api_codename_response')->validate($response)) > 0) { + if (count(app::getValidator('structure_api_codename_response')->validate($response)) > 0) { app::getLog('errormessage')->warning('CORE_BACKEND_CLASS_API_CODENAME_DECODERESPONSE::RESPONSE_INVALID ($response = ' . json_encode($response) . ')'); - // add detail data to erorstack + // add detail data to errorstack $this->errorstack->addError('', 'RESPONSE_INVALID', $response); return false; } $this->response = $response; - if(array_key_exists('errors', $response)) { + if (array_key_exists('errors', $response)) { app::getLog('errormessage')->warning('CORE_BACKEND_CLASS_API_CODENAME_DECODERESPONSE::RESPONSE_CONTAINS_ERRORS ($response = ' . json_encode($response) . ')'); // diff --git a/backend/class/api/rest/accesskey.php b/backend/class/api/rest/accesskey.php index 281cd20..83a3e54 100644 --- a/backend/class/api/rest/accesskey.php +++ b/backend/class/api/rest/accesskey.php @@ -1,30 +1,40 @@ $this->credential->getIdentifier(), - 'X-Secret' => $this->credential->getAuthentication() - ]; - } + /** + * {@inheritDoc} + */ + protected function getAuthenticationHeaders(): array + { + return [ + 'X-Accesskey' => $this->credential->getIdentifier(), + 'X-Secret' => $this->credential->getAuthentication(), + ]; + } } diff --git a/backend/class/api/rest/accesstoken.php b/backend/class/api/rest/accesstoken.php index e8675ef..f21bd71 100644 --- a/backend/class/api/rest/accesstoken.php +++ b/backend/class/api/rest/accesstoken.php @@ -1,32 +1,41 @@ $this->credential->getIdentifier(), - 'X-Token' => $this->credential->getAuthentication() - ]; - } + /** + * {@inheritDoc} + * @param array $data + * @return credential + * @throws ReflectionException + * @throws exception + */ + protected function createAuthenticationCredential(array $data): credential + { + return new \codename\rest\credential\accesstoken($data); + } + /** + * {@inheritDoc} + */ + protected function getAuthenticationHeaders(): array + { + return [ + 'X-Accesskey' => $this->credential->getIdentifier(), + 'X-Token' => $this->credential->getAuthentication(), + ]; + } } diff --git a/backend/class/app.php b/backend/class/app.php index c0489a7..2e9da3c 100644 --- a/backend/class/app.php +++ b/backend/class/app.php @@ -1,341 +1,310 @@ 'codename', + 'app' => 'rest', + 'namespace' => '\\codename\\rest', + ]); + + parent::__construct(); } - // self-inject - self::injectApp(array( - 'vendor' => 'codename', - 'app' => 'rest', - 'namespace' => '\\codename\\rest' - )); - - parent::__CONSTRUCT(); - } - - /** - * @inheritDoc - */ - public function run() - { - if(static::isRestClient()) { - $qualifier = self::getEndpointQualifier(); - $this->getRequest()->addData($qualifier); + /** + * returns true, if client is requesting via REST protocol (e.g. no HTML output) + * @return bool|null + */ + protected static function isRestClient(): ?bool + { + if (self::$overrideIsRestClient !== null) { + return self::$overrideIsRestClient; + } else { + // + // NOTE: possible bad request behaviour with unknown accept-header which causes a text-exception to occur -> FE output + // It is also possible we need to check for lowercase header (http_accept) due to HTTP2 specification + // + + if (app::getRequest() instanceof cli) { + // Definitely a CLI client + return false; + } + if (str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json')) { + // Prefer JSON response, we assume a REST Client + return true; + } + if (str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'text/html')) { + // No explicit JSON requested, but includes text/html - assume regular browser + return false; + } + + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + // OPTIONS request (typical for XHRs, so assume a REST Client) + return true; + } + + // Unknown state, falsy + return null; + } } - // run normally - parent::run(); - } - - /** - * @inheritDoc - */ - protected function mainRun() - { - if($this->getContext() instanceof \codename\core\context\customContextInterface) { - $this->doContextRun(); - } else { - $this->doAction()->doView(); - // HTTP API Endpoint-specific method running - if(self::isRestClient()) { - $this->doMethod(); - } + + /** + * overrides the app::isRestClient() value + * @param bool|null $state [true/false overrides, null resets] + */ + public static function setOverrideIsRestClient(?bool $state): void + { + self::$overrideIsRestClient = $state; + } + + /** + * Replaces the response object + * to be used for facade emulation + * + * @param response $response [description] + * @return response [description] + */ + public static function setResponse(response $response): response + { + return self::$instances['response'] = $response; } - $this->doShow()->doOutput(); - } - - /** - * @inheritDoc - */ - protected function doShow(): \codename\core\app - { - if(static::isRestClient() || ($this->getResponse() instanceof \codename\core\response\json)) { - // rest client output does NOT provide "show" - return $this; - } else { - // Fallback to default output (no rest client) - return parent::doShow(); + + /** + * {@inheritDoc} + * @throws \Exception + */ + public function run(): void + { + if (static::isRestClient()) { + $qualifier = self::getEndpointQualifier(); + static::getRequest()->addData($qualifier); + } + // run normally + parent::run(); } - } - /** - * performs HTTP-Method based routines - * @return \codename\core\app [description] - */ - protected function doMethod(): \codename\core\app - { - if($this->getContext() instanceof \codename\rest\context\restContextInterface) { - $httpMethod = strtolower($_SERVER['REQUEST_METHOD']); + /** + * Return the endpoint target of the request + * @return array + * @example $host/v1/context/view//?... + */ + public static function getEndpointQualifier(): array + { + if (!isset($_SERVER['REQUEST_URI'])) { + return []; + } + $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); - $method = "method_{$httpMethod}"; + // get rid of the first part of the uri (e.g. host, port, etc.) + array_shift($endpoints); - if (!method_exists($this->getContext(), $method)) { - throw new \codename\core\exception(self::EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND, \codename\core\exception::$ERRORLEVEL_ERROR, $method); - } + $ret = []; - $this->getContext()->$method(); - } - return $this; - } - - /** - * [EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND description] - * @var string - */ - const EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND = 'EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND'; - - /** - * returns true, if client is requesting via REST protocol (e.g. no HTML output) - * @return bool|null - */ - protected static function isRestClient() : ?bool { - if(self::$overrideIsRestClient !== null) { - return self::$overrideIsRestClient; - } else { - // - // NOTE: possible bad request behaviour with unknown accept-header which causes a text-exception to occur -> FE output - // It is also possible we need to check for lowercase header (http_accept) due to HTTP2 specification - // - - if(app::getRequest() instanceof \codename\core\request\cli) { - // Definitely a CLI client - return false; - } - if(strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') !== false) { - // Prefer JSON response, we assume a REST Client - return true; - } - if(strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'text/html') !== false) { - // No explicit JSON requested, but includes text/html - assume regular browser - return false; - } - - if($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { - // OPTIONS request (typical for XHRs, so assume a REST Client) - return true; - } - - // Unknown state, falsy - return null; + // get context, if defined + $i = 0; + if (!empty($endpoints[$i])) { + $ret['context'] = $endpoints[$i]; + } + + // get view, if defined + $i = 1; + if (!empty($endpoints[$i])) { + $ret['view'] = $endpoints[$i]; + } + + // get action, if defined + $i = 2; + if (!empty($endpoints[$i])) { + $ret['action'] = $endpoints[$i]; + } + + // cancel, if there are more than 3 parts + return $ret; } - } - - /** - * overrides the app::isRestClient() result, if !== null - * @var bool - */ - public static $overrideIsRestClient = null; - - /** - * overrides the app::isRestClient() value - * @param bool|null $state [true/false overrides, null resets] - */ - public static function setOverrideIsRestClient(?bool $state) { - self::$overrideIsRestClient = $state; - } - - /** - * Replaces the response object - * to be used for facade emulation - * - * @param \codename\core\response $response [description] - * @return \codename\core\response [description] - */ - public static function setResponse(\codename\core\response $response) : \codename\core\response { - return self::$instances['response'] = $response; - } - - /** - * handle authentication - * @return bool - */ - protected function authenticate() : bool { - return app::getAuth()->isAuthenticated(); - } - - /** - * @inheritDoc - */ - protected function handleAccess(): bool - { - if(isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') { - - if(!($this->getContext() instanceof \codename\rest\context\restContextInterface)) { - // this a REST Preflight request. Kill it. - self::getResponse()->pushOutput(); - exit(); + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + protected function mainRun(): void + { + if ($this->getContext() instanceof customContextInterface) { + $this->doContextRun(); } else { - $this->getContext()->method_options(); - self::getResponse()->pushOutput(); - exit(); + $this->doAction()->doView(); + // HTTP API Endpoint-specific method running + if (self::isRestClient()) { + $this->doMethod(); + } } - + $this->doShow()->doOutput(); } - if($this->getContext() instanceof \codename\core\context\customContextInterface) { - $isPublic = $this->getContext()->isPublic(); - } else { - $isPublic = self::getConfig()->get("context>{$this->getRequest()->getData('context')}>view>{$this->getRequest()->getData('view')}>public") === true; + /** + * performs HTTP-Method based routines + * @return \codename\core\app [description] + * @throws ReflectionException + * @throws exception + */ + protected function doMethod(): \codename\core\app + { + if ($this->getContext() instanceof restContextInterface) { + $httpMethod = strtolower($_SERVER['REQUEST_METHOD']); + + $method = "method_$httpMethod"; + + if (!method_exists($this->getContext(), $method)) { + throw new exception(self::EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND, exception::$ERRORLEVEL_ERROR, $method); + } + + $this->getContext()->$method(); + } + return $this; } - $isAuthenticated = null; - if(!$isPublic) { - // perform authentication - if(!$this->authenticate()) { - // authentication_error - self::getResponse()->setStatus(\codename\core\response::STATUS_UNAUTHENTICATED); - $isAuthenticated = false; - } else { - $isAuthenticated = true; - } + /** + * {@inheritDoc} + * overridden output method + * omit templating engines and stuff. + */ + protected function doOutput(): void + { + // Fallback to default output, if client is not a REST client + if (!self::isRestClient()) { + parent::doOutput(); + return; + } + + app::getResponse()->pushOutput(); } - $isAllowed = $this->getContext()->isAllowed(); + /** + * {@inheritDoc} + */ + protected function doShow(): \codename\core\app + { + if (static::isRestClient() || (static::getResponse() instanceof json)) { + // rest client output does NOT provide "show" + return $this; + } else { + // Fallback to default output (no rest client) + return parent::doShow(); + } + } - if(!$isAllowed && !$isPublic) { - self::getHook()->fire(\codename\core\hook::EVENT_APP_RUN_FORBIDDEN); + /** + * {@inheritDoc} + */ + protected function handleAccess(): bool + { + $context = $this->getContext(); + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + if ($context instanceof restContextInterface) { + $context->method_options(); + } + self::getResponse()->pushOutput(); + exit(); + } - if($isAuthenticated) { - self::getResponse()->setStatus(\codename\core\response::STATUS_FORBIDDEN); - // self::getResponse()->setData('session_debug', [ - // 'sess' => self::getSession()->getData(), - // 'is_allowed' => $isAllowed, - // 'is_public' => $isPublic - // ]); + if ($context instanceof customContextInterface) { + $isPublic = $context->isPublic(); } else { - self::getResponse()->setStatus(\codename\core\response::STATUS_UNAUTHENTICATED); + $isPublic = self::getConfig()->get('context>' . static::getRequest()->getData('context') . '>view>' . static::getRequest()->getData('view') . '>public') === true; } - self::getResponse()->pushOutput(); - exit(); - return false; - } else { + $isAuthenticated = null; + if (!$isPublic) { + // perform authentication + if (!$this->authenticate()) { + // authentication_error + self::getResponse()->setStatus(response::STATUS_UNAUTHENTICATED); + $isAuthenticated = false; + } else { + $isAuthenticated = true; + } + } + + $isAllowed = $this->getContext()->isAllowed(); + + if (!$isAllowed && !$isPublic) { + self::getHook()->fire(hook::EVENT_APP_RUN_FORBIDDEN); - if(!$isPublic) { - if(!$isAuthenticated) { - self::getResponse()->setStatus(\codename\core\response::STATUS_UNAUTHENTICATED); - self::getResponse()->pushOutput(); - exit(); + if ($isAuthenticated) { + self::getResponse()->setStatus(response::STATUS_FORBIDDEN); + } else { + self::getResponse()->setStatus(response::STATUS_UNAUTHENTICATED); + } + + self::getResponse()->pushOutput(); + exit(); + } else { + if (!$isPublic) { + if (!$isAuthenticated) { + self::getResponse()->setStatus(response::STATUS_UNAUTHENTICATED); + self::getResponse()->pushOutput(); + exit(); + } + } + return true; } - } - // self::getResponse()->setData('auth_debug', [ - // 'context::isAllowed' => $isAllowed, - // 'public' => $isPublic - // ]); - - // if($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { - // // this a REST Preflight request. Kill it. - // self::getResponse()->pushOutput(); - // exit(); - // } - return true; - } - } - - /** - * @inheritDoc - * overridden output method - * omit templating engines and stuff. - */ - protected function doOutput() - { - // Fallback to default output, if client is not a REST client - if(!self::isRestClient()) { - parent::doOutput(); - return; } - app::getResponse()->pushOutput(); - return; - - // ? - // app::getResponse()->setHeader('Content-Type: application/json'); - // - // $response = array( - // 'success' => app::getResponse()->getSuccess(), - // 'data' => app::getResponse()->getData() - // ); - // - // if(count($errors = app::getResponse()->getErrors()) > 0) { - // $response['errors'] = $errors; - // } - // - // $json = json_encode($response); - // - // if(json_last_error() !== JSON_ERROR_NONE) { - // $errorResponse = [ - // 'success' => 0, - // 'errors' => [ - // json_last_error_msg() - // ] - // ]; - // $json = json_encode($errorResponse); - // } - // - // print_r($json); - } - - /** - * Return the endpoint target of the request - * @example $host/v1/context/view//?... - * @return array - */ - public static function getEndpointQualifier() : array { - if(!isset($_SERVER['REQUEST_URI'])) { - return []; - } - $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); - - // get rid of the first part of the uri (e.g. host, port, etc.) - array_shift($endpoints); - - // if(count($endpoints) > 3) { - // throw new exception("CORE_REST_APP_TOO_MANY_ENDPOINT_QUALIFIERS", exception::$ERRORLEVEL_FATAL, $endpoints); - // } - - $ret = array(); - - // get context, if defined - $i = 0; - if(isset($endpoints[$i]) && !empty($endpoints[$i])) { - $ret['context'] = $endpoints[$i]; - } - - // get view, if defined - $i = 1; - if(isset($endpoints[$i]) && !empty($endpoints[$i])) { - $ret['view'] = $endpoints[$i]; - } - - // get action, if defined - $i = 2; - if(isset($endpoints[$i]) && !empty($endpoints[$i])) { - $ret['action'] = $endpoints[$i]; - } - - // cancel, if there are more than 3 parts - return $ret; - } + /** + * handle authentication + * @return bool + * @throws ReflectionException + * @throws exception + */ + protected function authenticate(): bool + { + return app::getAuth()->isAuthenticated(); + } } diff --git a/backend/class/auth/accesskey.php b/backend/class/auth/accesskey.php index f6b1627..1c13f84 100644 --- a/backend/class/auth/accesskey.php +++ b/backend/class/auth/accesskey.php @@ -1,51 +1,64 @@ getAuthentication(); // password_hash($credential->getAuthentication(), PASSWORD_BCRYPT); } - return $credential->getAuthentication(); // password_hash($credential->getAuthentication(), PASSWORD_BCRYPT); - } - /** - * @inheritDoc - */ - public function createCredential(array $parameters): \codename\core\credential - { - return new \codename\rest\credential\accesstoken($parameters); - } + /** + * {@inheritDoc} + * @param array $parameters + * @return credential + * @throws ReflectionException + * @throws exception + */ + public function createCredential(array $parameters): credential + { + return new \codename\rest\credential\accesstoken($parameters); + } - /** - * @inheritDoc - */ - public function memberOf(string $groupName): bool - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function memberOf(string $groupName): bool + { + throw new LogicException('Not implemented'); // TODO + } } diff --git a/backend/class/context/restApiContext.php b/backend/class/context/restApiContext.php index c571232..cd6bebc 100644 --- a/backend/class/context/restApiContext.php +++ b/backend/class/context/restApiContext.php @@ -1,110 +1,108 @@ getResponse() instanceof \codename\rest\response\json) { - $this->getResponse()->reset(); - } - } - - /** - * @inheritDoc - */ - public function run() - { - if(!isset($_SERVER['REQUEST_URI'])) { - return []; - } - $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); - - // get rid of the first part of the uri (e.g. host, port, etc.) - array_shift($endpoints); - - $shortName = (new \ReflectionClass($this))->getShortName(); - - if(($entryPoint = array_shift($endpoints)) == $shortName) { - $lookup = $entryPoint; - $endpointComponents = []; - - do { - - $lookup = $entryPoint . '_' . implode('_', $endpoints); - - // $this->getResponse()->setData('api_debug', array_merge( - // $this->getResponse()->getData('api_debug') ?? [], - // [ $lookup ] - // )); - - try { - // $class = app::getInheritedClass('context_'.$lookup); - $class = $this->getApiEndpointClass('context_'.$lookup); - } catch (\Exception $e) { - continue; +use codename\core\response; +use codename\rest\context\restApiContext\apiEndpoint; +use codename\rest\response\json; +use ReflectionClass; +use ReflectionException; + +abstract class restApiContext extends context implements customContextInterface +{ + /** + * @throws exception + */ + public function __construct() + { + // reset response data + // this is a data-only context + if ($this->getResponse() instanceof json) { + $this->getResponse()->reset(); } + } - $endpointConfig = [ - 'endpoint_components' => $endpoints - ]; - - $instance = new $class($endpointConfig); - if($instance instanceof \codename\rest\context\restApiContext\apiEndpoint) { - if(strtolower($_SERVER['REQUEST_METHOD'] ?? '') === 'options') { - $instance->method_options(); + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function run(): void + { + if (!isset($_SERVER['REQUEST_URI'])) { return; - } - if(!$instance->isPublic()) { - if(!app::getAuth()->isAuthenticated()) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_UNAUTHENTICATED); - return; - } - if(!$instance->isAllowed()) { - $this->getResponse()->setStatus(\codename\core\response::STATUS_FORBIDDEN); - return; - } - } - $instance->run(); - return; } - } while($endpoints = array_slice($endpoints, 0, -1)); - } + $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); + + // get rid of the first part of the uri (e.g. host, port, etc.) + array_shift($endpoints); + + $shortName = (new ReflectionClass($this))->getShortName(); + + if (($entryPoint = array_shift($endpoints)) == $shortName) { + do { + $lookup = $entryPoint . '_' . implode('_', $endpoints); + + try { + $class = $this->getApiEndpointClass('context_' . $lookup); + } catch (\Exception) { + continue; + } + + $endpointConfig = [ + 'endpoint_components' => $endpoints, + ]; + + $instance = new $class($endpointConfig); + if ($instance instanceof apiEndpoint) { + if (strtolower($_SERVER['REQUEST_METHOD'] ?? '') === 'options') { + $instance->method_options(); + return; + } + if (!$instance->isPublic()) { + if (!app::getAuth()->isAuthenticated()) { + $this->getResponse()->setStatus(response::STATUS_UNAUTHENTICATED); + return; + } + if (!$instance->isAllowed()) { + $this->getResponse()->setStatus(response::STATUS_FORBIDDEN); + return; + } + } + $instance->run(); + return; + } + } while ($endpoints = array_slice($endpoints, 0, -1)); + } - throw new exception('EXCEPTION_RESTAPICONTEXT_INVALID_ENTRY_POINT', exception::$ERRORLEVEL_FATAL); - } + throw new exception('EXCEPTION_RESTAPICONTEXT_INVALID_ENTRY_POINT', exception::$ERRORLEVEL_FATAL); + } - /** - * [getApiEndpointClass description] - * @param string $classname [description] - * @return string [description] - */ - protected function getApiEndpointClass(string $classname) : string { - $classname = str_replace('_', '\\', $classname); - $file = str_replace('\\', '/', $classname); - foreach(app::getAppstack() as $parentapp) { - // do not traverse, check for current app - if($parentapp['app'] == app::getApp()) { - $filename = CORE_VENDORDIR . $parentapp['vendor'] . '/' . $parentapp['app'] . '/backend/class/' . $file . '.php'; - if(app::getInstance('filesystem_local')->fileAvailable($filename)) { - $namespace = $parentapp['namespace'] ?? '\\' . $parentapp['vendor'] . '\\' . $parentapp['app']; - return $namespace . '\\' . $classname; + /** + * [getApiEndpointClass description] + * @param string $classname [description] + * @return string [description] + * @throws ReflectionException + * @throws exception + */ + protected function getApiEndpointClass(string $classname): string + { + $classname = str_replace('_', '\\', $classname); + $file = str_replace('\\', '/', $classname); + foreach (app::getAppstack() as $parentapp) { + // do not traverse, check for current app + if ($parentapp['app'] == app::getApp()) { + $filename = CORE_VENDORDIR . $parentapp['vendor'] . '/' . $parentapp['app'] . '/backend/class/' . $file . '.php'; + if (app::getInstance('filesystem_local')->fileAvailable($filename)) { + $namespace = $parentapp['namespace'] ?? '\\' . $parentapp['vendor'] . '\\' . $parentapp['app']; + return $namespace . '\\' . $classname; + } + } } - } else { - continue; - } + throw new exception('EXCEPTION_RESTAPICONTEXT_INVALID_ENDPOINT', exception::$ERRORLEVEL_FATAL); } - throw new exception('EXCEPTION_RESTAPICONTEXT_INVALID_ENDPOINT', exception::$ERRORLEVEL_FATAL); - } } diff --git a/backend/class/context/restApiContext/apiEndpoint.php b/backend/class/context/restApiContext/apiEndpoint.php index 5a9f284..da26b74 100644 --- a/backend/class/context/restApiContext/apiEndpoint.php +++ b/backend/class/context/restApiContext/apiEndpoint.php @@ -1,131 +1,142 @@ endpointConfig = new \codename\core\config($endpointConfig); - } - - /** - * [public description] - * @return void - */ - public function run() { - $httpMethod = strtolower($_SERVER['REQUEST_METHOD']); - $method = "method_{$httpMethod}"; - $this->$method(); - } - - /** - * @inheritDoc - */ - public function method_get() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_head() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_post() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_put() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_delete() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_options() - { - if(count($headers = $this->getAllowedHeaders()) > 0) { - $this->getResponse()->setHeader('Access-Control-Allow-Headers: '.implode(', ', $headers)); + /** + * [protected description] + * @var config + */ + protected config $endpointConfig; + + /** + * [__construct description] + * @param array $endpointConfig [description] + */ + public function __construct(array $endpointConfig) + { + $this->endpointConfig = new config($endpointConfig); + } + + /** + * [public description] + * @return void + */ + public function run(): void + { + $httpMethod = strtolower($_SERVER['REQUEST_METHOD']); + $method = "method_$httpMethod"; + $this->$method(); + } + + /** + * {@inheritDoc} + */ + public function method_get(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function method_head(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function method_post(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function method_put(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function method_delete(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + * @throws exception + */ + public function method_options(): void + { + if (count($headers = $this->getAllowedHeaders()) > 0) { + $this->getResponse()->setHeader('Access-Control-Allow-Headers: ' . implode(', ', $headers)); + } + } + + /** + * [getAllowedHeaders description] + * @return array [description] + */ + protected function getAllowedHeaders(): array + { + return []; + } + + /** + * {@inheritDoc} + */ + public function method_trace(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * {@inheritDoc} + */ + public function method_patch(): void + { + throw new LogicException('Not implemented'); // TODO + } + + /** + * [isPublic description] + * @return bool [description] + */ + abstract public function isPublic(): bool; + + /** + * @return bool + * @throws ReflectionException + * @throws exception + * @see \codename\core\cache_interface::get($group, $key) + */ + public function isAllowed(): bool + { + $identity = app::getSession()->identify(); + + if (!$identity) { + return false; + } + + return $identity; } - } - - /** - * [getAllowedHeaders description] - * @return array [description] - */ - protected function getAllowedHeaders () : array { - return []; - } - - /** - * @inheritDoc - */ - public function method_trace() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_patch() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * [isPublic description] - * @return bool [description] - */ - public abstract function isPublic () : bool; - - /** - * - * {@inheritDoc} - * @see \codename\core\cache_interface::get($group, $key) - */ - public function isAllowed() : bool { - $identity = app::getSession()->identify(); - - if(!$identity) { - return false; - } - - return $identity; - } } diff --git a/backend/class/context/restContext.php b/backend/class/context/restContext.php index 8ed8914..41b2d56 100644 --- a/backend/class/context/restContext.php +++ b/backend/class/context/restContext.php @@ -1,95 +1,101 @@ getResponse() instanceof \codename\rest\response\json) { - $this->getResponse()->reset(); + /** + * @throws exception + */ + public function __construct() + { + // reset response data + // this is a data-only context + $response = $this->getResponse(); + if ($response instanceof json) { + $response->reset(); + } } - } - /** - * [view_default description] - * @return [type] [description] - */ - public function view_default () { - // empty - } + /** + * [view_default description] + * @return void [type] [description] + */ + public function view_default(): void + { + // empty + } - /** - * @inheritDoc - */ - public function method_get() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_get(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_head() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_head(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_post() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_post(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_put() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_put(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_delete() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_delete(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_trace() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_trace(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_options() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_options(): void + { + throw new LogicException('Not implemented'); // TODO + } - /** - * @inheritDoc - */ - public function method_patch() - { - throw new \LogicException('Not implemented'); // TODO - } + /** + * {@inheritDoc} + */ + public function method_patch(): void + { + throw new LogicException('Not implemented'); // TODO + } } diff --git a/backend/class/context/restContextInterface.php b/backend/class/context/restContextInterface.php index e1417e9..bad4653 100644 --- a/backend/class/context/restContextInterface.php +++ b/backend/class/context/restContextInterface.php @@ -1,57 +1,59 @@ getResponse()->reset(); - } - - /** - * implement this function and return your model - * @return \codename\core\model - */ - public abstract function getModelInstance() : \codename\core\model; - - /** - * default view. - */ - public function view_default() { - - } - - /** - * @inheritDoc - */ - public function method_get() - { - if($this->getRequest()->isDefined('id')) { - // SINGLE ENTRY - - $data = $this->getModelInstance()->entryLoad($this->getRequest()->getData('id'))->getData(); - - // set output data - $this->getResponse()->addData($data); - - } else { - // List - no PKEY defined - - $model = $this->getModelInstance(); - - // apply filters requested - if($this->getRequest()->isDefined('filter')) { - foreach($this->getRequest()->getData('filter') as $filter) { - $model->addFilter($filter['field'], $filter['value'], $filter['operator'] ?? '='); +abstract class restcrud extends context implements restContextInterface +{ + /** + * [EXCEPTION_REST_METHOD_NO_ID_PROVIDED description] + * @var string + */ + public const EXCEPTION_REST_METHOD_NO_ID_PROVIDED = 'EXCEPTION_REST_METHOD_NO_ID_PROVIDED'; + /** + * Overwrite what model to use in the CRUD generator + * @var null|string + */ + protected ?string $modelName = null; + /** + * Overwrite the name of the app the requested model is located + * @var null|string + */ + protected ?string $modelApp = null; + /** + * Holds the model for this CRUD generator + * @var null|model + */ + protected ?model $model = null; + + /** + * instantiate a new instance of this restcrud + * @throws exception + */ + public function __construct() + { + // reset response data + // this is a data-only context + if ($this->getResponse() instanceof json) { + $this->getResponse()->reset(); } - } - - $data = $model->search()->getResult(); - - // reset and set - $this->getResponse()->addData($data); } - } - - /** - * @inheritDoc - */ - public function method_head() - { - // META - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_post() - { - // CREATE / custom stuff? - $model = $this->getModelInstance(); - - $model->entryMake($this->getRequest()->getData()); - if(count($errors = $model->entryValidate()) === 0) { - $model->entrySave(); - - if($this->getRequest()->getData($model->getPrimarykey())) { - // id submitted, this was an update - $this->getResponse()->setData('id', $this->getRequest()->getData($model->getPrimarykey())); - } else { - // new created, return last insert id - $this->getResponse()->setData('id', $model->lastInsertId()); - } - } else { - throw new exception('EXCEPTION_RESTCRUD_VALIDATION_ERROR', exception::$ERRORLEVEL_ERROR, $errors); - } - } - - /** - * @inheritDoc - */ - public function method_put() - { - if($this->getRequest()->isDefined('id')) { - // update existing entry - $model = $this->getModelInstance(); - - $model->entryLoad($this->getRequest()->getData('id')); - $model->entryUpdate($this->getRequest()->getData()); - $model->entrySave(); - - $this->getResponse()->setData('debug', $model->getData()); - // set output data - // $this->getResponse()->addData($data); + /** + * default view. + * @return void + */ + public function view_default(): void + { + } - } else { + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function method_get(): void + { + if ($this->getRequest()->isDefined('id')) { + // SINGLE ENTRY + + $data = $this->getModelInstance()->entryLoad($this->getRequest()->getData('id'))->getData(); + // set output data + } else { + // List - no PKEY defined + + $model = $this->getModelInstance(); + + // apply filters requested + if ($this->getRequest()->isDefined('filter')) { + foreach ($this->getRequest()->getData('filter') as $filter) { + $model->addFilter($filter['field'], $filter['value'], $filter['operator'] ?? '='); + } + } + + $data = $model->search()->getResult(); + // reset and set + } + $this->getResponse()->addData($data); + } - // create a new entry - // may contain a primary key value, though - $model = $this->getModelInstance(); + /** + * implement this function and return your model + * @return model + */ + abstract public function getModelInstance(): model; + + /** + * {@inheritDoc} + */ + public function method_head(): void + { + // META + throw new LogicException('Not implemented'); // TODO + } - $model->entryMake($this->getRequest()->getData()); - $model->entrySave(); + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function method_post(): void + { + // CREATE / custom stuff? + $model = $this->getModelInstance(); + + $model->entryMake($this->getRequest()->getData()); + if (count($errors = $model->entryValidate()) === 0) { + $model->entrySave(); + + if ($this->getRequest()->getData($model->getPrimaryKey())) { + // id submitted, this was an update + $this->getResponse()->setData('id', $this->getRequest()->getData($model->getPrimaryKey())); + } else { + // new created, return last insert id + $this->getResponse()->setData('id', $model->lastInsertId()); + } + } else { + throw new exception('EXCEPTION_RESTCRUD_VALIDATION_ERROR', exception::$ERRORLEVEL_ERROR, $errors); + } + } - // reset and set - // $this->getResponse()->addData($data); + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function method_put(): void + { + if ($this->getRequest()->isDefined('id')) { + // update existing entry + + $model = $this->getModelInstance(); + + $model->entryLoad($this->getRequest()->getData('id')); + $model->entryUpdate($this->getRequest()->getData()); + } else { + // create a new entry + // may contain a primary key value, though + $model = $this->getModelInstance(); + + $model->entryMake($this->getRequest()->getData()); + } + $model->entrySave(); } - } - - /** - * @inheritDoc - */ - public function method_delete() - { - if($this->getRequest()->isDefined('id')) { - $model = $this->getModelInstance(); - $model->entryLoad($this->getRequest()->getData('id')); - $model->entryDelete(); - } else { - // error: no id provided - throw new exception(self::EXCEPTION_REST_METHOD_NO_ID_PROVIDED, exception::$ERRORLEVEL_ERROR); + + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function method_delete(): void + { + if ($this->getRequest()->isDefined('id')) { + $model = $this->getModelInstance(); + $model->entryLoad($this->getRequest()->getData('id')); + $model->entryDelete(); + } else { + // error: no id provided + throw new exception(self::EXCEPTION_REST_METHOD_NO_ID_PROVIDED, exception::$ERRORLEVEL_ERROR); + } } - } - - /** - * @inheritDoc - */ - public function method_trace() - { - throw new \LogicException('Not implemented'); // TODO - } - - /** - * @inheritDoc - */ - public function method_options() - { - // show methods available - // throw new \LogicException('Not implemented'); // TODO - // we may change OPTIONS through header setting via $this->getResponse()->setHeader... - return; - } - - /** - * @inheritDoc - */ - public function method_patch() - { - // EDIT - if($this->getRequest()->isDefined('id')) { - $model = $this->getModelInstance(); - $model->entryLoad($this->getRequest()->getData('id')); - $model->entryUpdate($this->getRequest()->getData()); - $model->entrySave(); - } else { - // error: no id provided - throw new exception(self::EXCEPTION_REST_METHOD_NO_ID_PROVIDED, exception::$ERRORLEVEL_ERROR); + + /** + * {@inheritDoc} + */ + public function method_trace(): void + { + throw new LogicException('Not implemented'); // TODO } - } - /** - * [EXCEPTION_REST_METHOD_NO_ID_PROVIDED description] - * @var string - */ - const EXCEPTION_REST_METHOD_NO_ID_PROVIDED = 'EXCEPTION_REST_METHOD_NO_ID_PROVIDED'; + /** + * {@inheritDoc} + */ + public function method_options(): void + { + // show methods available + // throw new \LogicException('Not implemented'); // TODO + // we may change OPTIONS through header setting via $this->getResponse()->setHeader... + } + /** + * {@inheritDoc} + * @throws ReflectionException + * @throws exception + */ + public function method_patch(): void + { + // EDIT + if ($this->getRequest()->isDefined('id')) { + $model = $this->getModelInstance(); + $model->entryLoad($this->getRequest()->getData('id')); + $model->entryUpdate($this->getRequest()->getData()); + $model->entrySave(); + } else { + // error: no id provided + throw new exception(self::EXCEPTION_REST_METHOD_NO_ID_PROVIDED, exception::$ERRORLEVEL_ERROR); + } + } } diff --git a/backend/class/credential/accesskey.php b/backend/class/credential/accesskey.php index ba18dd5..23b9095 100644 --- a/backend/class/credential/accesskey.php +++ b/backend/class/credential/accesskey.php @@ -1,6 +1,10 @@ get('accesskey'); - } + /** + * {@inheritDoc} + */ + public function getIdentifier(): string + { + return $this->get('accesskey'); + } - /** - * @inheritDoc - */ - public function getAuthentication() - { - return $this->get('secret'); - } + /** + * {@inheritDoc} + */ + public function getAuthentication(): mixed + { + return $this->get('secret'); + } } diff --git a/backend/class/credential/accesstoken.php b/backend/class/credential/accesstoken.php index d5ac2e9..618da3b 100644 --- a/backend/class/credential/accesstoken.php +++ b/backend/class/credential/accesstoken.php @@ -1,6 +1,11 @@ get('accesskey'); - } - - /** - * @inheritDoc - */ - public function getAuthentication() - { - return $this->get('token'); - } - - /** - * @inheritDoc - */ - public function getExpiry() - { - return $this->get('valid_until'); - } +class accesstoken extends credential implements credentialInterface, credentialExpiryInterface +{ + + /** + * validator name to be used for validating input data + * @var string|null + */ + protected static $validatorName = 'structure_credential_accesstoken'; + + /** + * {@inheritDoc} + */ + public function getIdentifier(): string + { + return $this->get('accesskey'); + } + + /** + * {@inheritDoc} + */ + public function getAuthentication(): mixed + { + return $this->get('token'); + } + + /** + * {@inheritDoc} + */ + public function getExpiry(): mixed + { + return $this->get('valid_until'); + } } diff --git a/backend/class/helper/context.php b/backend/class/helper/context.php new file mode 100644 index 0000000..2ea9bc5 --- /dev/null +++ b/backend/class/helper/context.php @@ -0,0 +1,399 @@ + $value) { + if ($value === null || $value === '') { + continue; + } + + // skip custom filter + if (str_starts_with($key, '___')) { + continue; + } + + if (array_key_exists($key, $applicableFilter)) { + // apply filter + $applyFilter = $applicableFilter[$key]; + + $diveModel = &$model; + foreach ($applyFilter['structure'] as $modelName) { + if ($diveModel->getIdentifier() == $modelName) { + // + } else { + $nested = $diveModel->getNestedJoins($modelName); + if (count($nested) === 1) { + $diveModel = &$nested[0]->model; + } else { + // error! + } + } + } + + if (!is_array($value)) { + // Fallback to '=' + if (($applyFilter['operator'] ?? false) && $applyFilter['operator'] === 'LIKE') { + if (!str_ends_with($value, '%')) { + $value .= '%'; + } + } + $diveModel->addDefaultFilter($applyFilter['field'], $value, $applyFilter['operator'] ?? '='); + } else { + // Differentiate + $diveModel->addDefaultFilter($applyFilter['field'], $value['value'] ?? $value, $value['operator'] ?? '='); + } + } + } + + } + + /** + * [getModelFilter description] + * @param model $model [description] + * @param array $currentStructure [description] + * @param array $modelFields [description] + * @param array $modelFieldSettings [description] + * @return array [description] + * @throws ReflectionException + * @throws exception + */ + public static function getModelFilter(model $model, array $currentStructure, array $modelFields, array $modelFieldSettings = []): array + { + $filters = []; + + // filter out arrays + $fields = array_filter(array_values($modelFields), function ($item) { + return !is_array($item); + }); + + // check custom filter + foreach ($fields as $field) { + if (!str_starts_with($field, '___')) { + continue; + } + + // custom filters + $id = [$field]; + $idString = implode('.', $id); + $fieldName = app::getTranslate()->translate('DATAFIELD.' . substr($field, 3)); + + $fieldConfig = [ + 'field_ajax' => false, + 'field_class' => 'input', + 'field_datatype' => 'text', + 'field_description' => '', + 'field_fieldtype' => 'input', + 'field_id' => $field, + 'field_multiple' => false, + 'field_name' => $field, + 'field_noninput' => false, + 'field_placeholder' => $fieldName, + 'field_readonly' => false, + 'field_required' => false, + 'field_title' => $fieldName, + 'field_type' => 'input', + 'field_validator' => '', + 'field_value' => null, + ]; + + $filters[$field] = array_merge( + [ + 'structure' => [], + 'filter_identifier' => $id, + 'filter_name' => $idString, + 'model' => $model->getIdentifier(), + 'label' => $fieldName, + 'field' => $field, + 'operator' => null, + 'datatype' => $fieldConfig['field_datatype'], + ], + $modelFieldSettings[$idString] ?? [] + ); + $filters[$field]['field_config'] = array_merge($fieldConfig, $modelFieldSettings[$idString]['field_config'] ?? []); + } + + // get model field filter + foreach ($model->getFields() as $field) { + if (in_array($field, $fields)) { + // determine a type and stuff + + $id = array_merge($currentStructure, [$field]); + $idString = implode('.', $id); + + $datatype = $model->getConfig()->get('datatype>' . $field); + if ($datatype === 'text_timestamp' || $datatype === 'text_date') { + $filters[$idString . '.from'] = array_merge( + [ + 'structure' => $currentStructure, + 'filter_identifier' => array_merge($id, ['from']), + 'filter_name' => $idString . '.from', + 'model' => $model->getIdentifier(), + // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? + // e.g. the 'Customer Person's Lastname'? + 'label' => app::getTranslate()->translate('DATAFIELD.' . $field . '__from'), + 'field' => $field, + 'operator' => '>=', + 'datatype' => $model->getConfig()->get('datatype>' . $field), + ], + $modelFieldSettings[$idString] ?? [] + ); + $filters[$idString . '.from']['field_config'] = self::makeField( + $model, + $field, + array_merge( + [ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field . '__from'), + ], + $modelFieldSettings[$idString]['field_config'] ?? [], + ) + ); + $filters[$idString . '.until'] = array_merge( + [ + 'structure' => $currentStructure, + 'filter_identifier' => array_merge($id, ['until']), + 'filter_name' => $idString . '.until', + 'model' => $model->getIdentifier(), + // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? + // e.g. the 'Customer Person's Lastname'? + 'label' => app::getTranslate()->translate('DATAFIELD.' . $field . '__until'), + 'field' => $field, + 'operator' => '<=', + 'datatype' => $model->getConfig()->get('datatype>' . $field), + ], + $modelFieldSettings[$idString] ?? [] + ); + $filters[$idString . '.until']['field_config'] = self::makeField( + $model, + $field, + array_merge( + [ + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field . '__until'), + ], + $modelFieldSettings[$idString]['field_config'] ?? [], + ) + ); + } else { + $filters[$idString] = array_merge( + [ + 'structure' => $currentStructure, + 'filter_identifier' => $id, + 'filter_name' => $idString, + 'model' => $model->getIdentifier(), + // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? + // e.g. the 'Customer Person's Lastname'? + 'label' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field' => $field, + 'operator' => null, + 'datatype' => $model->getConfig()->get('datatype>' . $field), + ], + $modelFieldSettings[$idString] ?? [] + ); + $filters[$idString]['field_config'] = self::makeField($model, $field, $modelFieldSettings[$idString]['field_config'] ?? []); + } + } + } + + foreach ($modelFields as $key => $value) { + if (is_array($value)) { + // determine model from nested models + $nested = $model->getNestedJoins($key); + foreach ($nested as $join) { + $addFilters = self::getModelFilter($join->model, array_merge($currentStructure, [$key]), $value, $modelFieldSettings); + $filters = array_merge($filters, $addFilters); + } + } + } + + return $filters; + } + + + /** + * Creates the field instance for the given field and adds information to it. + * + * @param model $model [description] + * @param string $field [description] + * @param array $options [description] + * @return field [description] + * @throws ReflectionException + * @throws exception + */ + protected static function makeField(model $model, string $field, array $options = []): field + { + // load model config for simplicity + $modelconfig = $model->config->get(); + + // Error if field not in models + if (!in_array($field, $model->getFields())) { + throw new exception('EXCEPTION_MAKEFIELD_FIELDNOTFOUNDINMODEL', exception::$ERRORLEVEL_ERROR, $field); + } + + // Create a basic formfield array + $fielddata = [ + 'field_id' => $field, + 'field_name' => $field, + 'field_title' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_description' => app::getTranslate()->translate('DATAFIELD.' . $field . '_DESCRIPTION'), + 'field_type' => 'input', + 'field_required' => false, + 'field_placeholder' => app::getTranslate()->translate('DATAFIELD.' . $field), + 'field_multiple' => false, + 'field_readonly' => $options['field_readonly'] ?? false, + ]; + + // Get the displaytype of this field + if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype'])) { + $fielddata['field_type'] = crud::getDisplaytypeStatic($modelconfig['datatype'][$field]); + $fielddata['field_datatype'] = $modelconfig['datatype'][$field]; + } + + if ($fielddata['field_type'] == 'yesno') { + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = '{$element[\'field_name\']}'; + $fielddata['field_valuefield'] = 'field_value'; + + // NOTE: Datatype for this kind of pseudo-boolean field must be null or so + // because the boolean validator really needs a bool. + $fielddata['field_datatype'] = null; + $fielddata['field_elements'] = [ + [ + 'field_value' => true, + 'field_name' => 'Ja', + ], + [ + 'field_value' => false, + 'field_name' => 'Nein', + ], + ]; + } + + // Modify field to be a reference dropdown + if (array_key_exists('foreign', $modelconfig) && array_key_exists($field, $modelconfig['foreign'])) { + if (!app::getValidator('structure_config_modelreference')->isValid($modelconfig['foreign'][$field])) { + throw new exception('EXCEPTION_MAKEFIELD_INVALIDREFERENCEOBJECT', exception::$ERRORLEVEL_ERROR, $modelconfig['foreign'][$field]); + } + + $foreign = $modelconfig['foreign'][$field]; + + $elements = app::getModel($foreign['model'], $foreign['app'] ?? app::getApp()); + + if (array_key_exists('order', $foreign) && is_array($foreign['order'])) { + foreach ($foreign['order'] as $order) { + if (!app::getValidator('structure_config_modelorder')->isValid($order)) { + throw new exception('EXCEPTION_MAKEFIELD_INVALIDORDEROBJECT', exception::$ERRORLEVEL_ERROR, $order); + } + $elements->addOrder($order['field'], $order['direction']); + } + } + + if (array_key_exists('filter', $foreign) && is_array($foreign['filter'])) { + foreach ($foreign['filter'] as $filter) { + if (!app::getValidator('structure_config_modelfilter')->isValid($filter)) { + throw new exception('EXCEPTION_MAKEFIELD_INVALIDFILTEROBJECT', exception::$ERRORLEVEL_ERROR, $filter); + } + if ($filter['field'] == $elements->getIdentifier() . '_flag') { + if ($filter['operator'] == '=') { + $elements->withFlag($elements->config->get('flag>' . $filter['value'])); + } elseif ($filter['operator'] == '!=') { + $elements->withoutFlag($elements->config->get('flag>' . $filter['value'])); + } else { + throw new exception('EXCEPTION_MAKEFIELD_FILTER_FLAG_INVALIDOPERATOR', exception::$ERRORLEVEL_ERROR, $filter); + } + } else { + $elements->addFilter($filter['field'], $filter['value'], $filter['operator']); + } + } + } + + $fielddata['field_type'] = 'select'; + $fielddata['field_displayfield'] = $foreign['display']; + $fielddata['field_valuefield'] = $foreign['key']; + + if ($elements instanceof exposesRemoteApiInterface && isset($foreign['remote_source'])) { + $apiEndpoint = $elements->getExposedApiEndpoint(); + $fielddata['field_remote_source'] = $apiEndpoint; + + $remoteSource = $foreign['remote_source']; + + $filterKeys = []; + foreach ($remoteSource['filter_key'] as $filterKey => $filterData) { + if (is_array($filterData)) { + foreach ($filterData as $filterDataData) { + $filterKeys[$filterKey][$filterDataData] = true; + } + } else { + $filterKeys[$filterData] = true; + } + } + + $fielddata['field_remote_source_filter_key'] = $filterKeys; + $fielddata['field_remote_source_parameter'] = $remoteSource['parameters'] ?? []; + $fielddata['field_remote_source_display_key'] = $remoteSource['display_key'] ?? null; + $fielddata['field_remote_source_links'] = $foreign['remote_source']['links'] ?? []; + $fielddata['field_valuefield'] = $foreign['key']; + $fielddata['field_displayfield'] = $foreign['key']; + } else { + $fielddata['field_elements'] = $elements->search()->getResult(); + } + + if (array_key_exists('datatype', $modelconfig) && array_key_exists($field, $modelconfig['datatype']) && $modelconfig['datatype'][$field] == 'structure') { + $fielddata['field_multiple'] = true; + } + } + + $fielddata = array_replace($fielddata, $options); + + $field = new field($fielddata); + $field->setType('compact'); + + // Add the field to the form + return $field; + } + +} diff --git a/backend/class/model/exposesRemoteApiInterface.php b/backend/class/model/exposesRemoteApiInterface.php index 9724b75..fdfa0a0 100644 --- a/backend/class/model/exposesRemoteApiInterface.php +++ b/backend/class/model/exposesRemoteApiInterface.php @@ -1,16 +1,18 @@ client = $client; - } - - // /** - // * @inheritDoc - // */ - // protected function loadConfig(): \codename\core\config - // { - // return new \codename\core\config([]); - // } - - /** - * @inheritDoc - */ - private function obsoleteInternalQuery(string $query, array $params = array()) - { - $params = []; - - if(count($this->filter) > 0) { - // - // RestCrud-Style - // - // foreach($this->filter as $f) { - // $params['filter'][] = [ - // 'field' => $f->field->get(), - // 'value' => $f->value, - // 'operator' => $f->operator, - // ]; - // } - - // - // RestContext filter/filter_like style - // - foreach($this->filter as $f) { - if($f->operator === '=') { - $params['filter'][$f->field->get()] = $f->value; - } else if($f->operator === 'LIKE') { - $params['filter_like'][$f->field->get()] = $f->value; - } else if($f->operator === '<=') { - $params['filter_lte'][$f->field->get()] = $f->value; - } else if($f->operator === '<') { - $params['filter_lt'][$f->field->get()] = $f->value; - } else if($f->operator === '>') { - $params['filter_gt'][$f->field->get()] = $f->value; - } else if($f->operator === '>=') { - $params['filter_gte'][$f->field->get()] = $f->value; - } - } - } - - $result = $this->client->get($this->config->get('endpoint>query'), $params); - - if($result['success']) { - return $result['data'][$result['data']['data_key'] ?? 'data']; - } else { - throw new exception('EXCEPTION_MODEL_REMOTERESTAPIMODEL_UNSUCCESSFUL', exception::$ERRORLEVEL_ERROR); +abstract class remote extends json implements modelInterface +{ + + /** + * [protected description] + * @var rest + */ + protected rest $client; + + /** + * [setRestClient description] + * @param rest $client [description] + */ + protected function setRestClient(rest $client): void + { + $this->client = $client; } - } } diff --git a/backend/class/model/schemeless/remote/restcontext.php b/backend/class/model/schemeless/remote/restcontext.php index edc12ef..fce7138 100644 --- a/backend/class/model/schemeless/remote/restcontext.php +++ b/backend/class/model/schemeless/remote/restcontext.php @@ -1,59 +1,62 @@ filter) > 0) { - // - // RestContext filter/filter_like style - // - foreach($this->filter as $f) { - if($f->operator === '=') { - $params['filter'][$f->field->get()] = $f->value; - } else if($f->operator === 'LIKE') { - $params['filter_like'][$f->field->get()] = $f->value; - } else if($f->operator === '<=') { - $params['filter_lte'][$f->field->get()] = $f->value; - } else if($f->operator === '<') { - $params['filter_lt'][$f->field->get()] = $f->value; - } else if($f->operator === '>') { - $params['filter_gt'][$f->field->get()] = $f->value; - } else if($f->operator === '>=') { - $params['filter_gte'][$f->field->get()] = $f->value; +abstract class restcontext extends remote +{ + /** + * {@inheritDoc} + */ + protected function internalQuery(string $query, array $params = []): array + { + $params = []; + + if (count($this->filter) > 0) { + // + // RestContext filter/filter_like style + // + foreach ($this->filter as $f) { + if ($f->operator === '=') { + $params['filter'][$f->field->get()] = $f->value; + } elseif ($f->operator === 'LIKE') { + $params['filter_like'][$f->field->get()] = $f->value; + } elseif ($f->operator === '<=') { + $params['filter_lte'][$f->field->get()] = $f->value; + } elseif ($f->operator === '<') { + $params['filter_lt'][$f->field->get()] = $f->value; + } elseif ($f->operator === '>') { + $params['filter_gt'][$f->field->get()] = $f->value; + } elseif ($f->operator === '>=') { + $params['filter_gte'][$f->field->get()] = $f->value; + } + } } - } - } - if(count($this->order) > 0) { - foreach($this->order as $o) { - $params['order'][] = [ - 'field' => $o->field->get(), - 'direction' => $o->direction - ]; - } - } + if (count($this->order) > 0) { + foreach ($this->order as $o) { + $params['order'][] = [ + 'field' => $o->field->get(), + 'direction' => $o->direction, + ]; + } + } - if($this->limit->limit ?? false) { - $params['options']['limit'] = $this->limit->limit; - } + if ($this->limit->limit ?? false) { + $params['options']['limit'] = $this->limit->limit; + } - $result = $this->client->get($this->config->get('endpoint>query'), $params); + $result = $this->client->get($this->config->get('endpoint>query'), $params); - if($result['success']) { - return $result['data'][$result['data']['data_key'] ?? 'data']; - } else { - throw new exception('EXCEPTION_MODEL_SCHEMELESS_REMOTE_RESTCONTEXT_UNSUCCESSFUL', exception::$ERRORLEVEL_ERROR); + if ($result['success']) { + return $result['data'][$result['data']['data_key'] ?? 'data']; + } else { + throw new exception('EXCEPTION_MODEL_SCHEMELESS_REMOTE_RESTCONTEXT_UNSUCCESSFUL', exception::$ERRORLEVEL_ERROR); + } } - } } diff --git a/backend/class/model/schemeless/remote/restcrud.php b/backend/class/model/schemeless/remote/restcrud.php index 075b913..3563c14 100644 --- a/backend/class/model/schemeless/remote/restcrud.php +++ b/backend/class/model/schemeless/remote/restcrud.php @@ -1,39 +1,41 @@ filter) > 0) { - // - // RestCrud-Style - // - foreach($this->filter as $f) { - $params['filter'][] = [ - 'field' => $f->field->get(), - 'value' => $f->value, - 'operator' => $f->operator, - ]; - } - } + if (count($this->filter) > 0) { + // + // RestCrud-Style + // + foreach ($this->filter as $f) { + $params['filter'][] = [ + 'field' => $f->field->get(), + 'value' => $f->value, + 'operator' => $f->operator, + ]; + } + } - $result = $this->client->get($this->config->get('endpoint>query'), $params); + $result = $this->client->get($this->config->get('endpoint>query'), $params); - if($result['success']) { - return $result['data'][$result['data']['data_key'] ?? 'data']; - } else { - throw new exception('EXCEPTION_MODEL_SCHEMELESS_REMOTE_RESTCRUD_UNSUCCESSFUL', exception::$ERRORLEVEL_ERROR); + if ($result['success']) { + return $result['data'][$result['data']['data_key'] ?? 'data']; + } else { + throw new exception('EXCEPTION_MODEL_SCHEMELESS_REMOTE_RESTCRUD_UNSUCCESSFUL', exception::$ERRORLEVEL_ERROR); + } } - } } diff --git a/backend/class/request/json.php b/backend/class/request/json.php index 55ee73f..d549315 100644 --- a/backend/class/request/json.php +++ b/backend/class/request/json.php @@ -1,96 +1,100 @@ datacontainer = new \codename\core\datacontainer(array()); - $this->addData($_GET ?? []); - + protected array $files = []; + /** + * {@inheritDoc} + * @throws exception + */ + public function __construct() + { + parent::__construct(); + $this->addData($_GET ?? []); - // - // NOTE: [CODENAME-446] HTTP Headers should be handled lowercase/case-insensitive - // - $headers = array_change_key_case(getallheaders(), CASE_LOWER); + $data = null; - if(isset($headers['x-content-type']) && $headers['x-content-type'] == 'application/vnd.core.form+json+formdata') { - // - // special request content type defined by us. - // which allows JSON+Formdata (Object data mixed with binary uploads) // - $this->addData(json_decode($_POST['json'], true) ?? []); - $this->addData($_POST['formdata'] ?? []); - // add files? - $this->files = static::normalizeFiles($_FILES)['formdata'] ?? []; - } else if(!empty($_POST) || !empty($_FILES)) { + // NOTE: [CODENAME-446] HTTP Headers should be handled lowercase/case-insensitive // - // "regular" post request - // - $this->files = static::normalizeFiles($_FILES) ?? []; - $this->addData($_POST ?? []); + $headers = array_change_key_case(getallheaders()); + + if (isset($headers['x-content-type']) && $headers['x-content-type'] == 'application/vnd.core.form+json+formdata') { + // + // special request content type defined by us. + // which allows JSON+Formdata (Object data mixed with binary uploads) + // + $this->addData(json_decode($_POST['json'], true) ?? []); + $this->addData($_POST['formdata'] ?? []); + // add files? + $this->files = static::normalizeFiles($_FILES)['formdata'] ?? []; + } elseif (!empty($_POST) || !empty($_FILES)) { + // + // "regular" post request + // + $this->files = static::normalizeFiles($_FILES) ?? []; + $this->addData($_POST ?? []); + + // + // pure json payload parts, if possible? + // + $body = file_get_contents('php://input'); + $data = json_decode($body, true); + $this->addData($data ?? []); + } else { + // + // pure json payload + // as fallback + // + $body = file_get_contents('php://input'); + $data = json_decode($body, true); + $this->addData($data ?? []); + } // - // pure json payload parts, if possible? - // - $body = file_get_contents('php://input'); - $data = json_decode($body, true); - $this->addData($data ?? []); - } else { - // - // pure json payload - // as fallback + // Temporary solution: + // If we're receiving a request that exceed a limit + // defined through server config or php config + // simply kill it with fire and 413. // - $body = file_get_contents('php://input'); - $data = json_decode($body, true); - $this->addData($data ?? []); - } - - // - // Temporary solution: - // If we're receiving a request that exceed a limit - // defined through server config or php config - // simply kill it with fire and 413. - // - if (($_SERVER['REQUEST_METHOD'] === 'POST') + if (($_SERVER['REQUEST_METHOD'] === 'POST') && empty($_POST) && empty($_FILES) && ($data === null || $data === false) && ($_SERVER['CONTENT_LENGTH'] > 0) - ) { - \codename\core\app::getResponse()->setStatus(\codename\core\response::STATUS_REQUEST_SIZE_TOO_LARGE); - \codename\core\app::getResponse()->reset(); - \codename\core\app::getResponse()->pushOutput(); - exit(); - } - - $this->setData('lang', $this->getData('lang') ?? "de_DE"); - return $this; - } - - /** - * files from request - * @var array - */ - protected $files = []; + ) { + $response = app::getResponse(); + if (!($response instanceof response\http)) { + exit(); + } + $response->setStatus(response::STATUS_REQUEST_SIZE_TOO_LARGE); + $response->reset(); + $response->pushOutput(); + exit(); + } - /** - * @inheritDoc - */ - public function getFiles(): array - { - return $this->files; + $this->setData('lang', $this->getData('lang') ?? "de_DE"); + return $this; } /** @@ -101,16 +105,12 @@ public function getFiles(): array * * @param array $files * @return array - * @throws \InvalidArgumentException for unrecognized values + * @throws InvalidArgumentException for unrecognized values */ - public static function normalizeFiles(array $files) + public static function normalizeFiles(array $files): array { $normalized = []; foreach ($files as $key => $value) { - // if ($value instanceof \UploadedFileInterface) { - // $normalized[$key] = $value; - // continue; - // } if (is_array($value) && isset($value['tmp_name'])) { $normalized[$key] = self::createUploadedFileFromSpec($value); continue; @@ -119,7 +119,7 @@ public static function normalizeFiles(array $files) $normalized[$key] = self::normalizeFiles($value); continue; } - throw new \InvalidArgumentException('Invalid value in files specification'); + throw new InvalidArgumentException('Invalid value in files specification'); } return $normalized; } @@ -133,19 +133,12 @@ public static function normalizeFiles(array $files) * @param array $value $_FILES struct * @return array // |UploadedFileInterface */ - private static function createUploadedFileFromSpec(array $value) + private static function createUploadedFileFromSpec(array $value): array { if (is_array($value['tmp_name'])) { return self::normalizeNestedFileSpec($value); } return $value; - // return new UploadedFile( - // $value['tmp_name'], - // $value['size'], - // $value['error'], - // $value['name'], - // $value['type'] - // ); } /** @@ -157,19 +150,27 @@ private static function createUploadedFileFromSpec(array $value) * @param array $files * @return array // UploadedFileInterface[] */ - private static function normalizeNestedFileSpec(array $files = []) + private static function normalizeNestedFileSpec(array $files = []): array { $normalizedFiles = []; foreach (array_keys($files['tmp_name']) as $key) { $spec = [ - 'tmp_name' => $files['tmp_name'][$key], - 'size' => $files['size'][$key], - 'error' => $files['error'][$key], - 'name' => $files['name'][$key], - 'type' => $files['type'][$key], + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + 'name' => $files['name'][$key], + 'type' => $files['type'][$key], ]; $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); } return $normalizedFiles; } + + /** + * {@inheritDoc} + */ + public function getFiles(): array + { + return $this->files; + } } diff --git a/backend/class/response/json.php b/backend/class/response/json.php index 9f8c08f..aff530c 100644 --- a/backend/class/response/json.php +++ b/backend/class/response/json.php @@ -1,190 +1,178 @@ errorstack = new errorstack('error'); - } - - /** - * @inheritDoc - */ - protected function translateStatus() - { - $translate = array( - self::STATUS_SUCCESS => 1, - self::STATUS_INTERNAL_ERROR => 0, - self::STATUS_NOTFOUND => 0, - self::STATUS_UNAUTHENTICATED => 0, - self::STATUS_FORBIDDEN => 0, - self::STATUS_REQUEST_SIZE_TOO_LARGE => 0, - self::STATUS_BAD_REQUEST => 0, - ); - return $translate[$this->status]; - } - - /** - * [getSuccess description] - * @return bool [description] - */ - public function getSuccess() { - return $this->translateStatus(); - } - - /** - * [getErrors description] - * @return array [description] - */ - public function getErrors() : array { - return $this->errorstack->getErrors(); - } - - /** - * [reset description] - * @return \codename\core\response [description] - */ - public function reset(): \codename\core\response { - $this->data = []; - return $this; - } - - /** - * @inheritDoc - */ - public function displayException(\Exception $e) - { - $this->setStatus(self::STATUS_INTERNAL_ERROR); - - // log to stderr - // NOTE: we log twice, as the second one might be killed - // by memory exhaustion - if($e instanceof \codename\core\exception && !is_null($e->info)) { - $info = print_r($e->info, true); - } else { - $info = ''; +class json extends response\json +{ + + /** + * success state + * @var int|bool + */ + protected int|bool $success = 1; + + /** + * [public description] + * @var errorstack + */ + protected errorstack $errorstack; + + /** + * {@inheritDoc} + */ + public function __construct() + { + parent::__construct(); + $this->errorstack = new errorstack('error'); } - error_log("[SAFE ERROR LOG] "."{$e->getMessage()} (Code: {$e->getCode()}) in File: {$e->getFile()}:{$e->getLine()}, Info: {$info}"); - // error_log(print_r($e, true), 0); - - if(defined('CORE_ENVIRONMENT') - // && CORE_ENVIRONMENT != 'production' - ) { - /* echo $formatter->getColoredString("Hicks", 'red') . chr(10); - echo $formatter->getColoredString("{$e->getMessage()} (Code: {$e->getCode()})", 'yellow') . chr(10) . chr(10); - - if($e instanceof \codename\core\exception && !is_null($e->info)) { - echo $formatter->getColoredString("Information", 'cyan') . chr(10); - echo chr(10); - print_r($e->info); - echo chr(10); - } - - echo $formatter->getColoredString("Stacktrace", 'cyan') . chr(10); - echo chr(10); - print_r($e->getTrace()); - echo chr(10);*/ - - // print_r(json_encode($e)); - - $info = null; - if($e instanceof \codename\core\exception && !is_null($e->info)) { - $info = $e->info; - } - - $this->errorstack->addError($e->getMessage(), $e->getCode(), array( - 'info' => $info, - 'trace' => !($e instanceof \codename\core\sensitiveException) ? $e->getTrace() : null - )); - $this->pushOutput(); - - die(); - } else { - // show exception ? + /** + * [reset description] + * @return response [description] + */ + public function reset(): response + { + $this->data = []; + return $this; + } + + /** + * {@inheritDoc} + */ + public function displayException(\Exception $e): void + { + $this->setStatus(self::STATUS_INTERNAL_ERROR); + + // log to stderr + // NOTE: we log twice, as the second one might be killed + // by memory exhaustion + if ($e instanceof exception && !is_null($e->info)) { + $info = print_r($e->info, true); + } else { + $info = ''; + } + + error_log("[SAFE ERROR LOG] " . "{$e->getMessage()} (Code: {$e->getCode()}) in File: {$e->getFile()}:{$e->getLine()}, Info: $info"); + + if (defined('CORE_ENVIRONMENT')) { + $info = null; + if ($e instanceof exception && !is_null($e->info)) { + $info = $e->info; + } + + $this->errorstack->addError($e->getMessage(), $e->getCode(), [ + 'info' => $info, + 'trace' => !($e instanceof sensitiveException) ? $e->getTrace() : null, + ]); + $this->pushOutput(); + + die(); + } else { + // show exception ? + } + + + $this->pushOutput(); } + /** + * {@inheritDoc} + */ + public function pushOutput(): void + { + http_response_code($this->translateStatusToHttpStatus()); - $this->pushOutput(); - } + // Set correct header + $this->setHeader('Content-Type: application/json'); - /** - * @inheritDoc - */ - public function pushOutput() - { - http_response_code($this->translateStatusToHttpStatus()); + $response = [ + 'success' => $this->getSuccess(), + 'data' => $this->getData(), + ]; - // Set correct header - $this->setHeader('Content-Type: application/json'); + if (count($errors = $this->getErrors()) > 0) { + $response['errors'] = $errors; + } - $response = array( - 'success' => $this->getSuccess(), - 'data' => $this->getData() - ); + $json = json_encode($response); + + if (($jsonLastError = json_last_error()) !== JSON_ERROR_NONE) { + $errorResponse = [ + 'success' => 0, + 'errors' => [ + json_last_error_msg(), + ], + ]; + if ($jsonLastError === JSON_ERROR_UTF8) { + $errorResponse['erroneous_data'] = self::utf8ize($this->getData()); + } elseif ($jsonLastError === JSON_ERROR_UNSUPPORTED_TYPE) { + $errorResponse = $response; // simply provide the response data again and try via partial output + $errorResponse['partial_output'] = true; + } + + // enable partial output to overcome recursions and some type errors + $json = json_encode($errorResponse, JSON_PARTIAL_OUTPUT_ON_ERROR); + } + + print_r($json); + } + + /** + * @return bool|int + */ + public function getSuccess(): bool|int + { + return $this->translateStatus(); + } - if(count($errors = $this->getErrors()) > 0) { - $response['errors'] = $errors; + /** + * {@inheritDoc} + */ + protected function translateStatus(): int + { + $translate = [ + self::STATUS_SUCCESS => 1, + self::STATUS_INTERNAL_ERROR => 0, + self::STATUS_NOTFOUND => 0, + self::STATUS_UNAUTHENTICATED => 0, + self::STATUS_FORBIDDEN => 0, + self::STATUS_REQUEST_SIZE_TOO_LARGE => 0, + self::STATUS_BAD_REQUEST => 0, + ]; + return $translate[$this->status]; } - $json = json_encode($response); - - if(($jsonLastError = json_last_error()) !== JSON_ERROR_NONE) { - $errorResponse = [ - 'success' => 0, - 'errors' => [ - json_last_error_msg() - ] - ]; - if($jsonLastError === JSON_ERROR_UTF8) { - $errorResponse['erroneous_data'] = self::utf8ize($this->getData()); - } else if($jsonLastError === JSON_ERROR_UNSUPPORTED_TYPE) { - $errorResponse = $response; // simply provide the response data again and try via partial output - $errorResponse['partial_output'] = true; - } - - // enable partial output to overcome recursions and some type errors - $json = json_encode($errorResponse, JSON_PARTIAL_OUTPUT_ON_ERROR); + /** + * [getErrors description] + * @return array [description] + */ + public function getErrors(): array + { + return $this->errorstack->getErrors(); } - print_r($json); - } - - /** - * [utf8ize description] - * @param [type] $mixed [description] - * @return [type] [description] - */ - protected static function utf8ize( $mixed ) { - if (is_array($mixed)) { - foreach ($mixed as $key => $value) { - $mixed[$key] = self::utf8ize($value); + /** + * @param mixed $mixed + * @return mixed + */ + protected static function utf8ize(mixed $mixed): mixed + { + if (is_array($mixed)) { + foreach ($mixed as $key => $value) { + $mixed[$key] = self::utf8ize($value); + } + } elseif (is_string($mixed)) { + return mb_convert_encoding($mixed, "UTF-8", "UTF-8"); } - } elseif (is_string($mixed)) { - return mb_convert_encoding($mixed, "UTF-8", "UTF-8"); + return $mixed; } - return $mixed; - } } diff --git a/backend/class/validator/structure/credential/accesskey.php b/backend/class/validator/structure/credential/accesskey.php index 92a5648..4d63a8a 100644 --- a/backend/class/validator/structure/credential/accesskey.php +++ b/backend/class/validator/structure/credential/accesskey.php @@ -1,18 +1,22 @@ setHeader('Content-Type: application/json'); print_r(json_encode(app::getResponse()->getData())); diff --git a/phpcs.xml b/phpcs.xml index 73b533d..8644693 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,21 +1,21 @@ - The default CoreFramework coding standard + The default CoreFramework coding standard - - - + + + - - + + - - + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..fa3d97c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,22 @@ + + + + + backend/class/ + + + + + + + + tests + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..a1b8601 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/tests/autoload.php b/tests/autoload.php new file mode 100644 index 0000000..3599e4f --- /dev/null +++ b/tests/autoload.php @@ -0,0 +1,38 @@ +/composer.json + * + * and you need to build a local composer classmap + * that enables the usage of composer's 'autoload-dev' setting + * just for this project + * + * You should not want to do a "composer install" or "composer update" here. + * + */ + +// Default fixed environment for unit tests +const CORE_ENVIRONMENT = 'test'; + +// cross-project autoloader +$globalBootstrap = realpath(__DIR__ . '/../../../../bootstrap-cli.php'); +if (file_exists($globalBootstrap)) { + echo("Including autoloader at " . $globalBootstrap . chr(10)); + require_once $globalBootstrap; +} else { + die("ERROR: No global bootstrap.cli.php found. You might want to initialize your cross-project autoloader using the root composer.json first." . chr(10)); +} + +// local autoloader +$localAutoload = realpath(__DIR__ . '/../vendor/autoload.php'); +if (file_exists($localAutoload)) { + echo("Including autoloader at " . $localAutoload . chr(10)); + require_once $localAutoload; +} else { + die("ERROR: No local vendor/autoloader.php found. Please call \"composer dump-autoload --dev\" in this directory." . chr(10)); +} From 635533ee09a8ede168f4834624a41585ce84468d Mon Sep 17 00:00:00 2001 From: Ralf Date: Fri, 1 Nov 2024 10:33:40 +0100 Subject: [PATCH 2/2] update php 8.1 > 8.3, dependencies, bugfixing and code cleanup --- README.md | 2 +- backend/class/api/rest.php | 8 ++++---- backend/class/app.php | 16 ++++++++-------- backend/class/auth/accesskey.php | 2 +- backend/class/auth/accesstoken.php | 4 ++-- backend/class/context/restApiContext.php | 2 +- backend/class/context/restcrud.php | 7 ++++++- backend/class/helper/context.php | 11 +++++++---- backend/class/request/json.php | 6 +++--- backend/class/response/json.php | 11 ++++++----- composer.json | 2 +- tests/autoload.php | 6 +++--- 12 files changed, 43 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 39af52c..6324d10 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This README would normally document whatever steps are necessary to get your app ### How do I get set up? ### -* Summary of set up +* Summary of setup * Configuration * Dependencies * Database configuration diff --git a/backend/class/api/rest.php b/backend/class/api/rest.php index e02c90b..6c33031 100644 --- a/backend/class/api/rest.php +++ b/backend/class/api/rest.php @@ -43,7 +43,7 @@ abstract class rest extends \codename\core\api\rest */ protected datacontainer $authentication; /** - * Contains configuration of the service provider (host, port, etc) + * Contains configuration of the service provider (host, port, etc.) * @var serviceprovider */ protected serviceprovider $serviceprovider; @@ -61,7 +61,7 @@ abstract class rest extends \codename\core\api\rest /** * What is the request's special secret string? * Many codename API services are relying on a second authentication factor. - * By definition the second factor is dependent from the concrete topic of the request + * By definition, the second factor is dependent on the concrete topic of the request * The given salt is filled with the requested key's name. * So every different key has a different salt. * @internal Will not be transferred unencrypted @@ -70,13 +70,13 @@ abstract class rest extends \codename\core\api\rest protected string $salt = ''; /** * Contains the API service provider's response to the given request - * After retrieving a response from the foreign host, it will be stored here. + * After retrieving a response from the foreign host; it will be stored here. * @var array */ protected mixed $response = ''; /** * Contains the API type. - * Typically, is defined using the name of the foreign service in upper-case characters + * Typically, it is defined using the name of the foreign service in upper-case characters * @example YOURAPITYPE * @var string */ diff --git a/backend/class/app.php b/backend/class/app.php index 2e9da3c..e94c740 100644 --- a/backend/class/app.php +++ b/backend/class/app.php @@ -15,7 +15,7 @@ /** * core app class * for creating restful applications - * based on core framework + * based on the core framework */ class app extends \codename\core\app { @@ -24,7 +24,7 @@ class app extends \codename\core\app * [EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND description] * @var string */ - public const EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND = 'EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND'; + public const string EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND = 'EXCEPTION_DOMETHOD_REQUESTEDMETHODFUNCTIONNOTFOUND'; /** * overrides the app::isRestClient() result, if !== null * @var null|bool @@ -58,7 +58,7 @@ public function __construct() } /** - * returns true, if client is requesting via REST protocol (e.g. no HTML output) + * returns true, if client is requesting via REST protocol (e.g., no HTML output) * @return bool|null */ protected static function isRestClient(): ?bool @@ -67,7 +67,7 @@ protected static function isRestClient(): ?bool return self::$overrideIsRestClient; } else { // - // NOTE: possible bad request behaviour with unknown accept-header which causes a text-exception to occur -> FE output + // NOTE: possible bad request behavior with unknown accept-header which causes a text-exception to occur -> FE output // It is also possible we need to check for lowercase header (http_accept) due to HTTP2 specification // @@ -141,7 +141,7 @@ public static function getEndpointQualifier(): array } $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); - // get rid of the first part of the uri (e.g. host, port, etc.) + // get rid of the first part of the uri (e.g., host, port, etc.) array_shift($endpoints); $ret = []; @@ -188,7 +188,7 @@ protected function mainRun(): void } /** - * performs HTTP-Method based routines + * performs HTTP-Method-based routines * @return \codename\core\app [description] * @throws ReflectionException * @throws exception @@ -212,11 +212,11 @@ protected function doMethod(): \codename\core\app /** * {@inheritDoc} * overridden output method - * omit templating engines and stuff. + * omits templating engines and stuff. */ protected function doOutput(): void { - // Fallback to default output, if client is not a REST client + // Fallback to default output, if a client is not a REST client if (!self::isRestClient()) { parent::doOutput(); return; diff --git a/backend/class/auth/accesskey.php b/backend/class/auth/accesskey.php index 1c13f84..c67cf5b 100644 --- a/backend/class/auth/accesskey.php +++ b/backend/class/auth/accesskey.php @@ -20,7 +20,7 @@ abstract class accesskey extends auth * exception thrown, if a wrong type was passed to authenticate() * @var string */ - public const EXCEPTION_REST_AUTH_ACCESSKEY_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_ACCESSKEY_CREDENTIAL_INVALID'; + public const string EXCEPTION_REST_AUTH_ACCESSKEY_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_ACCESSKEY_CREDENTIAL_INVALID'; /** * {@inheritDoc} diff --git a/backend/class/auth/accesstoken.php b/backend/class/auth/accesstoken.php index 1cd32c1..f863945 100644 --- a/backend/class/auth/accesstoken.php +++ b/backend/class/auth/accesstoken.php @@ -20,13 +20,13 @@ abstract class accesstoken extends auth * exception thrown, if a wrong type was passed to authenticate() * @var string */ - public const EXCEPTION_REST_AUTH_ACCESSTOKEN_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_ACCESSTOKEN_CREDENTIAL_INVALID'; + public const string EXCEPTION_REST_AUTH_ACCESSTOKEN_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_ACCESSTOKEN_CREDENTIAL_INVALID'; /** * [EXCEPTION_REST_AUTH_MAKEHASH_CREDENTIAL_INVALID description] * @var string */ - public const EXCEPTION_REST_AUTH_MAKEHASH_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_MAKEHASH_CREDENTIAL_INVALID'; + public const string EXCEPTION_REST_AUTH_MAKEHASH_CREDENTIAL_INVALID = 'EXCEPTION_REST_AUTH_MAKEHASH_CREDENTIAL_INVALID'; /** * {@inheritDoc} diff --git a/backend/class/context/restApiContext.php b/backend/class/context/restApiContext.php index cd6bebc..fdbf6eb 100644 --- a/backend/class/context/restApiContext.php +++ b/backend/class/context/restApiContext.php @@ -38,7 +38,7 @@ public function run(): void } $endpoints = explode('/', explode('?', $_SERVER['REQUEST_URI'])[0]); - // get rid of the first part of the uri (e.g. host, port, etc.) + // get rid of the first part of the uri (e.g., host, port, etc.) array_shift($endpoints); $shortName = (new ReflectionClass($this))->getShortName(); diff --git a/backend/class/context/restcrud.php b/backend/class/context/restcrud.php index 528f7e0..cf25d63 100644 --- a/backend/class/context/restcrud.php +++ b/backend/class/context/restcrud.php @@ -6,6 +6,7 @@ use codename\core\exception; use codename\core\model; use codename\rest\response\json; +use DateMalformedStringException; use LogicException; use ReflectionException; @@ -18,7 +19,7 @@ abstract class restcrud extends context implements restContextInterface * [EXCEPTION_REST_METHOD_NO_ID_PROVIDED description] * @var string */ - public const EXCEPTION_REST_METHOD_NO_ID_PROVIDED = 'EXCEPTION_REST_METHOD_NO_ID_PROVIDED'; + public const string EXCEPTION_REST_METHOD_NO_ID_PROVIDED = 'EXCEPTION_REST_METHOD_NO_ID_PROVIDED'; /** * Overwrite what model to use in the CRUD generator * @var null|string @@ -59,6 +60,7 @@ public function view_default(): void /** * {@inheritDoc} * @throws ReflectionException + * @throws DateMalformedStringException * @throws exception */ public function method_get(): void @@ -129,6 +131,7 @@ public function method_post(): void /** * {@inheritDoc} + * @throws DateMalformedStringException * @throws ReflectionException * @throws exception */ @@ -153,6 +156,7 @@ public function method_put(): void /** * {@inheritDoc} + * @throws DateMalformedStringException * @throws ReflectionException * @throws exception */ @@ -188,6 +192,7 @@ public function method_options(): void /** * {@inheritDoc} + * @throws DateMalformedStringException * @throws ReflectionException * @throws exception */ diff --git a/backend/class/helper/context.php b/backend/class/helper/context.php index 2ea9bc5..9b0b2be 100644 --- a/backend/class/helper/context.php +++ b/backend/class/helper/context.php @@ -8,6 +8,7 @@ use codename\core\ui\crud; use codename\core\ui\field; use codename\rest\model\exposesRemoteApiInterface; +use DateMalformedStringException; use ReflectionException; /** @@ -27,6 +28,7 @@ public function __construct() * @param array $modelFields * @param array $modelFieldSettings * @return void + * @throws DateMalformedStringException * @throws ReflectionException * @throws exception */ @@ -90,7 +92,6 @@ public static function applicableModelFilter(array $filterData, model &$model, a } } } - } /** @@ -100,6 +101,7 @@ public static function applicableModelFilter(array $filterData, model &$model, a * @param array $modelFields [description] * @param array $modelFieldSettings [description] * @return array [description] + * @throws DateMalformedStringException * @throws ReflectionException * @throws exception */ @@ -175,7 +177,7 @@ public static function getModelFilter(model $model, array $currentStructure, arr 'filter_name' => $idString . '.from', 'model' => $model->getIdentifier(), // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? - // e.g. the 'Customer Person's Lastname'? + // e.g., the 'Customer Person's Lastname'? 'label' => app::getTranslate()->translate('DATAFIELD.' . $field . '__from'), 'field' => $field, 'operator' => '>=', @@ -200,7 +202,7 @@ public static function getModelFilter(model $model, array $currentStructure, arr 'filter_name' => $idString . '.until', 'model' => $model->getIdentifier(), // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? - // e.g. the 'Customer Person's Lastname'? + // e.g., the 'Customer Person's Lastname'? 'label' => app::getTranslate()->translate('DATAFIELD.' . $field . '__until'), 'field' => $field, 'operator' => '<=', @@ -226,7 +228,7 @@ public static function getModelFilter(model $model, array $currentStructure, arr 'filter_name' => $idString, 'model' => $model->getIdentifier(), // TODO: add translation to identify a field in a nested model as we can have multiple occurrences? - // e.g. the 'Customer Person's Lastname'? + // e.g., the 'Customer Person's Lastname'? 'label' => app::getTranslate()->translate('DATAFIELD.' . $field), 'field' => $field, 'operator' => null, @@ -262,6 +264,7 @@ public static function getModelFilter(model $model, array $currentStructure, arr * @param array $options [description] * @return field [description] * @throws ReflectionException + * @throws DateMalformedStringException * @throws exception */ protected static function makeField(model $model, string $field, array $options = []): field diff --git a/backend/class/request/json.php b/backend/class/request/json.php index d549315..e604026 100644 --- a/backend/class/request/json.php +++ b/backend/class/request/json.php @@ -63,7 +63,7 @@ public function __construct() $this->addData($data ?? []); } else { // - // pure json payload + // pure JSON payload // as fallback // $body = file_get_contents('php://input'); @@ -73,9 +73,9 @@ public function __construct() // // Temporary solution: - // If we're receiving a request that exceed a limit + // If we're receiving a request that exceeds a limit // defined through server config or php config - // simply kill it with fire and 413. + // kill it with fire and 413. // if (($_SERVER['REQUEST_METHOD'] === 'POST') && empty($_POST) diff --git a/backend/class/response/json.php b/backend/class/response/json.php index aff530c..e2fdb0d 100644 --- a/backend/class/response/json.php +++ b/backend/class/response/json.php @@ -4,8 +4,8 @@ use codename\core\errorstack; use codename\core\exception; +use codename\core\exception\sensitiveException; use codename\core\response; -use codename\core\sensitiveException; /** * I handle all the data for a JSON response @@ -77,7 +77,7 @@ public function displayException(\Exception $e): void die(); } else { - // show exception ? + // show exception? } @@ -91,7 +91,7 @@ public function pushOutput(): void { http_response_code($this->translateStatusToHttpStatus()); - // Set correct header + // Set the correct header $this->setHeader('Content-Type: application/json'); $response = [ @@ -115,7 +115,7 @@ public function pushOutput(): void if ($jsonLastError === JSON_ERROR_UTF8) { $errorResponse['erroneous_data'] = self::utf8ize($this->getData()); } elseif ($jsonLastError === JSON_ERROR_UNSUPPORTED_TYPE) { - $errorResponse = $response; // simply provide the response data again and try via partial output + $errorResponse = $response; // provide the response data again and try via partial output $errorResponse['partial_output'] = true; } @@ -144,11 +144,12 @@ protected function translateStatus(): int self::STATUS_INTERNAL_ERROR => 0, self::STATUS_NOTFOUND => 0, self::STATUS_UNAUTHENTICATED => 0, + self::STATUS_ACCESS_DENIED => 0, self::STATUS_FORBIDDEN => 0, self::STATUS_REQUEST_SIZE_TOO_LARGE => 0, self::STATUS_BAD_REQUEST => 0, ]; - return $translate[$this->status]; + return $translate[$this->status] ?? 0; } /** diff --git a/composer.json b/composer.json index 00cbd24..b570fe9 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "require": { "codename/core": "*", "codename/core-ui": "*", - "php": "^8.1" + "php": "^8.3" }, "require-dev": { "codename/core-test": "*", diff --git a/tests/autoload.php b/tests/autoload.php index 3599e4f..1be3e2d 100644 --- a/tests/autoload.php +++ b/tests/autoload.php @@ -4,11 +4,11 @@ * This is a per-project autoloading file * For initializing the local project and enabling it for development purposes * - * you need to build up your fullstack autoloading structure + * You need to build up your fullstack autoloading structure * using composer install / composer update - * e.g. for /composer.json + * e.g., for /composer.json * - * and you need to build a local composer classmap + * And you need to build a local composer classmap * that enables the usage of composer's 'autoload-dev' setting * just for this project *