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/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 dfdc261..6c33031 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 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
* @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, it 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..e94c740 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 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
+ //
+
+ 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
+ * omits templating engines and stuff.
+ */
+ protected function doOutput(): void
+ {
+ // Fallback to default output, if a 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..c67cf5b 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..fdbf6eb 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 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
+ */
+ 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 DateMalformedStringException
+ * @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 DateMalformedStringException
+ * @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 DateMalformedStringException
+ * @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 DateMalformedStringException
+ * @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..9b0b2be
--- /dev/null
+++ b/backend/class/helper/context.php
@@ -0,0 +1,402 @@
+ $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 DateMalformedStringException
+ * @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 DateMalformedStringException
+ * @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..e604026 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 exceeds a limit
+ // defined through server config or php config
+ // 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..e2fdb0d 100644
--- a/backend/class/response/json.php
+++ b/backend/class/response/json.php
@@ -1,190 +1,179 @@
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 the 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; // 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_ACCESS_DENIED => 0,
+ self::STATUS_FORBIDDEN => 0,
+ self::STATUS_REQUEST_SIZE_TOO_LARGE => 0,
+ self::STATUS_BAD_REQUEST => 0,
+ ];
+ return $translate[$this->status] ?? 0;
}
- $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..1be3e2d
--- /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));
+}