initialStateService = $initialStateService; $this->config = $config; $this->service = $service; $this->daemonConfigService = $daemonConfigService; $this->exAppScopeService = $exAppScopeService; $this->exAppApiScopeService = $exAppApiScopeService; $this->dockerActions = $dockerActions; $this->categoryFetcher = $categoryFetcher; $this->l10nFactory = $l10nFactory; $this->l10n = $l10n; $this->exAppFetcher = $exAppFetcher; $this->logger = $logger; $this->appManager = $appManager; $this->exAppUsersService = $exAppUsersService; } #[NoCSRFRequired] public function viewApps(): TemplateResponse { $defaultDaemonConfigName = $this->config->getAppValue(Application::APP_ID, 'default_daemon_config'); $appInitialData = [ 'appstoreEnabled' => $this->config->getSystemValueBool('appstoreenabled', true), 'updateCount' => count($this->getExAppsWithUpdates()), ]; if ($defaultDaemonConfigName !== '') { $daemonConfig = $this->daemonConfigService->getDaemonConfigByName($defaultDaemonConfigName); if ($daemonConfig !== null) { $this->dockerActions->initGuzzleClient($daemonConfig); $daemonConfigAccessible = $this->dockerActions->ping($this->dockerActions->buildDockerUrl($daemonConfig)); $appInitialData['daemon_config_accessible'] = $daemonConfigAccessible; if (!$daemonConfigAccessible) { $this->logger->error(sprintf('Deploy daemon "%s" is not accessible by Nextcloud. Please verify its configuration', $daemonConfig->getName())); } } } $this->initialStateService->provideInitialState('apps', $appInitialData); $templateResponse = new TemplateResponse(Application::APP_ID, 'main'); $policy = new ContentSecurityPolicy(); $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); $templateResponse->setContentSecurityPolicy($policy); return $templateResponse; } private function getExAppsWithUpdates(): array { $apps = $this->exAppFetcher->get(); $appsWithUpdates = array_filter($apps, function (array $app) { $exApp = $this->service->getExApp($app['id']); $newestVersion = $app['releases'][0]['version']; return $exApp !== null && isset($app['releases'][0]['version']) && version_compare($newestVersion, $exApp->getVersion(), '>'); }); return array_values($appsWithUpdates); } /** * Using the same algorithm of ExApps listing as for regular apps. * Returns all apps for a category from the App Store * * @throws Exception */ private function getAppsForCategory(string $requestedCategory = ''): array { $versionParser = new VersionParser(); $formattedApps = []; $apps = $this->exAppFetcher->get(); foreach ($apps as $app) { $exApp = $this->service->getExApp($app['id']); // Skip all apps not in the requested category if ($requestedCategory !== '') { $isInCategory = false; foreach ($app['categories'] as $category) { if ($category === $requestedCategory) { $isInCategory = true; } } if (!$isInCategory) { continue; } } // Default PHP and NC versions requirements check preserved if (!isset($app['releases'][0]['rawPlatformVersionSpec'])) { continue; } $nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']); $nextCloudVersionDependencies = []; if ($nextCloudVersion->getMinimumVersion() !== '') { $nextCloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion(); } if ($nextCloudVersion->getMaximumVersion() !== '') { $nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion(); } $phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']); $existsLocally = $exApp !== null; $phpDependencies = []; if ($phpVersion->getMinimumVersion() !== '') { $phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion(); } if ($phpVersion->getMaximumVersion() !== '') { $phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion(); } if (isset($app['releases'][0]['minIntSize'])) { $phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize']; } $authors = ''; foreach ($app['authors'] as $key => $author) { $authors .= $author['name']; if ($key !== count($app['authors']) - 1) { $authors .= ', '; } } $currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2); $enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no'); $groups = null; if ($enabledValue !== 'no' && $enabledValue !== 'yes') { $groups = $enabledValue; } if ($exApp !== null) { $currentVersion = $exApp->getVersion(); } else { $currentVersion = $app['releases'][0]['version']; } $scopes = null; $daemon = null; if ($exApp !== null) { $scopes = $this->exAppApiScopeService->mapScopeGroupsToNames(array_map(function (ExAppScope $exAppScope) { return $exAppScope->getScopeGroup(); }, $this->exAppScopeService->getExAppScopes($exApp))); $daemon = $this->daemonConfigService->getDaemonConfigByName($exApp->getDaemonConfigName()); } $formattedApps[] = [ 'id' => $app['id'], 'installed' => $exApp !== null, // if ExApp registered then it's assumed that it was already deployed (installed) 'appstore' => true, 'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'], 'description' => $app['translations'][$currentLanguage]['description'] ?? $app['translations']['en']['description'], 'summary' => $app['translations'][$currentLanguage]['summary'] ?? $app['translations']['en']['summary'], 'license' => $app['releases'][0]['licenses'], 'author' => $authors, 'shipped' => false, 'version' => $currentVersion, 'types' => [], 'documentation' => [ 'admin' => $app['adminDocs'], 'user' => $app['userDocs'], 'developer' => $app['developerDocs'] ], 'website' => $app['website'], 'bugs' => $app['issueTracker'], 'detailpage' => $app['website'], 'dependencies' => array_merge( $nextCloudVersionDependencies, $phpDependencies ), 'level' => ($app['isFeatured'] === true) ? 200 : 100, 'missingMaxOwnCloudVersion' => false, 'missingMinOwnCloudVersion' => false, 'canInstall' => true, 'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '', 'score' => $app['ratingOverall'], 'ratingNumOverall' => $app['ratingNumOverall'], 'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5, 'removable' => $existsLocally, 'active' => $exApp !== null && $exApp->getEnabled() === 1, 'needsDownload' => !$existsLocally, 'groups' => $groups, 'fromAppStore' => true, 'appstoreData' => $app, 'scopes' => $scopes, 'daemon' => $daemon, 'systemApp' => $exApp !== null && $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''), 'exAppUrl' => $exApp !== null ? AppAPIService::getExAppUrl($exApp->getProtocol(), $exApp->getHost(), $exApp->getPort()) : '', 'status' => $exApp !== null ? json_decode($exApp->getStatus(), true) : [], ]; } return $formattedApps; } #[NoCSRFRequired] public function listApps(): JSONResponse { $apps = $this->getAppsForCategory(''); $appsWithUpdate = $this->getExAppsWithUpdates(); $exApps = $this->service->getExAppsList('all'); $dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n); $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); if (!is_array($ignoreMaxApps)) { $this->logger->warning('The value given for app_install_overwrite is not an array. Ignoring...'); $ignoreMaxApps = []; } // Extend existing app details $apps = array_map(function (array $appData) use ($appsWithUpdate, $dependencyAnalyzer, $ignoreMaxApps) { if (isset($appData['appstoreData'])) { $appstoreData = $appData['appstoreData']; $appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($appstoreData['screenshots'][0]['url']) : ''; $appData['category'] = $appstoreData['categories']; $appData['releases'] = $appstoreData['releases']; } $appIdsWithUpdate = array_map(function (array $appWithUpdate) { return $appWithUpdate['id']; }, $appsWithUpdate); if (in_array($appData['id'], $appIdsWithUpdate)) { $appData['update'] = array_values(array_filter($appsWithUpdate, function ($appUpdate) use ($appData) { return $appUpdate['id'] === $appData['id']; }))[0]['releases'][0]['version']; } // fix groups to be an array $groups = []; if (is_string($appData['groups'])) { $groups = json_decode($appData['groups']); } $appData['groups'] = $groups; $appData['canUnInstall'] = !$appData['active'] && $appData['removable']; // fix licence vs license if (isset($appData['license']) && !isset($appData['licence'])) { $appData['licence'] = $appData['license']; } $ignoreMax = in_array($appData['id'], $ignoreMaxApps); // analyse dependencies $missing = $dependencyAnalyzer->analyze($appData, $ignoreMax); $appData['canInstall'] = empty($missing); $appData['missingDependencies'] = $missing; $appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']); $appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']); $appData['isCompatible'] = $dependencyAnalyzer->isMarkedCompatible($appData); return $appData; }, $apps); $apps = $this->buildLocalAppsList($apps, $exApps); usort($apps, [$this, 'sortApps']); return new JSONResponse(['apps' => $apps, 'status' => 'success']); } /** * Prepare list of local ExApps with required data structure to be listed in UI. * This is a temporal solution to display local apps with lack of information about it until * it's support extended. * * @param array $apps Formatted list of apps available in App Store * @param array $exApps List of registered ExApps (could be not listed in App Store) * * @return array */ private function buildLocalAppsList(array $apps, array $exApps): array { $registeredAppsIds = array_map(function ($app) { return $app['id']; }, $apps); $formattedLocalApps = []; foreach ($exApps as $app) { if (!in_array($app['id'], $registeredAppsIds)) { $exApp = $this->service->getExApp($app['id']); $daemon = $this->daemonConfigService->getDaemonConfigByName($exApp->getDaemonConfigName()); $scopes = $this->exAppApiScopeService->mapScopeGroupsToNames(array_map(function (ExAppScope $exAppScope) { return $exAppScope->getScopeGroup(); }, $this->exAppScopeService->getExAppScopes($exApp))); $formattedLocalApps[] = [ 'id' => $app['id'], 'appstore' => false, 'installed' => true, 'name' => $exApp->getName(), 'description' => '', 'summary' => '', 'license' => '', 'author' => '', 'shipped' => false, 'version' => $exApp->getVersion(), 'types' => [], 'documentation' => [ 'admin' => '', 'user' => '', 'developer' => '' ], 'website' => '', 'bugs' => '', 'detailpage' => '', 'dependencies' => [], 'level' => 100, 'missingMaxOwnCloudVersion' => false, 'missingMinOwnCloudVersion' => false, 'canInstall' => true, // to allow "remove" command for manual-install 'canUnInstall' => !($exApp->getEnabled() === 1), 'isCompatible' => true, 'screenshot' => '', 'score' => 0, 'ratingNumOverall' => 0, 'ratingNumThresholdReached' => false, 'removable' => true, // to allow "remove" command for manual-install 'active' => $exApp->getEnabled() === 1, 'needsDownload' => false, 'groups' => [], 'fromAppStore' => false, 'appstoreData' => $app, 'scopes' => $scopes, 'daemon' => $daemon, 'systemApp' => $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''), 'exAppUrl' => AppAPIService::getExAppUrl($exApp->getProtocol(), $exApp->getHost(), $exApp->getPort()), 'releases' => [], 'update' => null, 'status' => json_decode($exApp->getStatus(), true), ]; } } $apps = array_merge($apps, $formattedLocalApps); return $apps; } /** * @PasswordConfirmationRequired * * @param string $appId * @param array $groups // TODO: Add support of groups later if needed * * @return JSONResponse */ public function enableApp(string $appId, array $groups = []): JSONResponse { return $this->enableApps([$appId]); } /** * Enable one or more apps. * Deploy ExApp if it was not deployed yet. * * @PasswordConfirmationRequired */ public function enableApps(array $appIds, array $groups = []): JSONResponse { try { $updateRequired = false; foreach ($appIds as $appId) { // If ExApp is not null, assuming it was already deployed, therefore it could be registered $exApp = $this->service->getExApp($appId); // If ExApp not registered - then it's a "Deploy and Enable" action. Get default_daemon_config, deploy ExApp, register and finally enable if ($exApp === null) { $infoXml = $this->service->getLatestExAppInfoFromAppstore($appId); $defaultDaemonConfigName = $this->config->getAppValue(Application::APP_ID, 'default_daemon_config', ''); $daemonConfig = $this->daemonConfigService->getDaemonConfigByName($defaultDaemonConfigName); if ($daemonConfig->getAcceptsDeployId() !== $this->dockerActions->getAcceptsDeployId()) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Only docker-install is supported for now')]], Http::STATUS_INTERNAL_SERVER_ERROR); } // 1. Deploy ExApp if ($this->deployExApp($appId, $infoXml, $daemonConfig)) { // 2. Register ExApp (container must be already initialized successfully) if (!$this->registerExApp($appId, $infoXml, $daemonConfig)) { $this->service->unregisterExApp($appId); // Fallback unregister if failure return new JSONResponse(['data' => ['message' => $this->l10n->t('Failed to register ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR); } } else { $this->logger->error(sprintf('Failed to Deploy %s ExApp', $appId)); return new JSONResponse([ 'data' => [ 'message' => $this->l10n->t('Failed to deploy ExApp'), ] ], Http::STATUS_INTERNAL_SERVER_ERROR); } $exApp = $this->service->getExApp($appId); // Start ExApp initialization step (to download dynamic content, e.g. models) if (!$this->service->dispatchExAppInit($exApp)) { return new JSONResponse([ 'data' => [ 'message' => $this->l10n->t('Failed to send "init" event to ExApp.'), ] ], Http::STATUS_INTERNAL_SERVER_ERROR); } $scopes = $this->exAppApiScopeService->mapScopeGroupsToNames(array_map(function (ExAppScope $exAppScope) { return $exAppScope->getScopeGroup(); }, $this->exAppScopeService->getExAppScopes($exApp))); return new JSONResponse([ 'data' => [ 'daemon_config' => $daemonConfig, 'systemApp' => $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''), 'exAppUrl' => AppAPIService::getExAppUrl($exApp->getProtocol(), $exApp->getHost(), $exApp->getPort()), 'status' => json_decode($exApp->getStatus(), true), 'scopes' => $scopes, ] ]); } $appsWithUpdate = $this->getExAppsWithUpdates(); $appIdsWithUpdate = array_map(function (array $appWithUpdate) { return $appWithUpdate['id']; }, $appsWithUpdate); if (in_array($appId, $appIdsWithUpdate)) { $updateRequired = true; } if (!$this->service->enableExApp($exApp)) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Failed to enable ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR); } } return new JSONResponse(['data' => ['update_required' => $updateRequired]]); } catch (Exception $e) { $this->logger->error('Could not enable ExApps', ['exception' => $e]); return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); } } private function deployExApp(string $appId, SimpleXMLElement $infoXml, DaemonConfig $daemonConfig): bool { $deployParams = $this->dockerActions->buildDeployParams($daemonConfig, $infoXml); [$pullResult, $createResult, $startResult] = $this->dockerActions->deployExApp($daemonConfig, $deployParams); if (isset($pullResult['error']) || isset($createResult['error']) || isset($startResult['error'])) { return false; } if (!$this->dockerActions->healthcheckContainer($this->dockerActions->buildExAppContainerName($appId), $daemonConfig)) { return false; } if (!$this->service->heartbeatExApp([ 'protocol' => (string) ($infoXml->xpath('ex-app/protocol')[0] ?? 'http'), 'host' => $this->dockerActions->resolveDeployExAppHost($appId, $daemonConfig), 'port' => explode('=', $deployParams['container_params']['env'][7])[1], ])) { return false; } return true; } private function registerExApp(string $appId, SimpleXMLElement $infoXml, DaemonConfig $daemonConfig): bool { $exAppInfo = $this->dockerActions->loadExAppInfo($appId, $daemonConfig); $exApp = $this->service->registerExApp($appId, [ 'appid' => $exAppInfo['appid'], 'version' => $exAppInfo['version'], 'name' => $exAppInfo['name'], 'daemon_config_name' => $daemonConfig->getName(), 'protocol' => $exAppInfo['protocol'] ?? 'http', 'port' => (int) $exAppInfo['port'], 'host' => $exAppInfo['host'], 'secret' => $exAppInfo['secret'], ]); if ($exApp === null) { return false; } // Setup system flag $isSystemApp = $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''); if (filter_var($exAppInfo['system_app'], FILTER_VALIDATE_BOOLEAN) && !$isSystemApp) { $this->exAppUsersService->setupSystemAppFlag($exApp); } // Register ExApp ApiScopes $requestedExAppScopeGroups = $this->service->getExAppRequestedScopes($exApp, $infoXml, $exAppInfo); if (!$this->registerApiScopes($exApp, $requestedExAppScopeGroups, 'required')) { return false; } $this->registerApiScopes($exApp, $requestedExAppScopeGroups, 'optional'); return true; } private function registerApiScopes(ExApp $exApp, array $requestedExAppScopeGroups, string $scopeType): bool { $registeredScopeGroups = []; foreach ($this->exAppApiScopeService->mapScopeNamesToNumbers($requestedExAppScopeGroups[$scopeType]) as $scopeGroup) { if ($this->exAppScopeService->setExAppScopeGroup($exApp, $scopeGroup)) { $registeredScopeGroups[] = $scopeGroup; } } if (count($registeredScopeGroups) !== count($requestedExAppScopeGroups['required'])) { return false; } return true; } #[PasswordConfirmationRequired] public function disableApp(string $appId): JSONResponse { return $this->disableApps([$appId]); } #[PasswordConfirmationRequired] public function disableApps(array $appIds): JSONResponse { try { foreach ($appIds as $appId) { $exApp = $this->service->getExApp($appId); if (!$this->service->disableExApp($exApp)) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Failed to disable ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR); } } return new JSONResponse([]); } catch (Exception $e) { $this->logger->error('Could not disable ExApp', ['exception' => $e]); return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); } } /** * Default forced ExApp update process. * Update approval via password confirmation. * Scopes approval does not applied in UI for now. */ #[PasswordConfirmationRequired] #[NoCSRFRequired] public function updateApp(string $appId): JSONResponse { $appsWithUpdate = $this->getExAppsWithUpdates(); $appIdsWithUpdate = array_map(function (array $appWithUpdate) { return $appWithUpdate['id']; }, $appsWithUpdate); if (!in_array($appId, $appIdsWithUpdate)) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Could not update ExApp')]], Http::STATUS_INTERNAL_SERVER_ERROR); } $exApp = $this->service->getExApp($appId); // TODO: Add error messages on each step failure as in CLI // 1. Disable ExApp $this->service->disableExApp($exApp); $infoXml = $this->service->getLatestExAppInfoFromAppstore($appId); $daemonConfig = $this->daemonConfigService->getDaemonConfigByName($exApp->getDaemonConfigName()); if ($daemonConfig->getAcceptsDeployId() !== $this->dockerActions->getAcceptsDeployId()) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Only docker-install is supported for now')]], Http::STATUS_INTERNAL_SERVER_ERROR); } $this->dockerActions->initGuzzleClient($daemonConfig); $containerInfo = $this->dockerActions->inspectContainer($this->dockerActions->buildDockerUrl($daemonConfig), $this->dockerActions->buildExAppContainerName($appId)); $deployParams = $this->dockerActions->buildDeployParams($daemonConfig, $infoXml, [ 'container_info' => $containerInfo, ]); // 2. Update ExApp container (deploy new version) $this->dockerActions->updateExApp($daemonConfig, $deployParams); // 3. Update ExApp info on NC side $exAppInfo = $this->dockerActions->loadExAppInfo($appId, $daemonConfig); $this->service->updateExAppInfo($exApp, $exAppInfo); // 4. Update ExApp ApiScopes $this->upgradeExAppScopes($exApp, $infoXml); $exApp = $this->service->getExApp($appId); // 5. Heartbeat ExApp if ($this->service->heartbeatExApp([ 'protocol' => $exAppInfo['protocol'], 'host' => $exAppInfo['host'], 'port' => (int) $exAppInfo['port'], ])) { // 6. Dispatch init step on ExApp side if (!$this->service->dispatchExAppInit($exApp, true)) { return new JSONResponse([ 'data' => [ 'message' => $this->l10n->t('Failed to send "init" event to ExApp.'), ] ], Http::STATUS_INTERNAL_SERVER_ERROR); } } $scopes = $this->exAppApiScopeService->mapScopeGroupsToNames(array_map(function (ExAppScope $exAppScope) { return $exAppScope->getScopeGroup(); }, $this->exAppScopeService->getExAppScopes($exApp))); return new JSONResponse([ 'data' => [ 'appid' => $appId, 'status' => ['progress' => 0], 'systemApp' => filter_var($exAppInfo['system_app'], FILTER_VALIDATE_BOOLEAN), 'exAppUrl' => AppAPIService::getExAppUrl($exAppInfo['protocol'], $exAppInfo['host'], (int) $exAppInfo['port']), 'scopes' => $scopes, ] ]); } public function enableExApp(string $appId): JSONResponse { $exApp = $this->service->getExApp($appId); if (!$this->service->enableExApp($exApp)) { return new JSONResponse(['data' => ['message' => $this->l10n->t('Failed to enable ExApp')]]); } return new JSONResponse([ 'data' => [ 'appid' => $appId, 'systemApp' => $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''), 'exAppUrl' => AppAPIService::getExAppUrl($exApp->getProtocol(), $exApp->getHost(), $exApp->getPort()), ] ]); } private function upgradeExAppScopes(ExApp $exApp, SimpleXMLElement $infoXml): void { $newExAppScopes = $this->service->getExAppRequestedScopes($exApp, $infoXml); $newExAppScopes = array_merge( $this->exAppApiScopeService->mapScopeNamesToNumbers($newExAppScopes['required']), $this->exAppApiScopeService->mapScopeNamesToNumbers($newExAppScopes['optional']) ); $this->exAppScopeService->updateExAppScopes($exApp, $newExAppScopes); } /** * Unregister ExApp, remove container and volume by default */ #[PasswordConfirmationRequired] public function uninstallApp(string $appId, bool $removeContainer = true, bool $removeData = false): JSONResponse { $exApp = $this->service->getExApp($appId); $this->service->disableExApp($exApp); $daemonConfig = $this->daemonConfigService->getDaemonConfigByName($exApp->getDaemonConfigName()); if ($daemonConfig->getAcceptsDeployId() === $this->dockerActions->getAcceptsDeployId()) { $this->dockerActions->initGuzzleClient($daemonConfig); if ($removeContainer) { $this->dockerActions->removePrevExAppContainer($this->dockerActions->buildDockerUrl($daemonConfig), $this->dockerActions->buildExAppContainerName($appId)); if ($removeData) { $this->dockerActions->removeVolume($this->dockerActions->buildDockerUrl($daemonConfig), $this->dockerActions->buildExAppVolumeName($appId)); } } } $this->service->unregisterExApp($appId); return new JSONResponse(); } /** * Using default force mechanism for ExApps */ #[PasswordConfirmationRequired] public function force(string $appId): JSONResponse { $appId = OC_App::cleanAppId($appId); $this->appManager->ignoreNextcloudRequirementForApp($appId); return new JSONResponse(); } /** * Get all available categories */ public function listCategories(): JSONResponse { return new JSONResponse($this->getAllCategories()); } /** * Get ExApp status, that includes initialization information */ public function getAppStatus(string $appId): JSONResponse { $exApp = $this->service->getExApp($appId); return new JSONResponse(json_decode($exApp->getStatus(), true)); } /** * Using default methods to fetch App Store categories as they are the same for ExApps * * @return mixed */ private function getAllCategories(): array { $currentLang = substr($this->l10nFactory->findLanguage(), 0, 2); $categories = $this->categoryFetcher->get(); return array_map(function (array $category) use ($currentLang) { return [ 'id' => $category['id'], 'ident' => $category['id'], 'displayName' => $category['translations'][$currentLang]['name'] ?? $category['translations']['en']['name'] ]; }, $categories); } private function sortApps($a, $b): int { $a = (string)$a['name']; $b = (string)$b['name']; if ($a === $b) { return 0; } return ($a < $b) ? -1 : 1; } }