diff --git a/config/Middlewares.php b/config/Middlewares.php index 9eff9fa3..7b45176d 100644 --- a/config/Middlewares.php +++ b/config/Middlewares.php @@ -3,7 +3,9 @@ declare(strict_types=1); use App\Libs\Middlewares\APIKeyRequiredMiddleware; +use App\Libs\Middlewares\ParseJsonBodyMiddleware; return static fn(): array => [ fn() => new APIKeyRequiredMiddleware(), + fn() => new ParseJsonBodyMiddleware(), ]; diff --git a/src/API/Backends/Index.php b/src/API/Backends/Index.php index 916ad3c1..81f6084e 100644 --- a/src/API/Backends/Index.php +++ b/src/API/Backends/Index.php @@ -6,23 +6,24 @@ namespace App\API\Backends; use App\Libs\Attributes\Route\Get; use App\Libs\Config; +use App\Libs\ConfigFile; use App\Libs\HTTP_STATUS; use App\Libs\Options; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface as iResponse; +use Psr\Http\Message\ServerRequestInterface as iRequest; -#[Get(self::URL . '[/]', name: 'backends.index')] final class Index { - public const URL = '%{api.prefix}/backends'; + public const string URL = '%{api.prefix}/backends'; - public const BLACK_LIST = [ + public const array BLACK_LIST = [ 'token', 'webhook.token', 'options.' . Options::ADMIN_TOKEN ]; - public function __invoke(ServerRequestInterface $request, array $args = []): ResponseInterface + #[Get(self::URL . '[/]', name: 'backends.index')] + public function backendsIndex(iRequest $request): iResponse { $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); $urlPath = $request->getUri()->getPath(); @@ -34,7 +35,7 @@ final class Index ], ]; - foreach (self::getBackends(blacklist: true) as $backend) { + foreach (self::getBackends() as $backend) { $backend = array_filter( $backend, fn($key) => false === in_array($key, ['options', 'webhook'], true), @@ -51,19 +52,38 @@ final class Index return api_response(HTTP_STATUS::HTTP_OK, $response); } - public static function getBackends(string|null $name = null, bool $blacklist = false): array + #[Get(self::URL . '/{id:[a-zA-Z0-9_-]+}[/]', name: 'backends.view')] + public function backendsView(iRequest $request, array $args = []): iResponse + { + if (null === ($id = ag($args, 'id'))) { + return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST); + } + + $data = Index::getBackends(name: $id); + if (empty($data)) { + return api_error('Backend not found.', HTTP_STATUS::HTTP_NOT_FOUND); + } + + $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); + $data = array_pop($data); + + $response = [ + ...$data, + 'links' => [ + 'self' => (string)$apiUrl, + 'list' => (string)$apiUrl->withPath(parseConfigValue(self::URL)), + ], + ]; + + return api_response(HTTP_STATUS::HTTP_OK, ['backend' => $response]); + } + + private function getBackends(string|null $name = null): array { $backends = []; - foreach (Config::get('servers', []) as $backendName => $backend) { - $backend = ['name' => $backendName, ...$backend]; - if (true === $blacklist) { - foreach (self::BLACK_LIST as $hideValue) { - if (true === ag_exists($backend, $hideValue)) { - $backend = ag_set($backend, $hideValue, '__hidden__'); - } - } - } + foreach (ConfigFile::open(Config::get('backends_file'), 'yaml')->getAll() as $backendName => $backend) { + $backend = ['name' => $backendName, ...$backend]; if (null !== ag($backend, 'import.lastSync')) { $backend = ag_set($backend, 'import.lastSync', makeDate(ag($backend, 'import.lastSync'))); diff --git a/src/API/Backends/Plex/Discover.php b/src/API/Backends/Plex/Discover.php new file mode 100644 index 00000000..fd288181 --- /dev/null +++ b/src/API/Backends/Plex/Discover.php @@ -0,0 +1,40 @@ +getParsedBody()); + + if (null === ($token = $data->get('token'))) { + return api_error('No token was given.', HTTP_STATUS::HTTP_BAD_REQUEST); + } + + try { + $list = PlexClient::discover($this->http, $token); + } catch (\Throwable $e) { + return api_error($e->getMessage(), HTTP_STATUS::HTTP_INTERNAL_SERVER_ERROR); + } + + return api_response(HTTP_STATUS::HTTP_OK, ag($list, 'list', [])); + } +} diff --git a/src/API/Backends/View.php b/src/API/Backends/View.php deleted file mode 100644 index 5705dd5f..00000000 --- a/src/API/Backends/View.php +++ /dev/null @@ -1,42 +0,0 @@ -getUri()->withHost('')->withPort(0)->withScheme(''); - $data = array_pop($data); - - $response = [ - ...$data, - 'links' => [ - 'self' => (string)$apiUrl, - 'list' => (string)$apiUrl->withPath(parseConfigValue(self::URL)), - ], - ]; - - return api_response(HTTP_STATUS::HTTP_OK, ['backend' => $response]); - } - -} diff --git a/src/API/History/Index.php b/src/API/History/Index.php index e310a987..41ff160e 100644 --- a/src/API/History/Index.php +++ b/src/API/History/Index.php @@ -14,13 +14,12 @@ use App\Libs\Guid; use App\Libs\HTTP_STATUS; use App\Libs\Uri; use PDO; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface as iResponse; +use Psr\Http\Message\ServerRequestInterface as iRequest; -#[Get(self::URL . '[/]', name: 'history.index')] final class Index { - public const URL = '%{api.prefix}/history'; + public const string URL = '%{api.prefix}/history'; private PDO $pdo; public function __construct(private readonly iDB $db) @@ -28,7 +27,8 @@ final class Index $this->pdo = $this->db->getPDO(); } - public function __invoke(ServerRequestInterface $request, array $args = []): ResponseInterface + #[Get(self::URL . '[/]', name: 'history.index')] + public function historyIndex(iRequest $request): iResponse { $es = fn(string $val) => $this->db->identifier($val); $data = DataUtil::fromArray($request->getQueryParams()); @@ -316,4 +316,36 @@ final class Index return api_response(HTTP_STATUS::HTTP_OK, $response); } + + #[Get(self::URL . '/{id:\d+}[/]', name: 'history.view')] + public function historyView(iRequest $request, array $args = []): iResponse + { + if (null === ($id = ag($args, 'id'))) { + return api_error('Invalid value for id path parameter.', HTTP_STATUS::HTTP_BAD_REQUEST); + } + + $entity = Container::get(iState::class)::fromArray([iState::COLUMN_ID => $id]); + + if (null === ($item = $this->db->get($entity))) { + return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND); + } + + $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); + + $item = $item->getAll(); + + $item[iState::COLUMN_WATCHED] = $entity->isWatched(); + $item[iState::COLUMN_UPDATED] = makeDate($entity->updated); + + $item = [ + ...$item, + 'links' => [ + 'self' => (string)$apiUrl, + 'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)), + ], + ]; + + return api_response(HTTP_STATUS::HTTP_OK, ['history' => $item]); + } + } diff --git a/src/API/History/View.php b/src/API/History/View.php deleted file mode 100644 index 13a1fd84..00000000 --- a/src/API/History/View.php +++ /dev/null @@ -1,51 +0,0 @@ - $id]); - - if (null === ($item = $this->db->get($entity))) { - return api_error('Not found', HTTP_STATUS::HTTP_NOT_FOUND); - } - - $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); - - $item = $item->getAll(); - - $item[iState::COLUMN_WATCHED] = $entity->isWatched(); - $item[iState::COLUMN_UPDATED] = makeDate($entity->updated); - - $item = [ - ...$item, - 'links' => [ - 'self' => (string)$apiUrl, - 'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)), - ], - ]; - - return api_response(HTTP_STATUS::HTTP_OK, ['history' => $item]); - } -} diff --git a/src/API/System/Env.php b/src/API/System/Env.php index 5753fe99..9aca3a82 100644 --- a/src/API/System/Env.php +++ b/src/API/System/Env.php @@ -14,11 +14,13 @@ use Throwable; #[Get(self::URL . '[/]', name: 'system.env')] final class Env { - public const URL = '%{api.prefix}/system/env'; - private const BLACKLIST = [ + public const string URL = '%{api.prefix}/system/env'; + + private const array BLACKLIST = [ 'WS_API_KEY' ]; - private const BLACKLIST_PARSE_URL = [ + + private const array BLACKLIST_PARSE_URL = [ 'WS_CACHE_URL' => [ 'password', ], diff --git a/src/API/Tasks/Index.php b/src/API/Tasks/Index.php index 8aabf20e..8a7fa7cd 100644 --- a/src/API/Tasks/Index.php +++ b/src/API/Tasks/Index.php @@ -7,15 +7,15 @@ namespace App\API\Tasks; use App\Commands\System\TasksCommand; use App\Libs\Attributes\Route\Get; use App\Libs\HTTP_STATUS; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\ResponseInterface as iResponse; +use Psr\Http\Message\ServerRequestInterface as iRequest; -#[Get(self::URL . '[/]', name: 'tasks.index')] final class Index { - public const URL = '%{api.prefix}/tasks'; + public const string URL = '%{api.prefix}/tasks'; - public function __invoke(ServerRequestInterface $request, array $args = []): ResponseInterface + #[Get(self::URL . '[/]', name: 'tasks.index')] + public function tasksIndex(iRequest $request): iResponse { $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); $urlPath = rtrim($request->getUri()->getPath(), '/'); @@ -44,14 +44,40 @@ final class Index return api_response(HTTP_STATUS::HTTP_OK, $response); } - public static function formatTask(array $task): array + #[Get(self::URL . '/{id:[a-zA-Z0-9_-]+}[/]', name: 'tasks.view')] + public function __invoke(iRequest $request, array $args = []): iResponse + { + if (null === ($id = ag($args, 'id'))) { + return api_error('No id was given.', HTTP_STATUS::HTTP_BAD_REQUEST); + } + + $apiUrl = $request->getUri()->withHost('')->withPort(0)->withScheme(''); + + $task = TasksCommand::getTasks($id); + + if (empty($task)) { + return api_error('Task not found.', HTTP_STATUS::HTTP_NOT_FOUND); + } + + $response = [ + ...Index::formatTask($task), + 'links' => [ + 'self' => (string)$apiUrl, + 'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)), + ], + ]; + + return api_response(HTTP_STATUS::HTTP_OK, ['task' => $response]); + } + + private function formatTask(array $task): array { $isEnabled = (bool)ag($task, 'enabled', false); $item = [ 'name' => ag($task, 'name'), 'description' => ag($task, 'description'), - 'enabled' => $isEnabled, + 'enabled' => true === $isEnabled, 'timer' => ag($task, 'timer')->getexpression(), 'next_run' => null, 'prev_run' => null, @@ -63,7 +89,7 @@ final class Index $item['command'] = get_debug_type($item['command']); } - if ($isEnabled) { + if (true === $isEnabled) { $item['next_run'] = makeDate(ag($task, 'timer')->getNextRunDate()); $item['prev_run'] = makeDate(ag($task, 'timer')->getPreviousRunDate()); } diff --git a/src/API/Tasks/View.php b/src/API/Tasks/View.php deleted file mode 100644 index 930ab520..00000000 --- a/src/API/Tasks/View.php +++ /dev/null @@ -1,40 +0,0 @@ -getUri()->withHost('')->withPort(0)->withScheme(''); - - $task = TasksCommand::getTasks($id); - - if (empty($task)) { - return api_error('Task not found.', HTTP_STATUS::HTTP_NOT_FOUND); - } - - $response = [ - ...Index::formatTask($task), - 'links' => [ - 'self' => (string)$apiUrl, - 'list' => (string)$apiUrl->withPath(parseConfigValue(Index::URL)), - ], - ]; - - return api_response(HTTP_STATUS::HTTP_OK, ['task' => $response]); - } -} diff --git a/src/Libs/Middlewares/APIKeyRequiredMiddleware.php b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php index be0aded5..f37e7f92 100644 --- a/src/Libs/Middlewares/APIKeyRequiredMiddleware.php +++ b/src/Libs/Middlewares/APIKeyRequiredMiddleware.php @@ -14,7 +14,7 @@ use Random\RandomException; final class APIKeyRequiredMiddleware implements MiddlewareInterface { - public const KEY_NAME = 'apikey'; + public const string KEY_NAME = 'apikey'; /** * @throws RandomException if random_bytes() fails diff --git a/src/Libs/Middlewares/ParseJsonBodyMiddleware.php b/src/Libs/Middlewares/ParseJsonBodyMiddleware.php new file mode 100644 index 00000000..0c0c5253 --- /dev/null +++ b/src/Libs/Middlewares/ParseJsonBodyMiddleware.php @@ -0,0 +1,52 @@ +getMethod(), $this->nonBodyRequests)) { + return $handler->handle($request); + } + + $header = $request->getHeaderLine('Content-Type'); + + if (1 === preg_match('#^application/(|\S+\+)json($|[ ;])#', $header)) { + return $handler->handle($this->parse($request)); + } + + return $handler->handle($request); + } + + private function parse(ServerRequestInterface $request): ServerRequestInterface + { + $rawBody = (string)$request->getBody(); + + if (empty($rawBody)) { + return $request->withAttribute('rawBody', $rawBody)->withParsedBody(null); + } + + $parsedBody = json_decode($rawBody, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new RuntimeException(sprintf('Error when parsing JSON request body: %s', json_last_error_msg())); + } + + return $request->withAttribute('rawBody', $rawBody)->withParsedBody($parsedBody); + } +}