From 0bfa1634f94da2fbc7c2df34a5f4f31a61245065 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Thu, 11 Apr 2024 15:21:18 +0300 Subject: [PATCH 01/14] ExApp occ commands impl (1) Signed-off-by: Andrey Borysenko --- CHANGELOG.md | 1 + appinfo/register_command.php | 32 +++ appinfo/routes.php | 5 + docs/tech_details/api/index.rst | 1 + docs/tech_details/api/occ_command.rst | 60 +++++ lib/Controller/OccCommandController.php | 72 ++++++ lib/Db/Console/ExAppOccCommand.php | 97 ++++++++ lib/Db/Console/ExAppOccCommandMapper.php | 63 +++++ .../Version2205Date20240411124836.php | 77 ++++++ lib/Service/ExAppOccService.php | 221 ++++++++++++++++++ 10 files changed, 629 insertions(+) create mode 100644 appinfo/register_command.php create mode 100644 docs/tech_details/api/occ_command.rst create mode 100644 lib/Controller/OccCommandController.php create mode 100644 lib/Db/Console/ExAppOccCommand.php create mode 100644 lib/Db/Console/ExAppOccCommandMapper.php create mode 100644 lib/Migration/Version2205Date20240411124836.php create mode 100644 lib/Service/ExAppOccService.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 699307ad..af804a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Ability to add optional parameters when registering a daemon, for example *OVERRIDE_APP_HOST*. #269 - Correct support of the Docker `HEALTHCHECK` instruction. #273 - Support of pulling "custom" images for the selected compute device. #274 +- API for registering OCC commands. ### Fixed diff --git a/appinfo/register_command.php b/appinfo/register_command.php new file mode 100644 index 00000000..82874b81 --- /dev/null +++ b/appinfo/register_command.php @@ -0,0 +1,32 @@ +getSystemValueBool('installed', false)) { + $exAppOccService = Server::get(ExAppOccService::class); + /** + * @var ExAppOccCommand $occCommand + * @var SymfonyApplication $application + */ + foreach ($exAppOccService->getOccCommands() as $occCommand) { + $application->add($exAppOccService->buildCommand( + $occCommand, + $serverContainer + )); + } + } +} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { +} diff --git a/appinfo/routes.php b/appinfo/routes.php index 786df079..dc1835af 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -80,6 +80,11 @@ ['name' => 'EventsListener#unregisterListener', 'url' => '/api/v1/events_listener', 'verb' => 'DELETE'], ['name' => 'EventsListener#getListener', 'url' => '/api/v1/events_listener', 'verb' => 'GET'], + // Commands + ['name' => 'OccCommand#registerCommand', 'url' => '/api/v1/occ_command', 'verb' => 'POST'], + ['name' => 'OccCommand#unregisterCommand', 'url' => '/api/v1/occ_command', 'verb' => 'DELETE'], + ['name' => 'OccCommand#getCommand', 'url' => '/api/v1/occ_command', 'verb' => 'GET'], + // Talk bots ['name' => 'TalkBot#registerExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'POST'], ['name' => 'TalkBot#unregisterExAppTalkBot', 'url' => '/api/v1/talk_bot', 'verb' => 'DELETE'], diff --git a/docs/tech_details/api/index.rst b/docs/tech_details/api/index.rst index b0eadb6a..86d5a88d 100644 --- a/docs/tech_details/api/index.rst +++ b/docs/tech_details/api/index.rst @@ -19,6 +19,7 @@ AppAPI Nextcloud APIs settings notifications events_listener + occ_command talkbots speechtotext textprocessing diff --git a/docs/tech_details/api/occ_command.rst b/docs/tech_details/api/occ_command.rst new file mode 100644 index 00000000..a43863b7 --- /dev/null +++ b/docs/tech_details/api/occ_command.rst @@ -0,0 +1,60 @@ +.. _occ_command: + +=========== +OCC Command +=========== + +This API allows you to register the occ (CLI) commands. +The principal is similar to the regular Nextcloud OCC command for PHP apps, that are working in context of the Nextcloud instance, +but for ExApps it is a trigger via Nextcloud OCC interface to perform some action on the External App side. + + +Register +^^^^^^^^ + +OCS endpoint: ``POST /apps/app_api/api/v1/occ_command`` + +Params +****** + +.. code-block:: json + + { + "name": "appid:unique:command:name", + "description": "Description of the command", + "hidden": "true/false", + "arguments": [ + { + "name": "argument_name", + "mode": "required (InputArgument::REQUIRED)/optional(InputArgument::OPTIONAL)/array(InputArgument::IS_ARRAY)", + "description": "Description of the argument", + "default": "default_value" + } + ], + "options": [ + { + "name": "option_name", + "shortcut": "shortcut", + "mode": "value_required(InputOption::VALUE_REQUIRED)/value_optional(InputOption::VALUE_OPTIONAL)/value_none(InputOption::VALUE_NONE)/array(InputOption::VALUE_IS_ARRAY)/negatable(InputOption::VALUE_NEGATABLE)", + "description": "Description of the option", + "default": "default_value" + } + ], + "execute_handler": "handler_route" + } + +Unregister +^^^^^^^^^^ + +OCS endpoint: ``DELETE /apps/app_api/api/v1/occ_command`` + +Params +****** + +To unregister OCC Command, you just need to provide a command `name`: + +.. code-block:: json + + { + "name": "occ_command_name" + } diff --git a/lib/Controller/OccCommandController.php b/lib/Controller/OccCommandController.php new file mode 100644 index 00000000..130bd019 --- /dev/null +++ b/lib/Controller/OccCommandController.php @@ -0,0 +1,72 @@ +request = $request; + } + + #[NoCSRFRequired] + #[PublicPage] + #[AppAPIAuth] + public function registerCommand( + string $name, + string $description, + bool $hidden, + array $arguments, + array $options, + array $usages, + string $execute_handler + ): DataResponse { + $command = $this->service->registerCommand( + $this->request->getHeader('EX-APP-ID'), $name, + $description, $hidden, $arguments, $options, $usages, $execute_handler + ); + if ($command === null) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + return new DataResponse(); + } + + #[NoCSRFRequired] + #[PublicPage] + #[AppAPIAuth] + public function unregisterCommand(string $name): DataResponse { + $unregistered = $this->service->unregisterCommand($this->request->getHeader('EX-APP-ID'), $name); + if (!$unregistered) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + return new DataResponse(); + } + + #[AppAPIAuth] + #[PublicPage] + #[NoCSRFRequired] + public function getCommand(string $name): DataResponse { + $result = $this->service->getOccCommand($this->request->getHeader('EX-APP-ID'), $name); + if (!$result) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + return new DataResponse($result, Http::STATUS_OK); + } +} diff --git a/lib/Db/Console/ExAppOccCommand.php b/lib/Db/Console/ExAppOccCommand.php new file mode 100644 index 00000000..37b245a5 --- /dev/null +++ b/lib/Db/Console/ExAppOccCommand.php @@ -0,0 +1,97 @@ +addType('appid', 'string'); + $this->addType('name', 'string'); + $this->addType('description', 'string'); + $this->addType('hidden', 'bool'); + $this->addType('arguments', 'json'); + $this->addType('options', 'json'); + $this->addType('usages', 'json'); + $this->addType('executeHandler', 'string'); + + if (isset($params['id'])) { + $this->setId($params['id']); + } + if (isset($params['appid'])) { + $this->setAppid($params['appid']); + } + if (isset($params['name'])) { + $this->setName($params['name']); + } + if (isset($params['description'])) { + $this->setDescription($params['description']); + } + if (isset($params['hidden'])) { + $this->setHidden($params['hidden']); + } + if (isset($params['arguments'])) { + $this->setArguments($params['arguments']); + } + if (isset($params['options'])) { + $this->setOptions($params['options']); + } + if (isset($params['usages'])) { + $this->setUsages($params['usages']); + } + if (isset($params['execute_handler'])) { + $this->setExecuteHandler($params['execute_handler']); + } + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'appid' => $this->getAppid(), + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'hidden' => $this->getHidden(), + 'arguments' => $this->getArguments(), + 'options' => $this->getOptions(), + 'usages' => $this->getUsages(), + 'execute_handler' => $this->getExecuteHandler(), + ]; + } +} diff --git a/lib/Db/Console/ExAppOccCommandMapper.php b/lib/Db/Console/ExAppOccCommandMapper.php new file mode 100644 index 00000000..60742185 --- /dev/null +++ b/lib/Db/Console/ExAppOccCommandMapper.php @@ -0,0 +1,63 @@ + + */ +class ExAppOccCommandMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'ex_occ_commands'); + } + + /** + * @throws Exception + */ + public function findAllEnabled(): array { + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('exs.*') + ->from($this->tableName, 'exs') + ->innerJoin('exs', 'ex_apps', 'exa', 'exa.appid = exs.appid') + ->where( + $qb->expr()->eq('exa.enabled', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)) + ) + ->executeQuery(); + return $result->fetchAll(); + } + + public function removeByAppIdOccName(string $appId, string $name): bool { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR)), + $qb->expr()->eq('event_type', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)) + ); + try { + $result = $qb->executeStatement(); + if ($result) { + return true; + } + } catch (Exception) { + } + return false; + } + + /** + * @throws Exception + */ + public function removeAllByAppId(string $appId): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR)) + ); + return $qb->executeStatement(); + } +} diff --git a/lib/Migration/Version2205Date20240411124836.php b/lib/Migration/Version2205Date20240411124836.php new file mode 100644 index 00000000..b16463a2 --- /dev/null +++ b/lib/Migration/Version2205Date20240411124836.php @@ -0,0 +1,77 @@ +hasTable('ex_occ_commands')) { + $table = $schema->createTable('ex_occ_commands'); + + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('appid', Types::STRING, [ + 'notnull' => true, + 'length' => 32, + ]); + // Symfony\Component\Console\Command\Command->setName() + $table->addColumn('name', Types::STRING, [ + 'notnull' => true, + 'length' => 128, + ]); + // Symfony\Component\Console\Command\Command->setDescription() + $table->addColumn('description', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + // Symfony\Component\Console\Command\Command->setHidden() + $table->addColumn('hidden', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 0, + ]); + // Symfony\Component\Console\Command\Command->addArgument() + $table->addColumn('arguments', Types::JSON, [ + 'notnull' => false, + 'default' => '[]', + ]); + // Symfony\Component\Console\Command\Command->addOption() + $table->addColumn('options', Types::JSON, [ + 'notnull' => false, + 'default' => '[]', + ]); + // Symfony\Component\Console\Command\Command->addUsage() + $table->addColumn('usages', Types::JSON, [ + 'notnull' => false, + 'default' => '[]', + ]); + $table->addColumn('execute_handler', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['appid', 'name'], 'ex_occ_commands__idx'); + } + + return $schema; + } +} diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php new file mode 100644 index 00000000..2bc7c431 --- /dev/null +++ b/lib/Service/ExAppOccService.php @@ -0,0 +1,221 @@ +cache = $cacheFactory->createDistributed(Application::APP_ID . '/ex_occ_commands'); + } + + public function registerCommand( + string $appId, + string $name, + string $description, + bool $hidden, + array $arguments, + array $options, + array $usages, + string $executeHandler + ): ?ExAppOccCommand { + $occCommandEntry = $this->getOccCommand($appId, $name); + try { + $newOccCommandEntry = new ExAppOccCommand([ + 'appid' => $appId, + 'name' => $name, + 'description' => $description, + 'hidden' => $hidden, + 'arguments' => $arguments, + 'options' => $options, + 'usages' => $usages, + 'execute_handler' => $executeHandler, + ]); + if ($occCommandEntry !== null) { + $newOccCommandEntry->setId($occCommandEntry->getId()); + } + $occCommandEntry = $this->mapper->insertOrUpdate($newOccCommandEntry); + $this->resetCacheEnabled(); + } catch (Exception $e) { + $this->logger->error( + sprintf('Failed to register ExApp OCC command for %s. Error: %s', $appId, $e->getMessage()), ['exception' => $e] + ); + return null; + } + return $occCommandEntry; + } + + public function unregisterCommand(string $appId, string $name): bool { + if (!$this->mapper->removeByAppIdOccName($appId, $name)) { + return false; + } + $this->resetCacheEnabled(); + return true; + } + + public function getOccCommand(string $appId, string $name): ?ExAppOccCommand { + foreach ($this->getOccCommands() as $occCommand) { + if (($occCommand->getAppid() === $appId) && ($occCommand->getName() === $name)) { + return $occCommand; + } + } + return null; + } + + /** + * Get list of registered ExApp OCC Commands (only for enabled ExApps) + * + * @return ExAppOccCommand[] + */ + public function getOccCommands(): array { + try { + $cacheKey = '/ex_occ_commands'; + $records = $this->cache->get($cacheKey); + if ($records === null) { + $records = $this->mapper->findAllEnabled(); + $this->cache->set($cacheKey, $records); + } + return array_map(function ($record) { + return new ExAppOccCommand($record); + }, $records); + } catch (Exception) { + return []; + } + } + + public function buildCommand(ExAppOccCommand $occCommand, ContainerInterface $container): Command { + return new class($occCommand, $container) extends Command { + private PublicFunctions $service; + private LoggerInterface $logger; + + public function __construct( + private ExAppOccCommand $occCommand, + private ContainerInterface $container + ) { + parent::__construct(); + + $this->service = $this->container->get(PublicFunctions::class); + $this->logger = $this->container->get(LoggerInterface::class); + } + + protected function configure() { + $this->setName($this->occCommand->getName()); + $this->setDescription($this->occCommand->getDescription()); + $this->setHidden($this->occCommand->getHidden()); + foreach ($this->occCommand->getArguments() as $argument) { + $this->addArgument( + $argument['name'], + $this->buildArgumentMode($argument['mode']), + $argument['description'], + $argument['default'] + ); + } + foreach ($this->occCommand->getOptions() as $option) { + $this->addOption( + $option['name'], + $option['shortcut'] ?? null, + $this->buildOptionMode($option['mode']), + $option['description'], + $option['default'] ?? null); + } + foreach ($this->occCommand->getUsages() as $usage) { + $this->addUsage($usage); + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $arguments = []; + foreach ($this->occCommand->getArguments() as $argument) { + $arguments[$argument['name']] = $input->getArgument($argument['name']); + } + + $options = []; + foreach ($this->occCommand->getOptions() as $option) { + $options[$option['name']] = $input->getOption($option['name']); + } + + $executeHandler = $this->occCommand->getExecuteHandler(); + $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, null, 'POST', [], [ + 'occ' => [ + 'arguments' => $arguments, + 'options' => $options, + ] + ]); + if (!($response instanceof IResponse) && isset($response['error'])) { + $output->writeln(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error'])); + $this->logger->error(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error']), [ + 'app' => $this->occCommand->getAppid(), + ]); + return 1; + } + if ($response->getStatusCode() !== Http::STATUS_OK) { + $output->writeln(sprintf('[%s] command executeHandler failed', $this->occCommand->getName())); + $this->logger->error(sprintf('[%s] command executeHandler failed', $this->occCommand->getName()), [ + 'app' => $this->occCommand->getAppid(), + ]); + return 1; + } + $output->writeln($response->getBody()); + + return 0; + } + + private function buildArgumentMode(string $mode): int { + if ($mode === 'required') { + return InputArgument::REQUIRED; + } + if ($mode === 'optional') { + return InputArgument::OPTIONAL; + } + if ($mode === 'array') { + return InputArgument::IS_ARRAY; + } + return InputArgument::OPTIONAL; + } + + private function buildOptionMode(string $mode): int { + if ($mode === 'required') { + return InputOption::VALUE_REQUIRED; + } + if ($mode === 'optional') { + return InputOption::VALUE_OPTIONAL; + } + if ($mode === 'array') { + return InputOption::VALUE_IS_ARRAY; + } + if ($mode === 'negatable') { + return InputOption::VALUE_NEGATABLE; + } + return InputOption::VALUE_NONE; + } + }; + } + + public function resetCacheEnabled(): void { + $this->cache->remove('/ex_occ_commands'); + } +} From 657add20a449c22f515b6fad031efd8ccdefe4c1 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Tue, 16 Apr 2024 17:53:06 +0300 Subject: [PATCH 02/14] fix: change notnull => true Signed-off-by: Andrey Borysenko --- lib/Migration/Version2205Date20240411124836.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Migration/Version2205Date20240411124836.php b/lib/Migration/Version2205Date20240411124836.php index b16463a2..9d7098f7 100644 --- a/lib/Migration/Version2205Date20240411124836.php +++ b/lib/Migration/Version2205Date20240411124836.php @@ -50,17 +50,17 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); // Symfony\Component\Console\Command\Command->addArgument() $table->addColumn('arguments', Types::JSON, [ - 'notnull' => false, + 'notnull' => true, 'default' => '[]', ]); // Symfony\Component\Console\Command\Command->addOption() $table->addColumn('options', Types::JSON, [ - 'notnull' => false, + 'notnull' => true, 'default' => '[]', ]); // Symfony\Component\Console\Command\Command->addUsage() $table->addColumn('usages', Types::JSON, [ - 'notnull' => false, + 'notnull' => true, 'default' => '[]', ]); $table->addColumn('execute_handler', Types::STRING, [ From f3a47ca62eb7ecb2d785afb47a13c5fbe0e88b91 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 14:52:17 +0300 Subject: [PATCH 03/14] WIP: minor adjustments Signed-off-by: Andrey Borysenko --- docs/tech_details/api/occ_command.rst | 4 ++++ lib/Service/ExAppOccService.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/tech_details/api/occ_command.rst b/docs/tech_details/api/occ_command.rst index a43863b7..8e0254c1 100644 --- a/docs/tech_details/api/occ_command.rst +++ b/docs/tech_details/api/occ_command.rst @@ -9,6 +9,10 @@ The principal is similar to the regular Nextcloud OCC command for PHP apps, that but for ExApps it is a trigger via Nextcloud OCC interface to perform some action on the External App side. +.. note:: + + Passing files directly as an input argument to the occ command is not supported. + Register ^^^^^^^^ diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index 2bc7c431..a691a252 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -152,19 +152,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($this->occCommand->getArguments() as $argument) { $arguments[$argument['name']] = $input->getArgument($argument['name']); } - $options = []; foreach ($this->occCommand->getOptions() as $option) { $options[$option['name']] = $input->getOption($option['name']); } $executeHandler = $this->occCommand->getExecuteHandler(); - $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, null, 'POST', [], [ + $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, params: [ 'occ' => [ 'arguments' => $arguments, 'options' => $options, ] ]); + if (!($response instanceof IResponse) && isset($response['error'])) { $output->writeln(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error'])); $this->logger->error(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error']), [ From 9b7505aeb249a9d483f71b08b23b6712a659c7b6 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 14:54:28 +0300 Subject: [PATCH 04/14] update changelog PR num Signed-off-by: Andrey Borysenko --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af804a72..f4fadd79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Different compute device configuration for Daemon (NVIDIA, AMD, CPU). #267 - Ability to add optional parameters when registering a daemon, for example *OVERRIDE_APP_HOST*. #269 +- API for registering OCC commands. #272 - Correct support of the Docker `HEALTHCHECK` instruction. #273 - Support of pulling "custom" images for the selected compute device. #274 -- API for registering OCC commands. ### Fixed From bc2886b2c10de0af68a4c72c23efabffc666fb9a Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:00:58 +0300 Subject: [PATCH 05/14] WIP: add unregister methods on ExApp unregister Signed-off-by: Andrey Borysenko --- lib/Service/ExAppEventsListenerService.php | 10 ++++++++++ lib/Service/ExAppOccService.php | 10 ++++++++++ lib/Service/ExAppService.php | 5 ++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/Service/ExAppEventsListenerService.php b/lib/Service/ExAppEventsListenerService.php index a003d02a..07fa500f 100644 --- a/lib/Service/ExAppEventsListenerService.php +++ b/lib/Service/ExAppEventsListenerService.php @@ -92,6 +92,16 @@ public function getEventsListeners(): array { } } + public function unregisterExAppEventListeners(string $appId): int { + try { + $result = $this->mapper->removeAllByAppId($appId); + } catch (Exception) { + $result = -1; + } + $this->resetCacheEnabled(); + return $result; + } + public function resetCacheEnabled(): void { $this->cache->remove('/ex_events_listener'); } diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index a691a252..04fa1587 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -215,6 +215,16 @@ private function buildOptionMode(string $mode): int { }; } + public function unregisterExAppOccCommands(string $appId): int { + try { + $result = $this->mapper->removeAllByAppId($appId); + } catch (Exception) { + $result = -1; + } + $this->resetCacheEnabled(); + return $result; + } + public function resetCacheEnabled(): void { $this->cache->remove('/ex_occ_commands'); } diff --git a/lib/Service/ExAppService.php b/lib/Service/ExAppService.php index 5845c7e6..f93b022d 100644 --- a/lib/Service/ExAppService.php +++ b/lib/Service/ExAppService.php @@ -52,7 +52,8 @@ public function __construct( private readonly TranslationService $translationService, private readonly TalkBotsService $talkBotsService, private readonly SettingsService $settingsService, - private readonly ExAppEventsListenerService $appEventsListenerService, + private readonly ExAppEventsListenerService $eventsListenerService, + private readonly ExAppOccService $occService, ) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/service'); } @@ -110,6 +111,8 @@ public function unregisterExApp(string $appId): bool { $this->translationService->unregisterExAppTranslationProviders($appId); $this->settingsService->unregisterExAppForms($appId); $this->exAppArchiveFetcher->removeExAppFolder($appId); + $this->eventsListenerService->unregisterExAppEventListeners($appId); + $this->occService->unregisterExAppOccCommands($appId); $r = $this->exAppMapper->deleteExApp($appId); if ($r !== 1) { $this->logger->error(sprintf('Error while unregistering %s ExApp from the database.', $appId)); From 4b34b69adf9bf151da9730ca0a14f345e72599c0 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:04:20 +0300 Subject: [PATCH 06/14] fix: missing reset cache Signed-off-by: Andrey Borysenko --- lib/Service/ExAppService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Service/ExAppService.php b/lib/Service/ExAppService.php index f93b022d..92ab83a5 100644 --- a/lib/Service/ExAppService.php +++ b/lib/Service/ExAppService.php @@ -242,7 +242,8 @@ private function resetCaches(): void { $this->speechToTextService->resetCacheEnabled(); $this->translationService->resetCacheEnabled(); $this->settingsService->resetCacheEnabled(); - $this->appEventsListenerService->resetCacheEnabled(); + $this->eventsListenerService->resetCacheEnabled(); + $this->occService->resetCacheEnabled(); } public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo): array { From 60025aa9223e4a20f8a8244b54d7a01df09f27b9 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:08:31 +0300 Subject: [PATCH 07/14] adjust ExApp handler route length to default 410 Signed-off-by: Andrey Borysenko --- lib/Migration/Version2205Date20240411124836.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Migration/Version2205Date20240411124836.php b/lib/Migration/Version2205Date20240411124836.php index 9d7098f7..656e4c6f 100644 --- a/lib/Migration/Version2205Date20240411124836.php +++ b/lib/Migration/Version2205Date20240411124836.php @@ -65,7 +65,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ]); $table->addColumn('execute_handler', Types::STRING, [ 'notnull' => true, - 'length' => 255, + 'length' => 410, ]); $table->setPrimaryKey(['id']); From ea2e0df7953bcdf141f3cae870b2481ab6637e75 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:12:56 +0300 Subject: [PATCH 08/14] fix column name Signed-off-by: Andrey Borysenko --- lib/Db/Console/ExAppOccCommandMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/Console/ExAppOccCommandMapper.php b/lib/Db/Console/ExAppOccCommandMapper.php index 60742185..b350a55d 100644 --- a/lib/Db/Console/ExAppOccCommandMapper.php +++ b/lib/Db/Console/ExAppOccCommandMapper.php @@ -37,7 +37,7 @@ public function removeByAppIdOccName(string $appId, string $name): bool { $qb->delete($this->tableName) ->where( $qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR)), - $qb->expr()->eq('event_type', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)) + $qb->expr()->eq('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)) ); try { $result = $qb->executeStatement(); From a1156df323ef9ac35e0ae55ec70b9a038e787026 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:27:12 +0300 Subject: [PATCH 09/14] remove default values definition, it'll be set in runtime Signed-off-by: Andrey Borysenko --- lib/Migration/Version2205Date20240411124836.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/Migration/Version2205Date20240411124836.php b/lib/Migration/Version2205Date20240411124836.php index 656e4c6f..492fbee8 100644 --- a/lib/Migration/Version2205Date20240411124836.php +++ b/lib/Migration/Version2205Date20240411124836.php @@ -46,22 +46,18 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt // Symfony\Component\Console\Command\Command->setHidden() $table->addColumn('hidden', Types::SMALLINT, [ 'notnull' => true, - 'default' => 0, ]); // Symfony\Component\Console\Command\Command->addArgument() $table->addColumn('arguments', Types::JSON, [ 'notnull' => true, - 'default' => '[]', ]); // Symfony\Component\Console\Command\Command->addOption() $table->addColumn('options', Types::JSON, [ 'notnull' => true, - 'default' => '[]', ]); // Symfony\Component\Console\Command\Command->addUsage() $table->addColumn('usages', Types::JSON, [ 'notnull' => true, - 'default' => '[]', ]); $table->addColumn('execute_handler', Types::STRING, [ 'notnull' => true, From 74a5d2281c2dd40f1332dd1f173426eea268cd43 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 15:55:23 +0300 Subject: [PATCH 10/14] fix: use default values in registration API Signed-off-by: Andrey Borysenko --- lib/Controller/OccCommandController.php | 10 +++++----- lib/Service/ExAppOccService.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Controller/OccCommandController.php b/lib/Controller/OccCommandController.php index 130bd019..d9efa34b 100644 --- a/lib/Controller/OccCommandController.php +++ b/lib/Controller/OccCommandController.php @@ -32,11 +32,11 @@ public function __construct( public function registerCommand( string $name, string $description, - bool $hidden, - array $arguments, - array $options, - array $usages, - string $execute_handler + string $execute_handler, + bool $hidden = false, + array $arguments = [], + array $options = [], + array $usages = [], ): DataResponse { $command = $this->service->registerCommand( $this->request->getHeader('EX-APP-ID'), $name, diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index 04fa1587..1e6e7253 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -53,7 +53,7 @@ public function registerCommand( 'arguments' => $arguments, 'options' => $options, 'usages' => $usages, - 'execute_handler' => $executeHandler, + 'execute_handler' => ltrim($executeHandler, '/'), ]); if ($occCommandEntry !== null) { $newOccCommandEntry->setId($occCommandEntry->getId()); From f4b9565140d3e5ac7d918d29fe4109fc7cc14a3a Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 17 Apr 2024 17:44:02 +0300 Subject: [PATCH 11/14] add missing Api Scope Signed-off-by: Andrey Borysenko --- docs/tech_details/ApiScopes.rst | 1 + lib/Service/ExAppApiScopeService.php | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/tech_details/ApiScopes.rst b/docs/tech_details/ApiScopes.rst index 1e70ec2f..6602bb1a 100644 --- a/docs/tech_details/ApiScopes.rst +++ b/docs/tech_details/ApiScopes.rst @@ -29,6 +29,7 @@ Supported API Groups include: * ``60`` TALK_BOT * ``61`` AI_PROVIDERS * ``62`` EVENTS_LISTENER +* ``63`` OCC_COMMAND * ``110`` ACTIVITIES * ``120`` NOTES * ``200`` TEXT_PROCESSING diff --git a/lib/Service/ExAppApiScopeService.php b/lib/Service/ExAppApiScopeService.php index c0903ddf..905e3b3d 100644 --- a/lib/Service/ExAppApiScopeService.php +++ b/lib/Service/ExAppApiScopeService.php @@ -33,6 +33,7 @@ public function __construct( ['api_route' => $aeApiV1Prefix . '/talk_bot', 'scope_group' => 60, 'name' => 'TALK_BOT', 'user_check' => 0], ['api_route' => $aeApiV1Prefix . '/ai_provider/', 'scope_group' => 61, 'name' => 'AI_PROVIDERS', 'user_check' => 0], ['api_route' => $aeApiV1Prefix . '/events_listener', 'scope_group' => 62, 'name' => 'EVENTS_LISTENER', 'user_check' => 0], + ['api_route' => $aeApiV1Prefix . '/occ_command', 'scope_group' => 63, 'name' => 'OCC_COMMAND', 'user_check' => 0], // AppAPI internal scopes ['api_route' => '/apps/app_api/apps/status', 'scope_group' => 1, 'name' => 'BASIC', 'user_check' => 0], From 1d9f419bdd98216466bc115dd89799779a004186 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Thu, 18 Apr 2024 13:53:34 +0300 Subject: [PATCH 12/14] WIP: minor fixes, use streamed response output Signed-off-by: Andrey Borysenko --- docs/tech_details/api/occ_command.rst | 4 +-- lib/Service/ExAppOccService.php | 47 ++++++++++++++++++++------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/tech_details/api/occ_command.rst b/docs/tech_details/api/occ_command.rst index 8e0254c1..f0c2a4af 100644 --- a/docs/tech_details/api/occ_command.rst +++ b/docs/tech_details/api/occ_command.rst @@ -30,7 +30,7 @@ Params "arguments": [ { "name": "argument_name", - "mode": "required (InputArgument::REQUIRED)/optional(InputArgument::OPTIONAL)/array(InputArgument::IS_ARRAY)", + "mode": "required (InputArgument::REQUIRED)/optional (InputArgument::OPTIONAL)/array (InputArgument::IS_ARRAY)", "description": "Description of the argument", "default": "default_value" } @@ -39,7 +39,7 @@ Params { "name": "option_name", "shortcut": "shortcut", - "mode": "value_required(InputOption::VALUE_REQUIRED)/value_optional(InputOption::VALUE_OPTIONAL)/value_none(InputOption::VALUE_NONE)/array(InputOption::VALUE_IS_ARRAY)/negatable(InputOption::VALUE_NEGATABLE)", + "mode": "required (InputOption::VALUE_REQUIRED)/optional (InputOption::VALUE_OPTIONAL)/none (InputOption::VALUE_NONE)/array (InputOption::VALUE_IS_ARRAY)/negatable (InputOption::VALUE_NEGATABLE)", "description": "Description of the option", "default": "default_value" } diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index 1e6e7253..4e4e6528 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -14,6 +14,7 @@ use OCP\ICache; use OCP\ICacheFactory; use Psr\Container\ContainerInterface; +use Psr\Http\Message\StreamInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -148,22 +149,35 @@ protected function configure() { } protected function execute(InputInterface $input, OutputInterface $output): int { - $arguments = []; - foreach ($this->occCommand->getArguments() as $argument) { - $arguments[$argument['name']] = $input->getArgument($argument['name']); + if (count($this->occCommand->getArguments()) > 0) { + $arguments = []; + foreach ($this->occCommand->getArguments() as $argument) { + $arguments[$argument['name']] = $input->getArgument($argument['name']); + } + } else { + $arguments = null; } - $options = []; - foreach ($this->occCommand->getOptions() as $option) { - $options[$option['name']] = $input->getOption($option['name']); + if (count($this->occCommand->getOptions()) > 0) { + $options = []; + foreach ($this->occCommand->getOptions() as $option) { + $options[$option['name']] = $input->getOption($option['name']); + } + } else { + $options = null; } $executeHandler = $this->occCommand->getExecuteHandler(); - $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, params: [ - 'occ' => [ - 'arguments' => $arguments, - 'options' => $options, + $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, + params: [ + 'occ' => [ + 'arguments' => $arguments, + 'options' => $options, + ], + ], + options: [ + 'stream' => true, ] - ]); + ); if (!($response instanceof IResponse) && isset($response['error'])) { $output->writeln(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error'])); @@ -172,6 +186,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]); return 1; } + if ($response->getStatusCode() !== Http::STATUS_OK) { $output->writeln(sprintf('[%s] command executeHandler failed', $this->occCommand->getName())); $this->logger->error(sprintf('[%s] command executeHandler failed', $this->occCommand->getName()), [ @@ -179,7 +194,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]); return 1; } - $output->writeln($response->getBody()); + + $body = $response->getBody(); + while(!$body->eof()) { + $row = $body->read(1024); + $output->write($row); + } return 0; } @@ -204,6 +224,9 @@ private function buildOptionMode(string $mode): int { if ($mode === 'optional') { return InputOption::VALUE_OPTIONAL; } + if ($mode === 'none') { + return InputOption::VALUE_NONE; + } if ($mode === 'array') { return InputOption::VALUE_IS_ARRAY; } From b3dcbb424fdd1a84ff2f6e673d3b4e2461dde0a5 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Thu, 18 Apr 2024 21:08:55 +0300 Subject: [PATCH 13/14] minor fixes, use clean guzzle client to support streamed response read, adjust docs, fix usage of multiple modes, cleanup Signed-off-by: Andrey Borysenko --- docs/tech_details/api/occ_command.rst | 50 +++++++++++++++++++++++++-- lib/DeployActions/DockerActions.php | 8 ----- lib/PublicFunctions.php | 20 +++++++++++ lib/Service/AppAPIService.php | 44 ++++++++++++++++++++--- lib/Service/ExAppOccService.php | 30 ++++++++++------ 5 files changed, 126 insertions(+), 26 deletions(-) diff --git a/docs/tech_details/api/occ_command.rst b/docs/tech_details/api/occ_command.rst index f0c2a4af..0e5069f2 100644 --- a/docs/tech_details/api/occ_command.rst +++ b/docs/tech_details/api/occ_command.rst @@ -30,7 +30,7 @@ Params "arguments": [ { "name": "argument_name", - "mode": "required (InputArgument::REQUIRED)/optional (InputArgument::OPTIONAL)/array (InputArgument::IS_ARRAY)", + "mode": "required/optional/array", "description": "Description of the argument", "default": "default_value" } @@ -38,12 +38,56 @@ Params "options": [ { "name": "option_name", - "shortcut": "shortcut", - "mode": "required (InputOption::VALUE_REQUIRED)/optional (InputOption::VALUE_OPTIONAL)/none (InputOption::VALUE_NONE)/array (InputOption::VALUE_IS_ARRAY)/negatable (InputOption::VALUE_NEGATABLE)", + "shortcut": "s", + "mode": "required/optional/none/array/negatable", "description": "Description of the option", "default": "default_value" } ], + "usages": [ + "occ appid:unique:command:name argument_name --option_name", + "occ appid:unique:command:name argument_name -s" + ], + "execute_handler": "handler_route" + } + +For more details on the command arguments and options modes, +see the original docs for the Symfony console input parameters, which are actually being built from the provided data: +`https://symfony.com/doc/current/console/input.html#using-command-arguments `_ + + +Example +******* + +Lets assume we have a command `ping` that takes an argument `test_arg` and has an option `test-option`: + +.. code-block:: json + + { + "name": "my_app_id:ping", + "description": "Test ping command", + "hidden": "false", + "arguments": [ + { + "name": "test_arg", + "mode": "required", + "description": "Test argument", + "default": 123 + } + ], + "options": [ + { + "name": "test-option", + "shortcut": "t", + "mode": "none", + "description": "Test option", + } + ], + "usages": [ + "occ my_app_id:ping 12345", + "occ my_app_id:ping 12345 --test-option", + "occ my_app_id:ping 12345 -t" + ], "execute_handler": "handler_route" } diff --git a/lib/DeployActions/DockerActions.php b/lib/DeployActions/DockerActions.php index 7a17ccde..d6e0e007 100644 --- a/lib/DeployActions/DockerActions.php +++ b/lib/DeployActions/DockerActions.php @@ -602,14 +602,6 @@ public function buildExAppVolumeName(string $appId): string { return self::EX_APP_CONTAINER_PREFIX . $appId . '_data'; } - private function isGPUAvailable(): bool { - $gpusDir = '/dev/dri'; - if (is_dir($gpusDir) && is_readable($gpusDir)) { - return true; - } - return false; - } - /** * Return default GPU device requests for container. */ diff --git a/lib/PublicFunctions.php b/lib/PublicFunctions.php index 7986e518..1d6db03f 100644 --- a/lib/PublicFunctions.php +++ b/lib/PublicFunctions.php @@ -8,6 +8,7 @@ use OCA\AppAPI\Service\ExAppService; use OCP\Http\Client\IResponse; use OCP\IRequest; +use Psr\Http\Message\ResponseInterface; class PublicFunctions { @@ -54,4 +55,23 @@ public function exAppRequestWithUserInit( } return $this->service->aeRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request); } + + /** + * Request to ExApp with AppAPI auth headers using clean Guzzle client + */ + public function exAppRequestGuzzle( + string $appId, + string $route, + ?string $userId = null, + string $method = 'POST', + array $params = [], + array $options = [], + ?IRequest $request = null, + ): array|IResponse|ResponseInterface { + $exApp = $this->exAppService->getExApp($appId); + if ($exApp === null) { + return ['error' => sprintf('ExApp `%s` not found', $appId)]; + } + return $this->service->requestToExAppGuzzle($exApp, $route, $userId, $method, $params, $options, $request); + } } diff --git a/lib/Service/AppAPIService.php b/lib/Service/AppAPIService.php index e14f8f05..b45870d1 100644 --- a/lib/Service/AppAPIService.php +++ b/lib/Service/AppAPIService.php @@ -4,6 +4,7 @@ namespace OCA\AppAPI\Service; +use GuzzleHttp\Client; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\DaemonConfig; use OCA\AppAPI\Db\ExApp; @@ -15,6 +16,7 @@ use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; +use OCP\ICertificateManager; use OCP\IConfig; use OCP\IRequest; use OCP\ISession; @@ -23,6 +25,7 @@ use OCP\L10N\IFactory; use OCP\Log\ILogFactory; use OCP\Security\Bruteforce\IThrottler; +use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; class AppAPIService { @@ -37,6 +40,7 @@ public function __construct( IClientService $clientService, private readonly IUserSession $userSession, private readonly ISession $session, + private readonly ICertificateManager $certificateManager, private readonly IUserManager $userManager, private readonly IFactory $l10nFactory, private readonly ExNotificationsManager $exNotificationsManager, @@ -95,20 +99,24 @@ private function requestToExAppInternal( string $uri, #[\SensitiveParameter] array $options, - ): array|IResponse { + Client $client = null, + ): array|IResponse|ResponseInterface { try { + if ($client === null) { + $client = $this->client; + } switch ($method) { case 'GET': - $response = $this->client->get($uri, $options); + $response = $client->get($uri, $options); break; case 'POST': - $response = $this->client->post($uri, $options); + $response = $client->post($uri, $options); break; case 'PUT': - $response = $this->client->put($uri, $options); + $response = $client->put($uri, $options); break; case 'DELETE': - $response = $this->client->delete($uri, $options); + $response = $client->delete($uri, $options); break; default: return ['error' => 'Bad HTTP method']; @@ -159,6 +167,21 @@ private function requestToExAppInternalAsync( }); } + public function requestToExAppGuzzle( + ExApp $exApp, + string $route, + ?string $userId = null, + string $method = 'POST', + array $params = [], + array $options = [], + ?IRequest $request = null, + ): array|IResponse|ResponseInterface { + $requestData = $this->prepareRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request); + $options = $this->setupCerts($requestData['options']); + $client = new Client($options); + return $this->requestToExAppInternal($exApp, $method, $requestData['url'], $requestData['options'], $client); + } + private function prepareRequestToExApp( ExApp $exApp, string $route, @@ -217,6 +240,17 @@ private function getUriEncodedParams(array $params): string { return $paramsContent . http_build_query($params); } + private function setupCerts(array $guzzleParams): array { + if (!$this->config->getSystemValueBool('installed', false)) { + $certs = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; + } else { + $certs = $this->certificateManager->getAbsoluteBundlePath(); + } + + $guzzleParams['verify'] = $certs; + return $guzzleParams; + } + /** * AppAPI authentication request validation for Nextcloud: * - checks if ExApp exists and is enabled diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index 4e4e6528..5e8674d6 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -10,11 +10,9 @@ use OCA\AppAPI\PublicFunctions; use OCP\AppFramework\Http; use OCP\DB\Exception; -use OCP\Http\Client\IResponse; use OCP\ICache; use OCP\ICacheFactory; use Psr\Container\ContainerInterface; -use Psr\Http\Message\StreamInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -110,8 +108,8 @@ public function getOccCommands(): array { public function buildCommand(ExAppOccCommand $occCommand, ContainerInterface $container): Command { return new class($occCommand, $container) extends Command { - private PublicFunctions $service; private LoggerInterface $logger; + private PublicFunctions $service; public function __construct( private ExAppOccCommand $occCommand, @@ -119,8 +117,8 @@ public function __construct( ) { parent::__construct(); - $this->service = $this->container->get(PublicFunctions::class); $this->logger = $this->container->get(LoggerInterface::class); + $this->service = $this->container->get(PublicFunctions::class); } protected function configure() { @@ -132,7 +130,7 @@ protected function configure() { $argument['name'], $this->buildArgumentMode($argument['mode']), $argument['description'], - $argument['default'] + in_array($argument['mode'], ['optional', 'array']) ? $argument['default'] : null, ); } foreach ($this->occCommand->getOptions() as $option) { @@ -141,7 +139,10 @@ protected function configure() { $option['shortcut'] ?? null, $this->buildOptionMode($option['mode']), $option['description'], - $option['default'] ?? null); + $this->buildOptionMode($option['mode']) !== InputOption::VALUE_NONE + ? $option['default'] ?? null + : null + ); } foreach ($this->occCommand->getUsages() as $usage) { $this->addUsage($usage); @@ -167,7 +168,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $executeHandler = $this->occCommand->getExecuteHandler(); - $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, + $response = $this->service->exAppRequestGuzzle($this->occCommand->getAppid(), $executeHandler, params: [ 'occ' => [ 'arguments' => $arguments, @@ -176,10 +177,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int ], options: [ 'stream' => true, + 'timeout' => 0, ] ); - if (!($response instanceof IResponse) && isset($response['error'])) { + if (is_array($response) && isset($response['error'])) { $output->writeln(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error'])); $this->logger->error(sprintf('[%s] command executeHandler failed. Error: %s', $this->occCommand->getName(), $response['error']), [ 'app' => $this->occCommand->getAppid(), @@ -197,14 +199,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $body = $response->getBody(); while(!$body->eof()) { - $row = $body->read(1024); - $output->write($row); + $output->write($body->read(1024)); } return 0; } private function buildArgumentMode(string $mode): int { + $modes = explode(',', $mode); + $argumentMode = 0; + foreach ($modes as $mode) { + $argumentMode |= $this->_buildArgumentMode($mode); + } + return $argumentMode; + } + + private function _buildArgumentMode(string $mode): int { if ($mode === 'required') { return InputArgument::REQUIRED; } From 480c14c026ca5fc556e6ac5b82dc84cd510ae670 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Fri, 19 Apr 2024 10:53:44 +0300 Subject: [PATCH 14/14] revert not needed clean guzzle methods, use file stream handle to read Signed-off-by: Andrey Borysenko --- lib/PublicFunctions.php | 20 --------------- lib/Service/AppAPIService.php | 44 ++++----------------------------- lib/Service/ExAppOccService.php | 8 +++--- 3 files changed, 10 insertions(+), 62 deletions(-) diff --git a/lib/PublicFunctions.php b/lib/PublicFunctions.php index 1d6db03f..7986e518 100644 --- a/lib/PublicFunctions.php +++ b/lib/PublicFunctions.php @@ -8,7 +8,6 @@ use OCA\AppAPI\Service\ExAppService; use OCP\Http\Client\IResponse; use OCP\IRequest; -use Psr\Http\Message\ResponseInterface; class PublicFunctions { @@ -55,23 +54,4 @@ public function exAppRequestWithUserInit( } return $this->service->aeRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request); } - - /** - * Request to ExApp with AppAPI auth headers using clean Guzzle client - */ - public function exAppRequestGuzzle( - string $appId, - string $route, - ?string $userId = null, - string $method = 'POST', - array $params = [], - array $options = [], - ?IRequest $request = null, - ): array|IResponse|ResponseInterface { - $exApp = $this->exAppService->getExApp($appId); - if ($exApp === null) { - return ['error' => sprintf('ExApp `%s` not found', $appId)]; - } - return $this->service->requestToExAppGuzzle($exApp, $route, $userId, $method, $params, $options, $request); - } } diff --git a/lib/Service/AppAPIService.php b/lib/Service/AppAPIService.php index b45870d1..e14f8f05 100644 --- a/lib/Service/AppAPIService.php +++ b/lib/Service/AppAPIService.php @@ -4,7 +4,6 @@ namespace OCA\AppAPI\Service; -use GuzzleHttp\Client; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\DaemonConfig; use OCA\AppAPI\Db\ExApp; @@ -16,7 +15,6 @@ use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; -use OCP\ICertificateManager; use OCP\IConfig; use OCP\IRequest; use OCP\ISession; @@ -25,7 +23,6 @@ use OCP\L10N\IFactory; use OCP\Log\ILogFactory; use OCP\Security\Bruteforce\IThrottler; -use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; class AppAPIService { @@ -40,7 +37,6 @@ public function __construct( IClientService $clientService, private readonly IUserSession $userSession, private readonly ISession $session, - private readonly ICertificateManager $certificateManager, private readonly IUserManager $userManager, private readonly IFactory $l10nFactory, private readonly ExNotificationsManager $exNotificationsManager, @@ -99,24 +95,20 @@ private function requestToExAppInternal( string $uri, #[\SensitiveParameter] array $options, - Client $client = null, - ): array|IResponse|ResponseInterface { + ): array|IResponse { try { - if ($client === null) { - $client = $this->client; - } switch ($method) { case 'GET': - $response = $client->get($uri, $options); + $response = $this->client->get($uri, $options); break; case 'POST': - $response = $client->post($uri, $options); + $response = $this->client->post($uri, $options); break; case 'PUT': - $response = $client->put($uri, $options); + $response = $this->client->put($uri, $options); break; case 'DELETE': - $response = $client->delete($uri, $options); + $response = $this->client->delete($uri, $options); break; default: return ['error' => 'Bad HTTP method']; @@ -167,21 +159,6 @@ private function requestToExAppInternalAsync( }); } - public function requestToExAppGuzzle( - ExApp $exApp, - string $route, - ?string $userId = null, - string $method = 'POST', - array $params = [], - array $options = [], - ?IRequest $request = null, - ): array|IResponse|ResponseInterface { - $requestData = $this->prepareRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request); - $options = $this->setupCerts($requestData['options']); - $client = new Client($options); - return $this->requestToExAppInternal($exApp, $method, $requestData['url'], $requestData['options'], $client); - } - private function prepareRequestToExApp( ExApp $exApp, string $route, @@ -240,17 +217,6 @@ private function getUriEncodedParams(array $params): string { return $paramsContent . http_build_query($params); } - private function setupCerts(array $guzzleParams): array { - if (!$this->config->getSystemValueBool('installed', false)) { - $certs = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt'; - } else { - $certs = $this->certificateManager->getAbsoluteBundlePath(); - } - - $guzzleParams['verify'] = $certs; - return $guzzleParams; - } - /** * AppAPI authentication request validation for Nextcloud: * - checks if ExApp exists and is enabled diff --git a/lib/Service/ExAppOccService.php b/lib/Service/ExAppOccService.php index 5e8674d6..261d82da 100644 --- a/lib/Service/ExAppOccService.php +++ b/lib/Service/ExAppOccService.php @@ -168,7 +168,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $executeHandler = $this->occCommand->getExecuteHandler(); - $response = $this->service->exAppRequestGuzzle($this->occCommand->getAppid(), $executeHandler, + $response = $this->service->exAppRequest($this->occCommand->getAppid(), $executeHandler, params: [ 'occ' => [ 'arguments' => $arguments, @@ -198,8 +198,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $body = $response->getBody(); - while(!$body->eof()) { - $output->write($body->read(1024)); + if (is_resource($body)) { + while (!feof($body)) { + $output->write(fread($body, 1024)); + } } return 0;