2018-04-11 19:49:35 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Grocy\Controllers;
|
|
|
|
|
2020-09-01 19:59:40 +02:00
|
|
|
use LessQL\Result;
|
2023-05-13 14:43:51 +02:00
|
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
2023-09-01 17:58:02 +02:00
|
|
|
use Slim\Exception\HttpException;
|
2020-09-01 19:59:40 +02:00
|
|
|
|
2018-04-11 19:49:35 +02:00
|
|
|
class BaseApiController extends BaseController
|
|
|
|
{
|
2020-09-01 19:59:40 +02:00
|
|
|
const PATTERN_FIELD = '[A-Za-z_][A-Za-z0-9_]+';
|
2022-05-30 17:20:15 +02:00
|
|
|
const PATTERN_OPERATOR = '!?((>=)|(<=)|=|~|<|>|(§))';
|
2021-11-12 17:52:32 +01:00
|
|
|
const PATTERN_VALUE = '[A-Za-z\p{L}\p{M}0-9*_.$#^| -\\\]+';
|
2020-09-01 19:59:40 +02:00
|
|
|
|
2021-07-16 17:32:08 +02:00
|
|
|
protected $OpenApiSpec = null;
|
2018-04-22 14:25:08 +02:00
|
|
|
|
2023-05-13 14:43:51 +02:00
|
|
|
protected function ApiResponse(Response $response, $data, $cache = false)
|
2018-04-22 14:25:08 +02:00
|
|
|
{
|
2021-06-29 20:24:02 +02:00
|
|
|
if ($cache)
|
|
|
|
{
|
|
|
|
$response = $response->withHeader('Cache-Control', 'max-age=2592000');
|
|
|
|
}
|
|
|
|
|
2021-08-20 21:45:56 +02:00
|
|
|
$response->getBody()->write(json_encode($data));
|
2020-02-11 17:42:03 +01:00
|
|
|
return $response;
|
2018-04-22 14:25:08 +02:00
|
|
|
}
|
|
|
|
|
2023-05-13 14:43:51 +02:00
|
|
|
protected function EmptyApiResponse(Response $response, $status = 204)
|
2019-01-19 14:51:51 +01:00
|
|
|
{
|
|
|
|
return $response->withStatus($status);
|
|
|
|
}
|
|
|
|
|
2023-05-13 14:43:51 +02:00
|
|
|
protected function GenericErrorResponse(Response $response, $errorMessage, $status = 400)
|
2018-04-11 19:49:35 +02:00
|
|
|
{
|
2020-08-31 20:40:31 +02:00
|
|
|
return $response->withStatus($status)->withJson([
|
2018-04-22 14:25:08 +02:00
|
|
|
'error_message' => $errorMessage
|
2020-08-31 20:40:31 +02:00
|
|
|
]);
|
2018-04-11 19:49:35 +02:00
|
|
|
}
|
2020-08-31 20:40:31 +02:00
|
|
|
|
2023-05-13 14:43:51 +02:00
|
|
|
public function FilteredApiResponse(Response $response, Result $data, array $query)
|
2020-09-01 19:59:40 +02:00
|
|
|
{
|
|
|
|
$data = $this->queryData($data, $query);
|
|
|
|
return $this->ApiResponse($response, $data);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function queryData(Result $data, array $query)
|
|
|
|
{
|
|
|
|
if (isset($query['query']))
|
2020-09-01 21:29:47 +02:00
|
|
|
{
|
2020-09-01 19:59:40 +02:00
|
|
|
$data = $this->filter($data, $query['query']);
|
2020-09-01 21:29:47 +02:00
|
|
|
}
|
2020-12-09 21:04:04 +01:00
|
|
|
|
2023-01-12 13:32:12 +01:00
|
|
|
if (isset($query['limit']) || isset($query['offset']))
|
2020-09-01 21:29:47 +02:00
|
|
|
{
|
2023-01-12 13:32:12 +01:00
|
|
|
if (!isset($query['limit']))
|
|
|
|
{
|
|
|
|
$query['limit'] = -1;
|
|
|
|
}
|
|
|
|
|
2020-09-01 19:59:40 +02:00
|
|
|
$data = $data->limit(intval($query['limit']), intval($query['offset'] ?? 0));
|
2020-09-01 21:29:47 +02:00
|
|
|
}
|
2020-12-09 21:04:04 +01:00
|
|
|
|
2020-09-01 19:59:40 +02:00
|
|
|
if (isset($query['order']))
|
2020-09-01 21:29:47 +02:00
|
|
|
{
|
2020-12-09 21:04:04 +01:00
|
|
|
$parts = explode(':', $query['order']);
|
|
|
|
|
|
|
|
if (count($parts) == 1)
|
|
|
|
{
|
|
|
|
$data = $data->orderBy($parts[0]);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if ($parts[1] != 'asc' && $parts[1] != 'desc')
|
|
|
|
{
|
|
|
|
throw new \Exception('Invalid sort order ' . $parts[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = $data->orderBy($parts[0], $parts[1]);
|
|
|
|
}
|
2020-09-01 21:29:47 +02:00
|
|
|
}
|
2020-12-09 21:04:04 +01:00
|
|
|
|
2020-09-01 19:59:40 +02:00
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function filter(Result $data, array $query): Result
|
|
|
|
{
|
2020-09-01 21:29:47 +02:00
|
|
|
foreach ($query as $q)
|
|
|
|
{
|
|
|
|
$matches = [];
|
|
|
|
preg_match(
|
|
|
|
'/(?P<field>' . self::PATTERN_FIELD . ')'
|
2020-09-01 19:59:40 +02:00
|
|
|
. '(?P<op>' . self::PATTERN_OPERATOR . ')'
|
2021-07-09 20:23:30 +02:00
|
|
|
. '(?P<value>' . self::PATTERN_VALUE . ')/u',
|
2020-09-01 21:29:47 +02:00
|
|
|
$q,
|
|
|
|
$matches
|
2020-09-01 19:59:40 +02:00
|
|
|
);
|
2020-12-11 19:32:08 +01:00
|
|
|
|
|
|
|
if (!array_key_exists('field', $matches) || !array_key_exists('op', $matches) || !array_key_exists('value', $matches))
|
|
|
|
{
|
|
|
|
throw new \Exception('Invalid query');
|
|
|
|
}
|
|
|
|
|
2020-12-16 21:52:24 +01:00
|
|
|
$sqlOrNull = '';
|
2020-12-11 19:32:08 +01:00
|
|
|
if (strtolower($matches['value']) == 'null')
|
|
|
|
{
|
2020-12-16 21:52:24 +01:00
|
|
|
$sqlOrNull = ' OR ' . $matches['field'] . ' IS NULL';
|
2020-12-11 19:32:08 +01:00
|
|
|
}
|
|
|
|
|
2023-01-12 13:32:12 +01:00
|
|
|
switch ($matches['op'])
|
|
|
|
{
|
2020-09-01 19:59:40 +02:00
|
|
|
case '=':
|
2020-12-16 21:52:24 +01:00
|
|
|
$data = $data->where($matches['field'] . ' = ?' . $sqlOrNull, $matches['value']);
|
2020-09-01 19:59:40 +02:00
|
|
|
break;
|
|
|
|
case '!=':
|
2020-12-16 21:52:24 +01:00
|
|
|
$data = $data->where($matches['field'] . ' != ?' . $sqlOrNull, $matches['value']);
|
2020-09-01 19:59:40 +02:00
|
|
|
break;
|
|
|
|
case '~':
|
|
|
|
$data = $data->where($matches['field'] . ' LIKE ?', '%' . $matches['value'] . '%');
|
|
|
|
break;
|
|
|
|
case '!~':
|
|
|
|
$data = $data->where($matches['field'] . ' NOT LIKE ?', '%' . $matches['value'] . '%');
|
|
|
|
break;
|
|
|
|
case '<':
|
|
|
|
$data = $data->where($matches['field'] . ' < ?', $matches['value']);
|
|
|
|
break;
|
|
|
|
case '>':
|
|
|
|
$data = $data->where($matches['field'] . ' > ?', $matches['value']);
|
|
|
|
break;
|
|
|
|
case '>=':
|
|
|
|
$data = $data->where($matches['field'] . ' >= ?', $matches['value']);
|
|
|
|
break;
|
|
|
|
case '<=':
|
|
|
|
$data = $data->where($matches['field'] . ' <= ?', $matches['value']);
|
|
|
|
break;
|
2020-12-12 10:44:27 +01:00
|
|
|
case '§':
|
|
|
|
$data = $data->where($matches['field'] . ' REGEXP ?', $matches['value']);
|
|
|
|
break;
|
2020-09-01 19:59:40 +02:00
|
|
|
}
|
|
|
|
}
|
2020-12-11 19:32:08 +01:00
|
|
|
|
2020-09-01 19:59:40 +02:00
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2020-08-31 20:40:31 +02:00
|
|
|
protected function getOpenApispec()
|
|
|
|
{
|
|
|
|
if ($this->OpenApiSpec == null)
|
|
|
|
{
|
|
|
|
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->OpenApiSpec;
|
|
|
|
}
|
2023-09-01 17:58:02 +02:00
|
|
|
|
|
|
|
private static $htmlPurifierInstance = null;
|
|
|
|
protected function GetParsedAndFilteredRequestBody($request)
|
|
|
|
{
|
|
|
|
if ($request->getHeaderLine('Content-Type') != 'application/json')
|
|
|
|
{
|
|
|
|
throw new HttpException($request, 'Bad Content-Type', 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$htmlPurifierInstance == null)
|
|
|
|
{
|
|
|
|
$htmlPurifierConfig = \HTMLPurifier_Config::createDefault();
|
|
|
|
$htmlPurifierConfig->set('Cache.SerializerPath', GROCY_DATAPATH . '/viewcache');
|
|
|
|
$htmlPurifierConfig->set('HTML.Allowed', 'div,b,strong,i,em,u,a[href|title|target],iframe[src|width|height|frameborder],ul,ol,li,p[style],br,span[style],img[style|width|height|alt|src],table[border|width|style],tbody,tr,td,th,blockquote,*[style|class|id],h1,h2,h3,h4,h5,h6');
|
|
|
|
$htmlPurifierConfig->set('Attr.EnableID', true);
|
|
|
|
$htmlPurifierConfig->set('HTML.SafeIframe', true);
|
|
|
|
$htmlPurifierConfig->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,width,height');
|
|
|
|
$htmlPurifierConfig->set('URI.AllowedSchemes', ['data' => true, 'http' => true, 'https' => true]);
|
|
|
|
$htmlPurifierConfig->set('URI.SafeIframeRegexp', '%^.*%'); // Allow any iframe source
|
|
|
|
$htmlPurifierConfig->set('CSS.MaxImgLength', null);
|
|
|
|
|
|
|
|
self::$htmlPurifierInstance = new \HTMLPurifier($htmlPurifierConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
$requestBody = $request->getParsedBody();
|
|
|
|
foreach ($requestBody as $key => &$value)
|
|
|
|
{
|
|
|
|
// HTMLPurifier removes boolean values (true/false) and arrays, so explicitly keep them
|
|
|
|
// Maybe also possible through HTMLPurifier config (http://htmlpurifier.org/live/configdoc/plain.html)
|
|
|
|
if (!is_bool($value) && !is_array($value))
|
|
|
|
{
|
|
|
|
$value = self::$htmlPurifierInstance->purify($value);
|
|
|
|
|
|
|
|
// Allow some special chars
|
|
|
|
// Maybe also possible through HTMLPurifier config (http://htmlpurifier.org/live/configdoc/plain.html)
|
|
|
|
$value = str_replace('&', '&', $value);
|
|
|
|
$value = str_replace('>', '>', $value);
|
|
|
|
$value = str_replace('<', '<', $value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $requestBody;
|
|
|
|
}
|
2018-04-11 19:49:35 +02:00
|
|
|
}
|