%PDF- %PDF-
Direktori : /www/varak.net/nextcloud.varak.net/apps/app_api/lib/Service/ |
Current File : //www/varak.net/nextcloud.varak.net/apps/app_api/lib/Service/ExAppService.php |
<?php declare(strict_types=1); namespace OCA\AppAPI\Service; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\ExApp; use OCA\AppAPI\Db\ExAppMapper; use OCA\AppAPI\Fetcher\ExAppArchiveFetcher; use OCA\AppAPI\Fetcher\ExAppFetcher; use OCA\AppAPI\Service\ProvidersAI\SpeechToTextService; use OCA\AppAPI\Service\ProvidersAI\TaskProcessingService; use OCA\AppAPI\Service\ProvidersAI\TextProcessingService; use OCA\AppAPI\Service\ProvidersAI\TranslationService; use OCA\AppAPI\Service\UI\FilesActionsMenuService; use OCA\AppAPI\Service\UI\InitialStateService; use OCA\AppAPI\Service\UI\ScriptsService; use OCA\AppAPI\Service\UI\SettingsService; use OCA\AppAPI\Service\UI\StylesService; use OCA\AppAPI\Service\UI\TopMenuService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception; use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; use SimpleXMLElement; class ExAppService { private ?ICache $cache = null; public function __construct( private readonly LoggerInterface $logger, ICacheFactory $cacheFactory, private readonly IUserManager $userManager, private readonly ExAppFetcher $exAppFetcher, private readonly ExAppArchiveFetcher $exAppArchiveFetcher, private readonly ExAppMapper $exAppMapper, private readonly TopMenuService $topMenuService, private readonly InitialStateService $initialStateService, private readonly ScriptsService $scriptsService, private readonly StylesService $stylesService, private readonly FilesActionsMenuService $filesActionsMenuService, private readonly SpeechToTextService $speechToTextService, private readonly TextProcessingService $textProcessingService, private readonly TranslationService $translationService, private readonly TaskProcessingService $taskProcessingService, private readonly TalkBotsService $talkBotsService, private readonly SettingsService $settingsService, private readonly ExAppEventsListenerService $eventsListenerService, private readonly ExAppOccService $occService, private readonly IConfig $config, ) { if ($cacheFactory->isAvailable()) { $distributedCacheClass = ltrim($config->getSystemValueString('memcache.distributed', ''), '\\'); $localCacheClass = ltrim($config->getSystemValueString('memcache.local', ''), '\\'); if ( ($distributedCacheClass === '' && ($localCacheClass !== \OC\Memcache\APCu::class)) && ($distributedCacheClass !== \OC\Memcache\APCu::class) ) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/service'); } } } public function getExApp(string $appId): ?ExApp { foreach ($this->getExApps() as $exApp) { if ($exApp->getAppid() === $appId) { return $exApp; } } $this->logger->debug(sprintf('ExApp "%s" not found.', $appId)); return null; } public function registerExApp(array $appInfo): ?ExApp { $exApp = new ExApp([ 'appid' => $appInfo['id'], 'version' => $appInfo['version'], 'name' => $appInfo['name'], 'daemon_config_name' => $appInfo['daemon_config_name'], 'port' => $appInfo['port'], 'secret' => $appInfo['secret'], 'status' => json_encode(['deploy' => 0, 'init' => 0, 'action' => '', 'type' => 'install', 'error' => '']), 'created_time' => time(), ]); try { $this->exAppMapper->insert($exApp); $exApp = $this->exAppMapper->findByAppId($appInfo['id']); $this->cache?->remove('/ex_apps'); if (isset($appInfo['external-app']['routes'])) { $exApp->setRoutes($this->registerExAppRoutes($exApp, $appInfo['external-app']['routes'])->getRoutes() ?? []); } return $exApp; } catch (Exception | MultipleObjectsReturnedException | DoesNotExistException $e) { $this->logger->error(sprintf('Error while registering ExApp %s: %s', $appInfo['id'], $e->getMessage())); return null; } } public function unregisterExApp(string $appId): bool { $exApp = $this->getExApp($appId); if ($exApp === null) { return false; } $this->talkBotsService->unregisterExAppTalkBots($exApp); // TODO: Think about internal Events for clean and flexible unregister ExApp callbacks $this->filesActionsMenuService->unregisterExAppFileActions($appId); $this->topMenuService->unregisterExAppMenuEntries($appId); $this->initialStateService->deleteExAppInitialStates($appId); $this->scriptsService->deleteExAppScripts($appId); $this->stylesService->deleteExAppStyles($appId); $this->speechToTextService->unregisterExAppSpeechToTextProviders($appId); $this->textProcessingService->unregisterExAppTextProcessingProviders($appId); $this->translationService->unregisterExAppTranslationProviders($appId); $this->taskProcessingService->unregisterExAppTaskProcessingProviders($appId); $this->settingsService->unregisterExAppForms($appId); $this->exAppArchiveFetcher->removeExAppFolder($appId); $this->eventsListenerService->unregisterExAppEventListeners($appId); $this->occService->unregisterExAppOccCommands($appId); $this->unregisterExAppWebhooks($appId); $r = $this->exAppMapper->deleteExApp($appId); if ($r !== 1) { $this->logger->error(sprintf('Error while unregistering %s ExApp from the database.', $appId)); } $rmRoutes = $this->removeExAppRoutes($exApp); if ($rmRoutes === null) { $this->logger->error(sprintf('Error while unregistering %s ExApp routes from the database.', $appId)); } $this->cache?->remove('/ex_apps'); return $r === 1 && $rmRoutes !== null; } public function getExAppFreePort(): int { try { $ports = $this->exAppMapper->getUsedPorts(); for ($port = 23000; $port <= 23999; $port++) { if (!in_array($port, $ports)) { return $port; } } } catch (Exception) { } return 0; } public function enableExAppInternal(ExApp $exApp): bool { $exApp->setEnabled(1); $status = $exApp->getStatus(); $status['error'] = ''; $exApp->setStatus($status); return $this->updateExApp($exApp, ['enabled', 'status']); } public function disableExAppInternal(ExApp $exApp): void { $exApp->setEnabled(0); $this->updateExApp($exApp, ['enabled']); } public function getExAppsByDaemonName(string $daemonName): array { try { return array_filter($this->exAppMapper->findAll(), function (ExApp $exApp) use ($daemonName) { return $exApp->getDaemonConfigName() === $daemonName; }); } catch (Exception $e) { $this->logger->error(sprintf('Error while getting ExApps list. Error: %s', $e->getMessage()), ['exception' => $e]); return []; } } public function getExAppsList(string $list = 'enabled'): array { $exApps = $this->getExApps(); if ($list === 'enabled') { $exApps = array_values(array_filter($exApps, function (ExApp $exApp) { return $exApp->getEnabled() === 1; })); } return array_map(function (ExApp $exApp) { return $this->formatExAppInfo($exApp); }, $exApps); } public function formatExAppInfo(ExApp $exApp): array { return [ 'id' => $exApp->getAppid(), 'name' => $exApp->getName(), 'version' => $exApp->getVersion(), 'enabled' => filter_var($exApp->getEnabled(), FILTER_VALIDATE_BOOLEAN), 'status' => $exApp->getStatus(), ]; } public function getNCUsersList(): ?array { return array_map(function (IUser $user) { return $user->getUID(); }, $this->userManager->searchDisplayName('')); } public function updateExAppInfo(ExApp $exApp, array $appInfo): bool { $exApp->setVersion($appInfo['version']); $exApp->setName($appInfo['name']); if (!$this->updateExApp($exApp, ['version', 'name'])) { return false; } return true; } public function updateExApp(ExApp $exApp, array $fields = ['version', 'name', 'port', 'status', 'enabled']): bool { try { $this->exAppMapper->updateExApp($exApp, $fields); $this->cache?->remove('/ex_apps'); if (in_array('enabled', $fields) || in_array('version', $fields)) { $this->resetCaches(); } return true; } catch (Exception $e) { $this->logger->error(sprintf('Failed to update "%s" ExApp info.', $exApp->getAppid()), ['exception' => $e]); $this->cache?->remove('/ex_apps'); $this->resetCaches(); } return false; } public function getLatestExAppInfoFromAppstore(string $appId, string &$extractedDir): ?SimpleXMLElement { $exApps = $this->exAppFetcher->get(); $exAppAppstoreData = array_filter($exApps, function (array $exAppItem) use ($appId) { return $exAppItem['id'] === $appId && count($exAppItem['releases']) > 0; }); if (empty($exAppAppstoreData)) { return null; } $exAppAppstoreData = end($exAppAppstoreData); $exAppReleaseInfo = end($exAppAppstoreData['releases']); if ($exAppReleaseInfo !== false) { return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData, $extractedDir); } return null; } private function resetCaches(): void { $this->topMenuService->resetCacheEnabled(); $this->filesActionsMenuService->resetCacheEnabled(); $this->textProcessingService->resetCacheEnabled(); $this->speechToTextService->resetCacheEnabled(); $this->translationService->resetCacheEnabled(); $this->settingsService->resetCacheEnabled(); $this->eventsListenerService->resetCacheEnabled(); $this->occService->resetCacheEnabled(); } public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo): array { $extractedDir = ''; if ($jsonInfo !== null) { $appInfo = json_decode($jsonInfo, true); # fill 'id' if it is missing(this field was called `appid` in previous versions in json) $appInfo['id'] = $appInfo['id'] ?? $appId; # during manual install JSON can have all values at root level foreach (['docker-install', 'translations_folder', 'routes'] as $key) { if (isset($appInfo[$key])) { $appInfo['external-app'][$key] = $appInfo[$key]; unset($appInfo[$key]); } } } else { if ($infoXml !== null) { $xmlAppInfo = simplexml_load_string(file_get_contents($infoXml)); if ($xmlAppInfo === false) { return ['error' => sprintf('Failed to load info.xml from %s', $infoXml)]; } } else { $xmlAppInfo = $this->getLatestExAppInfoFromAppstore($appId, $extractedDir); if (empty($xmlAppInfo)) { return ['error' => sprintf('Failed to get app info for `%s` from the Appstore', $appId)]; } } $appInfo = json_decode(json_encode((array)$xmlAppInfo), true); if (isset($appInfo['external-app']['routes']['route'])) { if (isset($appInfo['external-app']['routes']['route'][0])) { $appInfo['external-app']['routes'] = $appInfo['external-app']['routes']['route']; } else { $appInfo['external-app']['routes'] = [$appInfo['external-app']['routes']['route']]; } // update routes, map string access_level to int $appInfo['external-app']['routes'] = array_map(function ($route) use ($appId) { $route['access_level'] = $this->mapExAppRouteAccessLevelNameToNumber($route['access_level']); if ($route['access_level'] !== -1) { return $route; } else { $this->logger->error(sprintf('Invalid access level `%s` for route `%s` in ExApp `%s`', $route['access_level'], $route['url'], $appId)); } }, $appInfo['external-app']['routes']); } if ($extractedDir) { if (file_exists($extractedDir . '/l10n')) { $appInfo['translations_folder'] = $extractedDir . '/l10n'; } else { $this->logger->info(sprintf('Application %s does not support translations', $appId)); } } } return $appInfo; } public function mapExAppRouteAccessLevelNameToNumber(string $accessLevel): int { return match($accessLevel) { 'PUBLIC' => 0, 'USER' => 1, 'ADMIN' => 2, default => -1, }; } public function setAppDeployProgress(ExApp $exApp, int $progress, string $error = ''): void { if ($progress < 0 || $progress > 100) { throw new \InvalidArgumentException('Invalid ExApp deploy status progress value'); } $status = $exApp->getStatus(); if ($progress !== 0 && isset($status['deploy']) && $status['deploy'] === 100) { return; } if ($error !== '') { $this->logger->error(sprintf('ExApp %s deploying failed. Error: %s', $exApp->getAppid(), $error)); $status['error'] = $error; } else { if ($progress === 0) { $status['action'] = 'deploy'; $status['deploy_start_time'] = time(); $status['error'] = ''; } $status['deploy'] = $progress; } if ($progress === 100) { $status['action'] = 'healthcheck'; } $exApp->setStatus($status); $this->updateExApp($exApp, ['status']); } public function waitInitStepFinish(string $appId): string { do { $exApp = $this->getExApp($appId); $status = $exApp->getStatus(); if (isset($status['error']) && $status['error'] !== '') { return sprintf('ExApp %s initialization step failed. Error: %s', $appId, $status['error']); } usleep(100000); // 0.1s } while ($status['init'] !== 100); return ""; } public function setStatusError(ExApp $exApp, string $error): void { $status = $exApp->getStatus(); $status['error'] = $error; $exApp->setStatus($status); $this->updateExApp($exApp, ['status']); } /** * Get list of registered ExApps * * @return ExApp[] */ public function getExApps(): array { try { $cacheKey = '/ex_apps'; $records = $this->cache?->get($cacheKey); if ($records !== null) { return array_map(function ($record) { return $record instanceof ExApp ? $record : new ExApp($record); }, $records); } $records = $this->exAppMapper->findAll(); $this->cache?->set($cacheKey, $records); return $records; } catch (Exception) { return []; } } public function registerExAppRoutes(ExApp $exApp, array $routes): ?ExApp { try { $this->exAppMapper->registerExAppRoutes($exApp, $routes); $exApp->setRoutes($routes); return $exApp; } catch (Exception $e) { $this->logger->error(sprintf('Error while registering ExApp %s routes: %s. Routes: %s', $exApp->getAppid(), $e->getMessage(), json_encode($routes))); return null; } } public function removeExAppRoutes(ExApp $exApp): ?ExApp { try { $this->exAppMapper->removeExAppRoutes($exApp); $exApp->setRoutes([]); return $exApp; } catch (Exception) { return null; } } /** * @psalm-suppress UndefinedClass */ private function unregisterExAppWebhooks(string $appId): void { // webhook_listeners app since NC30 only if (version_compare($this->config->getSystemValueString('version', '0.0.0'), '30.0', '<')) { return; } try { $webhookListenerMapper = \OCP\Server::get(\OCA\WebhookListeners\Db\WebhookListenerMapper::class); $webhookListenerMapper->deleteByAppId($appId); } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { } catch (Exception $e) { $this->logger->debug(sprintf('Error while unregistering ExApp %s webhooks: %s', $appId, $e->getMessage())); } } }