diff --git a/apps/appstore/REUSE.toml b/apps/appstore/REUSE.toml new file mode 100644 index 00000000000..9d147c0d392 --- /dev/null +++ b/apps/appstore/REUSE.toml @@ -0,0 +1,16 @@ +version = 1 +SPDX-PackageName = "nextcloud" +SPDX-PackageSupplier = "Nextcloud " +SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server" + +[[annotations]] +path = ["tests/fixtures/categories.json", "tests/fixtures/categories-api-response.json"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "CC-BY-SA-4.0" + +[[annotations]] +path = ["img/app.svg"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2018-2024 Google LLC" +SPDX-License-Identifier = "Apache-2.0" diff --git a/apps/appstore/appinfo/routes.php b/apps/appstore/appinfo/routes.php deleted file mode 100644 index b1dfba13ba7..00000000000 --- a/apps/appstore/appinfo/routes.php +++ /dev/null @@ -1,27 +0,0 @@ - [ - ['name' => 'AppSettings#getAppDiscoverJSON', 'url' => '/settings/api/apps/discover', 'verb' => 'GET', 'root' => ''], - ['name' => 'AppSettings#getAppDiscoverMedia', 'url' => '/settings/api/apps/media', 'verb' => 'GET', 'root' => ''], - ['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'POST' , 'root' => ''], - ['name' => 'AppSettings#enableApps', 'url' => '/settings/apps/enable', 'verb' => 'POST' , 'root' => ''], - ['name' => 'AppSettings#disableApp', 'url' => '/settings/apps/disable/{appId}', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#disableApps', 'url' => '/settings/apps/disable', 'verb' => 'POST' , 'root' => ''], - ['name' => 'AppSettings#updateApp', 'url' => '/settings/apps/update/{appId}', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#uninstallApp', 'url' => '/settings/apps/uninstall/{appId}', 'verb' => 'GET' , 'root' => ''], - ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}', 'verb' => 'GET', 'defaults' => ['category' => ''] , 'root' => ''], - ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}/{id}', 'verb' => 'GET', 'defaults' => ['category' => '', 'id' => ''] , 'root' => ''], - ['name' => 'AppSettings#force', 'url' => '/settings/apps/force', 'verb' => 'POST' , 'root' => ''], - ], -]; diff --git a/apps/appstore/composer/composer/autoload_classmap.php b/apps/appstore/composer/composer/autoload_classmap.php index 8dab41160b3..b84e2600e43 100644 --- a/apps/appstore/composer/composer/autoload_classmap.php +++ b/apps/appstore/composer/composer/autoload_classmap.php @@ -8,7 +8,8 @@ $baseDir = $vendorDir; return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Appstore\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', - 'OCA\\Appstore\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php', + 'OCA\\Appstore\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', + 'OCA\\Appstore\\Controller\\DiscoverController' => $baseDir . '/../lib/Controller/DiscoverController.php', + 'OCA\\Appstore\\Controller\\PageController' => $baseDir . '/../lib/Controller/PageController.php', 'OCA\\Appstore\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php', - 'OCA\\Settings\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', ); diff --git a/apps/appstore/composer/composer/autoload_static.php b/apps/appstore/composer/composer/autoload_static.php index 78e9f608378..c2d2bc3df05 100644 --- a/apps/appstore/composer/composer/autoload_static.php +++ b/apps/appstore/composer/composer/autoload_static.php @@ -7,14 +7,14 @@ namespace Composer\Autoload; class ComposerStaticInitAppstore { public static $prefixLengthsPsr4 = array ( - 'O' => + 'O' => array ( 'OCA\\Appstore\\' => 13, ), ); public static $prefixDirsPsr4 = array ( - 'OCA\\Appstore\\' => + 'OCA\\Appstore\\' => array ( 0 => __DIR__ . '/..' . '/../lib', ), @@ -23,9 +23,10 @@ class ComposerStaticInitAppstore public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Appstore\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', - 'OCA\\Appstore\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php', + 'OCA\\Appstore\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', + 'OCA\\Appstore\\Controller\\DiscoverController' => __DIR__ . '/..' . '/../lib/Controller/DiscoverController.php', + 'OCA\\Appstore\\Controller\\PageController' => __DIR__ . '/..' . '/../lib/Controller/PageController.php', 'OCA\\Appstore\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php', - 'OCA\\Settings\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/appstore/img/apps.svg b/apps/appstore/img/app.svg similarity index 100% rename from apps/appstore/img/apps.svg rename to apps/appstore/img/app.svg diff --git a/apps/appstore/lib/AppInfo/Application.php b/apps/appstore/lib/AppInfo/Application.php index ca23da831ef..3c5dd05a3b2 100644 --- a/apps/appstore/lib/AppInfo/Application.php +++ b/apps/appstore/lib/AppInfo/Application.php @@ -23,10 +23,12 @@ class Application extends App implements IBootstrap { parent::__construct(self::APP_ID, $urlParams); } + #[\Override] public function register(IRegistrationContext $context): void { $context->registerSearchProvider(AppSearch::class); } + #[\Override] public function boot(IBootContext $context): void { } } diff --git a/apps/appstore/lib/Controller/ApiController.php b/apps/appstore/lib/Controller/ApiController.php new file mode 100644 index 00000000000..1c0f3a05570 --- /dev/null +++ b/apps/appstore/lib/Controller/ApiController.php @@ -0,0 +1,497 @@ +, array{}> + * + * 200: The categories were found successfully + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/apps/categories')] + public function listCategories(): DataResponse { + $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); + + $categories = $this->categoryFetcher->get(); + $categories = array_map(fn ($category) => [ + 'id' => $category['id'], + 'displayName' => $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name'], + ], $categories); + + return new DataResponse($categories); + } + + /** + * Get all available apps + * + * @return DataResponse, array{}> + * + * 200: The apps were found successfully + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/apps')] + public function listApps(): DataResponse { + $apps = $this->getAllApps(); + + $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 ($ignoreMaxApps) { + if (isset($appData['appstoreData'])) { + $appstoreData = $appData['appstoreData']; + $appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? ''); + $appData['category'] = $appstoreData['categories']; + $appData['releases'] = $appstoreData['releases']; + } + + $newVersion = $this->installer->isUpdateAvailable($appData['id']); + if ($newVersion) { + $appData['update'] = $newVersion; + } + + // fix groups to be an array + $groups = []; + if (is_string($appData['groups'])) { + /** @var list|string $groups */ + $groups = json_decode($appData['groups']); + // ensure 'groups' is an array + if (!is_array($groups)) { + $groups = [$groups]; + } + } + $appData['groups'] = $groups; + $appData['canUninstall'] = !$appData['active'] && $appData['removable']; + + // analyze dependencies + $ignoreMax = in_array($appData['id'], $ignoreMaxApps); + $missing = $this->dependencyAnalyzer->analyze($appData, $ignoreMax); + $appData['canInstall'] = empty($missing); + $appData['missingDependencies'] = $missing; + + $appData['missingMinNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']); + $appData['missingMaxNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']); + $appData['isCompatible'] = $this->dependencyAnalyzer->isMarkedCompatible($appData); + + return $appData; + }, $apps); + + usort($apps, $this->sortApps(...)); + + /** + * @var list $apps + */ + return new DataResponse($apps); + } + + /** + * Enable one apps + * + * App will be enabled for specific groups only if $groups is defined + * + * @param string $appId - The app to enable + * @param list $groups - The groups to enable the app for + * @param bool $force - Whether to force enable the app even if Nextcloud version requirements are not met + * + * @return DataResponse + * @throws OCSException - if the app could not be enabled + * + * 200: App successfully enabled + */ + #[PasswordConfirmationRequired(strict: true)] + #[ApiRoute(verb: 'POST', url: '/api/v1/apps/enable')] + public function enableApp(string $appId, array $groups = [], bool $force = false): DataResponse { + try { + $appId = $this->appManager->cleanAppId($appId); + if ($force) { + $this->appManager->overwriteNextcloudRequirement($appId); + } + + // Check if app is already downloaded + if (!$this->installer->isDownloaded($appId)) { + $this->installer->downloadApp($appId); + } + + $this->installer->installApp($appId); + + if (count($groups) > 0) { + $this->appManager->enableAppForGroups($appId, $this->getGroupList($groups)); + } else { + $this->appManager->enableApp($appId); + } + $updateRequired = $this->appManager->isUpgradeRequired($appId); + return new DataResponse(['update_required' => $updateRequired]); + } catch (\Throwable $e) { + $this->logger->error('could not enable app', ['exception' => $e]); + throw new OCSException('could not enable app', Http::STATUS_INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * Disable an app + * + * @param string $appId - The app to disable + * + * @return DataResponse + * @throws OCSException - if the app could not be disabled + * + * 200: App successfully disabled + */ + #[PasswordConfirmationRequired(strict: false)] + #[ApiRoute(verb: 'POST', url: '/api/v1/apps/disable')] + public function disableApp(string $appId): DataResponse { + try { + $appId = $this->appManager->cleanAppId($appId); + $this->appManager->disableApp($appId); + return new DataResponse([]); + } catch (\Exception $e) { + $this->logger->error('could not disable app', ['exception' => $e]); + throw new OCSException('could not disable app', Http::STATUS_INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * Uninstall an app. + * This will disable the app - if needed - and then remove the app from the system + * + * @param string $appId - The app to uninstall + * @return DataResponse + * @throws OCSException - if the app could not be uninstalled + * + * 200: App successfully uninstalled + */ + #[PasswordConfirmationRequired(strict: true)] + #[ApiRoute(verb: 'POST', url: '/api/v1/apps/uninstall')] + public function uninstallApp(string $appId): DataResponse { + $appId = $this->appManager->cleanAppId($appId); + $result = $this->installer->removeApp($appId); + if ($result !== false) { + // If this app was force enabled, remove the force-enabled-state + $this->appManager->removeOverwriteNextcloudRequirement($appId); + $this->appManager->clearAppsCache(); + return new DataResponse([]); + } + throw new OCSException('could not remove app', Http::STATUS_INTERNAL_SERVER_ERROR); + } + + /** + * Update an app + * + * @param string $appId - The app to update + * @return DataResponse + * @throws OCSException - if the app could not be updated + * + * 200: App successfully updated + */ + #[PasswordConfirmationRequired(strict: true)] + #[ApiRoute(verb: 'POST', url: '/api/v1/apps/update')] + public function updateApp(string $appId): DataResponse { + $appId = $this->appManager->cleanAppId($appId); + + $this->config->setSystemValue('maintenance', true); + try { + $result = $this->installer->updateAppstoreApp($appId); + $this->config->setSystemValue('maintenance', false); + if ($result === false) { + throw new \Exception('Update failed'); + } + } catch (\Exception $ex) { + $this->config->setSystemValue('maintenance', false); + throw new OCSException('could not update app', Http::STATUS_INTERNAL_SERVER_ERROR, $ex); + } + + return new DataResponse([]); + } + + /** + * Enable all apps of a bundle + * + * @param string $bundleId - The bundle to enable + * @return DataResponse + * @throws OCSException - if the bundle, or one app within, could not be enabled + * + * 200: Bundle successfully enabled + */ + #[PasswordConfirmationRequired(strict: true)] + #[ApiRoute(verb: 'POST', url: '/api/v1/bundles/enable')] + public function enableBundle(string $bundleId): DataResponse { + try { + $bundle = $this->bundleFetcher->getBundleByIdentifier($bundleId); + $this->config->setSystemValue('maintenance', true); + $this->installer->installAppBundle($bundle); + } catch (\BadMethodCallException $e) { + throw new OCSNotFoundException('Bundle not found', $e); + } catch (\Exception $exception) { + $this->logger->error('could not enable bundle', ['bundleId' => $bundleId, 'exception' => $exception]); + throw new OCSException('could not enable bundle', Http::STATUS_INTERNAL_SERVER_ERROR, $exception); + } finally { + $this->config->setSystemValue('maintenance', false); + } + + return new DataResponse([]); + } + + /** + * Convert URL to proxied URL so CSP is no problem + */ + private function createProxyPreviewUrl(string $url): string { + if ($url === '') { + return ''; + } + return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url); + } + + private function fetchApps() { + $appClass = new \OC_App(); + $apps = $appClass->listAllApps(); + foreach ($apps as $app) { + $app['installed'] = true; + + if (isset($app['screenshot'][0])) { + $appScreenshot = $app['screenshot'][0] ?? null; + if (is_array($appScreenshot)) { + // Screenshot with thumbnail + $appScreenshot = $appScreenshot['@value']; + } + + $app['screenshot'] = $this->createProxyPreviewUrl($appScreenshot); + } + $this->allApps[$app['id']] = $app; + } + + $apps = $this->getAppsForCategory(''); + $supportedApps = $this->subscriptionRegistry->delegateGetSupportedApps(); + foreach ($apps as $app) { + $app['appstore'] = true; + if (!array_key_exists($app['id'], $this->allApps)) { + $this->allApps[$app['id']] = $app; + } else { + $this->allApps[$app['id']] = array_merge($app, $this->allApps[$app['id']]); + } + + if (in_array($app['id'], $supportedApps)) { + $this->allApps[$app['id']]['level'] = \OC_App::supportedApp; + } + } + + // add bundle information + $bundles = $this->bundleFetcher->getBundles(); + foreach ($bundles as $bundle) { + foreach ($bundle->getAppIdentifiers() as $identifier) { + foreach ($this->allApps as &$app) { + if ($app['id'] === $identifier) { + $app['bundleIds'][] = $bundle->getIdentifier(); + continue; + } + } + } + } + } + + private function getAllApps() { + if (empty($this->allApps)) { + $this->fetchApps(); + } + return $this->allApps; + } + + /** + * Get all apps for a category from the app store + * + * @param string $requestedCategory + * @return array + * @throws \Exception + */ + private function getAppsForCategory($requestedCategory = ''): array { + $versionParser = new VersionParser(); + $formattedApps = []; + $apps = $this->appFetcher->get(); + foreach ($apps as $app) { + // 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; + } + } + + 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']); + + try { + $this->appManager->getAppPath($app['id']); + $existsLocally = true; + } catch (AppPathNotFoundException) { + $existsLocally = false; + } + + $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($this->l10nFactory->findLanguage(), 0, 2); + $enabledValue = $this->appConfig->getValueString($app['id'], 'enabled', 'no'); + $groups = null; + if ($enabledValue !== 'no' && $enabledValue !== 'yes') { + $groups = $enabledValue; + } + + $currentVersion = ''; + if ($this->appManager->isEnabledForAnyone($app['id'])) { + $currentVersion = $this->appManager->getAppVersion($app['id']); + } else { + $currentVersion = $app['releases'][0]['version']; + } + + $formattedApps[] = [ + 'id' => $app['id'], + 'app_api' => false, + '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' => $this->appManager->isShipped($app['id']), + 'version' => $currentVersion, + 'types' => [], + 'documentation' => [ + 'admin' => $app['adminDocs'], + 'user' => $app['userDocs'], + 'developer' => $app['developerDocs'] + ], + 'website' => $app['website'], + 'bugs' => $app['issueTracker'], + 'dependencies' => array_merge( + $nextcloudVersionDependencies, + $phpDependencies + ), + 'level' => ($app['isFeatured'] === true) ? 200 : 100, + 'missingMaxNextcloudVersion' => false, + 'missingMinNextcloudVersion' => 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' => $this->appManager->isEnabledForUser($app['id']), + 'needsDownload' => !$existsLocally, + 'groups' => $groups, + 'fromAppStore' => true, + 'appstoreData' => $app, + ]; + } + + return $formattedApps; + } + + private function getGroupList(array $groups) { + $groupManager = Server::get(IGroupManager::class); + $groupsList = []; + foreach ($groups as $group) { + $groupItem = $groupManager->get($group); + if ($groupItem instanceof IGroup) { + $groupsList[] = $groupManager->get($group); + } + } + return $groupsList; + } + + private function sortApps($a, $b) { + $a = (string)$a['name']; + $b = (string)$b['name']; + if ($a === $b) { + return 0; + } + return ($a < $b) ? -1 : 1; + } +} diff --git a/apps/appstore/lib/Controller/AppSettingsController.php b/apps/appstore/lib/Controller/AppSettingsController.php deleted file mode 100644 index 1dbe67f7edf..00000000000 --- a/apps/appstore/lib/Controller/AppSettingsController.php +++ /dev/null @@ -1,689 +0,0 @@ -appData = $appDataFactory->get('appstore'); - } - - /** - * @psalm-suppress UndefinedClass AppAPI is shipped since 30.0.1 - * - * @return TemplateResponse - */ - #[NoCSRFRequired] - public function viewApps(): TemplateResponse { - $this->navigationManager->setActiveEntry('core_apps'); - - $this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true)); - $this->initialState->provideInitialState('appstoreBundles', $this->getBundles()); - $this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates())); - $this->initialState->provideInitialState('isAllInOne', filter_var(getenv('THIS_IS_AIO'), FILTER_VALIDATE_BOOL)); - - $groups = array_map(static fn (IGroup $group): array => [ - 'id' => $group->getGID(), - 'name' => $group->getDisplayName(), - ], $this->groupManager->search('', 5)); - - $this->initialState->provideInitialState('usersSettings', [ 'systemGroups' => $groups]); - - if ($this->appManager->isEnabledForAnyone('app_api')) { - try { - Server::get(ExAppsPageService::class)->provideAppApiState($this->initialState); - } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { - } - } - - $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); - - $templateResponse = new TemplateResponse('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]); - $templateResponse->setContentSecurityPolicy($policy); - - Util::addScript('appstore', 'main'); - - return $templateResponse; - } - - /** - * Get all active entries for the app discover section - */ - #[NoCSRFRequired] - public function getAppDiscoverJSON(): JSONResponse { - $data = $this->discoverFetcher->get(true); - return new JSONResponse($data); - } - - /** - * Get a image for the app discover section - this is proxied for privacy and CSP reasons - * - * @param string $image - * @throws \Exception - */ - #[NoCSRFRequired] - public function getAppDiscoverMedia(string $fileName, ILimiter $limiter, IUserSession $session): Response { - $getEtag = $this->discoverFetcher->getETag() ?? date('Y-m'); - $etag = trim($getEtag, '"'); - - $folder = null; - try { - $folder = $this->appData->getFolder('app-discover-cache'); - $this->cleanUpImageCache($folder, $etag); - } catch (\Throwable $e) { - $folder = $this->appData->newFolder('app-discover-cache'); - } - - // Get the current cache folder - try { - $folder = $folder->getFolder($etag); - } catch (NotFoundException $e) { - $folder = $folder->newFolder($etag); - } - - $info = pathinfo($fileName); - $hashName = md5($fileName); - $allFiles = $folder->getDirectoryListing(); - // Try to find the file - $file = array_filter($allFiles, function (ISimpleFile $file) use ($hashName) { - return str_starts_with($file->getName(), $hashName); - }); - // Get the first entry - $file = reset($file); - // If not found request from Web - if ($file === false) { - $user = $session->getUser(); - // this route is not public thus we can assume a user is logged-in - assert($user !== null); - // Register a user request to throttle fetching external data - // this will prevent using the server for DoS of other systems. - $limiter->registerUserRequest( - 'settings-discover-media', - // allow up to 24 media requests per hour - // this should be a sane default when a completely new section is loaded - // keep in mind browsers request all files from a source-set - 24, - 60 * 60, - $user, - ); - - if (!$this->checkCanDownloadMedia($fileName)) { - $this->logger->warning('Tried to load media files for app discover section from untrusted source'); - return new NotFoundResponse(Http::STATUS_BAD_REQUEST); - } - - try { - $client = $this->clientService->newClient(); - $fileResponse = $client->get($fileName); - $contentType = $fileResponse->getHeader('Content-Type'); - $extension = $info['extension'] ?? ''; - $file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody()); - } catch (\Throwable $e) { - $this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]); - return new NotFoundResponse(); - } - } else { - // File was found so we can get the content type from the file name - $contentType = base64_decode(explode('.', $file->getName())[1] ?? ''); - } - - $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]); - // cache for 7 days - $response->cacheFor(604800, false, true); - return $response; - } - - private function checkCanDownloadMedia(string $filename): bool { - $urlInfo = parse_url($filename); - if (!isset($urlInfo['host']) || !isset($urlInfo['path'])) { - return false; - } - - // Always allowed hosts - if ($urlInfo['host'] === 'nextcloud.com') { - return true; - } - - // Hosts that need further verification - // Github is only allowed if from our organization - $ALLOWED_HOSTS = ['github.com', 'raw.githubusercontent.com']; - if (!in_array($urlInfo['host'], $ALLOWED_HOSTS)) { - return false; - } - - if (str_starts_with($urlInfo['path'], '/nextcloud/') || str_starts_with($urlInfo['path'], '/nextcloud-gmbh/')) { - return true; - } - - return false; - } - - /** - * Remove orphaned folders from the image cache that do not match the current etag - * @param ISimpleFolder $folder The folder to clear - * @param string $etag The etag (directory name) to keep - */ - private function cleanUpImageCache(ISimpleFolder $folder, string $etag): void { - // Cleanup old cache folders - $allFiles = $folder->getDirectoryListing(); - foreach ($allFiles as $dir) { - try { - if ($dir->getName() !== $etag) { - $dir->delete(); - } - } catch (NotPermittedException $e) { - // ignore folder for now - } - } - } - - private function getAppsWithUpdates() { - $appClass = new \OC_App(); - $apps = $appClass->listAllApps(); - foreach ($apps as $key => $app) { - $newVersion = $this->installer->isUpdateAvailable($app['id']); - if ($newVersion === false) { - unset($apps[$key]); - } - } - return $apps; - } - - private function getBundles() { - $result = []; - $bundles = $this->bundleFetcher->getBundles(); - foreach ($bundles as $bundle) { - $result[] = [ - 'name' => $bundle->getName(), - 'id' => $bundle->getIdentifier(), - 'appIdentifiers' => $bundle->getAppIdentifiers() - ]; - } - return $result; - } - - /** - * Get all available categories - * - * @return JSONResponse - */ - public function listCategories(): JSONResponse { - return new JSONResponse($this->getAllCategories()); - } - - private function getAllCategories() { - $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); - - $categories = $this->categoryFetcher->get(); - return array_map(fn ($category) => [ - 'id' => $category['id'], - 'displayName' => $category['translations'][$currentLanguage]['name'] ?? $category['translations']['en']['name'], - ], $categories); - } - - /** - * Convert URL to proxied URL so CSP is no problem - */ - private function createProxyPreviewUrl(string $url): string { - if ($url === '') { - return ''; - } - return 'https://usercontent.apps.nextcloud.com/' . base64_encode($url); - } - - private function fetchApps() { - $appClass = new \OC_App(); - $apps = $appClass->listAllApps(); - foreach ($apps as $app) { - $app['installed'] = true; - - if (isset($app['screenshot'][0])) { - $appScreenshot = $app['screenshot'][0] ?? null; - if (is_array($appScreenshot)) { - // Screenshot with thumbnail - $appScreenshot = $appScreenshot['@value']; - } - - $app['screenshot'] = $this->createProxyPreviewUrl($appScreenshot); - } - $this->allApps[$app['id']] = $app; - } - - $apps = $this->getAppsForCategory(''); - $supportedApps = $appClass->getSupportedApps(); - foreach ($apps as $app) { - $app['appstore'] = true; - if (!array_key_exists($app['id'], $this->allApps)) { - $this->allApps[$app['id']] = $app; - } else { - $this->allApps[$app['id']] = array_merge($app, $this->allApps[$app['id']]); - } - - if (in_array($app['id'], $supportedApps)) { - $this->allApps[$app['id']]['level'] = \OC_App::supportedApp; - } - } - - // add bundle information - $bundles = $this->bundleFetcher->getBundles(); - foreach ($bundles as $bundle) { - foreach ($bundle->getAppIdentifiers() as $identifier) { - foreach ($this->allApps as &$app) { - if ($app['id'] === $identifier) { - $app['bundleIds'][] = $bundle->getIdentifier(); - continue; - } - } - } - } - } - - private function getAllApps() { - return $this->allApps; - } - - /** - * Get all available apps in a category - * - * @return JSONResponse - * @throws \Exception - */ - public function listApps(): JSONResponse { - $this->fetchApps(); - $apps = $this->getAllApps(); - - $dependencyAnalyzer = Server::get(DependencyAnalyzer::class); - - $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 ($dependencyAnalyzer, $ignoreMaxApps) { - if (isset($appData['appstoreData'])) { - $appstoreData = $appData['appstoreData']; - $appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? ''); - $appData['category'] = $appstoreData['categories']; - $appData['releases'] = $appstoreData['releases']; - } - - $newVersion = $this->installer->isUpdateAvailable($appData['id']); - if ($newVersion) { - $appData['update'] = $newVersion; - } - - // fix groups to be an array - $groups = []; - if (is_string($appData['groups'])) { - $groups = json_decode($appData['groups']); - // ensure 'groups' is an array - if (!is_array($groups)) { - $groups = [$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); - - usort($apps, [$this, 'sortApps']); - - return new JSONResponse(['apps' => $apps, 'status' => 'success']); - } - - /** - * Get all apps for a category from the app store - * - * @param string $requestedCategory - * @return array - * @throws \Exception - */ - private function getAppsForCategory($requestedCategory = ''): array { - $versionParser = new VersionParser(); - $formattedApps = []; - $apps = $this->appFetcher->get(); - foreach ($apps as $app) { - // 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; - } - } - - 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']); - - try { - $this->appManager->getAppPath($app['id']); - $existsLocally = true; - } catch (AppPathNotFoundException) { - $existsLocally = false; - } - - $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($this->l10nFactory->findLanguage(), 0, 2); - $enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no'); - $groups = null; - if ($enabledValue !== 'no' && $enabledValue !== 'yes') { - $groups = $enabledValue; - } - - $currentVersion = ''; - if ($this->appManager->isEnabledForAnyone($app['id'])) { - $currentVersion = $this->appManager->getAppVersion($app['id']); - } else { - $currentVersion = $app['releases'][0]['version']; - } - - $formattedApps[] = [ - 'id' => $app['id'], - 'app_api' => false, - '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' => $this->appManager->isShipped($app['id']), - 'version' => $currentVersion, - 'default_enable' => '', - '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' => $this->appManager->isEnabledForUser($app['id']), - 'needsDownload' => !$existsLocally, - 'groups' => $groups, - 'fromAppStore' => true, - 'appstoreData' => $app, - ]; - } - - return $formattedApps; - } - - /** - * @param string $appId - * @param array $groups - * @return JSONResponse - */ - #[PasswordConfirmationRequired] - public function enableApp(string $appId, array $groups = []): JSONResponse { - return $this->enableApps([$appId], $groups); - } - - /** - * Enable one or more apps - * - * apps will be enabled for specific groups only if $groups is defined - * - * @param array $appIds - * @param array $groups - * @return JSONResponse - */ - #[PasswordConfirmationRequired(strict: true)] - public function enableApps(array $appIds, array $groups = []): JSONResponse { - try { - $updateRequired = false; - - foreach ($appIds as $appId) { - $appId = $this->appManager->cleanAppId($appId); - - // Check if app is already downloaded - if (!$this->installer->isDownloaded($appId)) { - $this->installer->downloadApp($appId); - } - - $this->installer->installApp($appId); - - if (count($groups) > 0) { - $this->appManager->enableAppForGroups($appId, $this->getGroupList($groups)); - } else { - $this->appManager->enableApp($appId); - } - $updateRequired = $updateRequired || $this->appManager->isUpgradeRequired($appId); - } - return new JSONResponse(['data' => ['update_required' => $updateRequired]]); - } catch (\Throwable $e) { - $this->logger->error('could not enable apps', ['exception' => $e]); - return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } - - private function getGroupList(array $groups) { - $groupManager = Server::get(IGroupManager::class); - $groupsList = []; - foreach ($groups as $group) { - $groupItem = $groupManager->get($group); - if ($groupItem instanceof IGroup) { - $groupsList[] = $groupManager->get($group); - } - } - return $groupsList; - } - - /** - * @param string $appId - * @return JSONResponse - */ - #[PasswordConfirmationRequired] - public function disableApp(string $appId): JSONResponse { - return $this->disableApps([$appId]); - } - - /** - * @param array $appIds - * @return JSONResponse - */ - #[PasswordConfirmationRequired] - public function disableApps(array $appIds): JSONResponse { - try { - foreach ($appIds as $appId) { - $appId = $this->appManager->cleanAppId($appId); - $this->appManager->disableApp($appId); - } - return new JSONResponse([]); - } catch (\Exception $e) { - $this->logger->error('could not disable app', ['exception' => $e]); - return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } - - /** - * @param string $appId - * @return JSONResponse - */ - #[PasswordConfirmationRequired] - public function uninstallApp(string $appId): JSONResponse { - $appId = $this->appManager->cleanAppId($appId); - $result = $this->installer->removeApp($appId); - if ($result !== false) { - // If this app was force enabled, remove the force-enabled-state - $this->appManager->removeOverwriteNextcloudRequirement($appId); - $this->appManager->clearAppsCache(); - return new JSONResponse(['data' => ['appid' => $appId]]); - } - return new JSONResponse(['data' => ['message' => $this->l10n->t('Could not remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - /** - * @param string $appId - * @return JSONResponse - */ - public function updateApp(string $appId): JSONResponse { - $appId = $this->appManager->cleanAppId($appId); - - $this->config->setSystemValue('maintenance', true); - try { - $result = $this->installer->updateAppstoreApp($appId); - $this->config->setSystemValue('maintenance', false); - } catch (\Exception $ex) { - $this->config->setSystemValue('maintenance', false); - return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - if ($result !== false) { - return new JSONResponse(['data' => ['appid' => $appId]]); - } - return new JSONResponse(['data' => ['message' => $this->l10n->t('Could not update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - private function sortApps($a, $b) { - $a = (string)$a['name']; - $b = (string)$b['name']; - if ($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - } - - public function force(string $appId): JSONResponse { - $appId = $this->appManager->cleanAppId($appId); - $this->appManager->overwriteNextcloudRequirement($appId); - return new JSONResponse(); - } -} diff --git a/apps/appstore/lib/Controller/DiscoverController.php b/apps/appstore/lib/Controller/DiscoverController.php new file mode 100644 index 00000000000..754c0642545 --- /dev/null +++ b/apps/appstore/lib/Controller/DiscoverController.php @@ -0,0 +1,197 @@ +appData = $appDataFactory->get(Application::APP_ID); + } + + /** + * Get all active entries for the app discover section + * + * @return DataResponse, array{}> + * + * 200: List of active entries for the app discover section + */ + #[NoCSRFRequired] + #[ApiRoute(verb: 'GET', url:'/api/v1/discover')] + public function getAppDiscoverJSON(): DataResponse { + $data = $this->discoverFetcher->get(true); + return new DataResponse($data); + } + + /** + * Get a image for the app discover section - this is proxied for privacy and CSP reasons + * + * @param string $fileName - The image file name + * @return FileDisplayResponse + * @throws OCSBadRequestException - if the media source is not trusted + * @throws OCSNotFoundException - if the media file could not be found + * + * 200: The media file was found and is returned + * 400: The media source is not trusted + * 404: The media file could not be found + */ + #[NoCSRFRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/discover/media')] + public function getAppDiscoverMedia(string $fileName, ILimiter $limiter, IUserSession $session): FileDisplayResponse { + $getEtag = $this->discoverFetcher->getETag() ?? date('Y-m'); + $etag = trim($getEtag, '"'); + + $folder = null; + try { + $folder = $this->appData->getFolder('app-discover-cache'); + $this->cleanUpImageCache($folder, $etag); + } catch (\Throwable $e) { + $folder = $this->appData->newFolder('app-discover-cache'); + } + + // Get the current cache folder + try { + $folder = $folder->getFolder($etag); + } catch (NotFoundException $e) { + $folder = $folder->newFolder($etag); + } + + $info = pathinfo($fileName); + $hashName = md5($fileName); + $allFiles = $folder->getDirectoryListing(); + // Try to find the file + $file = array_filter($allFiles, function (ISimpleFile $file) use ($hashName) { + return str_starts_with($file->getName(), $hashName); + }); + // Get the first entry + $file = reset($file); + // If not found request from Web + if ($file === false) { + $user = $session->getUser(); + // this route is not public thus we can assume a user is logged-in + assert($user !== null); + // Register a user request to throttle fetching external data + // this will prevent using the server for DoS of other systems. + $limiter->registerUserRequest( + 'settings-discover-media', + // allow up to 24 media requests per hour + // this should be a sane default when a completely new section is loaded + // keep in mind browsers request all files from a source-set + 24, + 60 * 60, + $user, + ); + + if (!$this->checkCanDownloadMedia($fileName)) { + $this->logger->warning('Tried to load media files for app discover section from untrusted source'); + throw new OCSBadRequestException('Untrusted media source'); + } + + try { + $client = $this->clientService->newClient(); + $fileResponse = $client->get($fileName); + $contentType = $fileResponse->getHeader('Content-Type'); + $extension = $info['extension'] ?? ''; + $file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody()); + } catch (\Throwable $e) { + $this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]); + throw new OCSNotFoundException('Media file not found'); + } + } else { + // File was found so we can get the content type from the file name + $contentType = base64_decode(explode('.', $file->getName())[1] ?? ''); + } + + $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]); + // cache for 7 days + $response->cacheFor(604800, false, true); + return $response; + } + + private function checkCanDownloadMedia(string $filename): bool { + $urlInfo = parse_url($filename); + if (!isset($urlInfo['host']) || !isset($urlInfo['path'])) { + return false; + } + + // Always allowed hosts + if ($urlInfo['host'] === 'nextcloud.com') { + return true; + } + + // Hosts that need further verification + // Github is only allowed if from our organization + $ALLOWED_HOSTS = ['github.com', 'raw.githubusercontent.com']; + if (!in_array($urlInfo['host'], $ALLOWED_HOSTS)) { + return false; + } + + if (str_starts_with($urlInfo['path'], '/nextcloud/') || str_starts_with($urlInfo['path'], '/nextcloud-gmbh/')) { + return true; + } + + return false; + } + + /** + * Remove orphaned folders from the image cache that do not match the current etag + * @param ISimpleFolder $folder The folder to clear + * @param string $etag The etag (directory name) to keep + */ + private function cleanUpImageCache(ISimpleFolder $folder, string $etag): void { + // Cleanup old cache folders + $allFiles = $folder->getDirectoryListing(); + foreach ($allFiles as $dir) { + try { + if ($dir->getName() !== $etag) { + $dir->delete(); + } + } catch (NotPermittedException $e) { + // ignore folder for now + } + } + } +} diff --git a/apps/appstore/lib/Controller/PageController.php b/apps/appstore/lib/Controller/PageController.php new file mode 100644 index 00000000000..5c9a1f360f6 --- /dev/null +++ b/apps/appstore/lib/Controller/PageController.php @@ -0,0 +1,110 @@ + ''], root: '')] + #[FrontpageRoute(verb: 'GET', url: '/settings/apps/{category}/{id}', defaults: ['category' => '', 'id' => ''], root: '')] + public function viewApps(): TemplateResponse { + $this->navigationManager->setActiveEntry('core_apps'); + + $this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true)); + $this->initialState->provideInitialState('appstoreBundles', $this->getBundles()); + $this->initialState->provideInitialState('appstoreDeveloperDocs', $this->urlGenerator->linkToDocs('developer-manual')); + $this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates())); + + if ($this->appManager->isEnabledForAnyone('app_api')) { + try { + Server::get(ExAppsPageService::class)->provideAppApiState($this->initialState); + } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { + } + } + + $policy = new ContentSecurityPolicy(); + $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); + + $templateResponse = new TemplateResponse(Application::APP_ID, 'empty', ['pageTitle' => $this->l10n->t('App store')]); + $templateResponse->setContentSecurityPolicy($policy); + + Util::addStyle(Application::APP_ID, 'main'); + Util::addScript(Application::APP_ID, 'main'); + + return $templateResponse; + } + + + private function getAppsWithUpdates() { + $appClass = new \OC_App(); + $apps = $appClass->listAllApps(); + foreach ($apps as $key => $app) { + $newVersion = $this->installer->isUpdateAvailable($app['id']); + if ($newVersion === false) { + unset($apps[$key]); + } + } + return $apps; + } + + private function getBundles() { + $result = []; + $bundles = $this->bundleFetcher->getBundles(); + foreach ($bundles as $bundle) { + $result[] = [ + 'name' => $bundle->getName(), + 'id' => $bundle->getIdentifier(), + 'appIdentifiers' => $bundle->getAppIdentifiers() + ]; + } + return $result; + } +} diff --git a/apps/appstore/lib/Search/AppSearch.php b/apps/appstore/lib/Search/AppSearch.php index f78bff134af..5aaa40bbdb6 100644 --- a/apps/appstore/lib/Search/AppSearch.php +++ b/apps/appstore/lib/Search/AppSearch.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OCA\Appstore\Search; +use OCA\Appstore\AppInfo\Application; use OCP\IL10N; use OCP\INavigationManager; use OCP\IUser; @@ -25,7 +26,7 @@ class AppSearch implements IProvider { #[\Override] public function getId(): string { - return 'settings_apps'; + return Application::APP_ID; } #[\Override] @@ -35,7 +36,7 @@ class AppSearch implements IProvider { #[\Override] public function getOrder(string $route, array $routeParameters): int { - return $route === 'settings.AppSettings.viewApps' ? -50 : 100; + return $route === 'appstore.Page.viewApps' ? -50 : 100; } #[\Override] diff --git a/apps/appstore/openapi-administration.json b/apps/appstore/openapi-administration.json deleted file mode 100644 index 6e099465cb9..00000000000 --- a/apps/appstore/openapi-administration.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "settings-administration", - "version": "0.0.1", - "description": "Nextcloud settings", - "license": { - "name": "agpl" - } - }, - "components": { - "securitySchemes": { - "basic_auth": { - "type": "http", - "scheme": "basic" - }, - "bearer_auth": { - "type": "http", - "scheme": "bearer" - } - }, - "schemas": {} - }, - "paths": { - "/index.php/settings/admin/log/download": { - "get": { - "operationId": "log_settings-download", - "summary": "download logfile", - "description": "This endpoint requires admin access", - "tags": [ - "log_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "responses": { - "200": { - "description": "Logfile returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string", - "enum": [ - "attachment; filename=\"nextcloud.log\"" - ] - } - } - }, - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "Logged in account must be an admin", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "tags": [] -} diff --git a/apps/appstore/openapi-full.json b/apps/appstore/openapi-full.json deleted file mode 100644 index 33ffe61ea04..00000000000 --- a/apps/appstore/openapi-full.json +++ /dev/null @@ -1,709 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "settings-full", - "version": "0.0.1", - "description": "Nextcloud settings", - "license": { - "name": "agpl" - } - }, - "components": { - "securitySchemes": { - "basic_auth": { - "type": "http", - "scheme": "basic" - }, - "bearer_auth": { - "type": "http", - "scheme": "bearer" - } - }, - "schemas": { - "DeclarativeForm": { - "type": "object", - "required": [ - "id", - "priority", - "section_type", - "section_id", - "storage_type", - "title", - "app", - "fields" - ], - "properties": { - "id": { - "type": "string" - }, - "priority": { - "type": "integer", - "format": "int64" - }, - "section_type": { - "type": "string", - "enum": [ - "admin", - "personal" - ] - }, - "section_id": { - "type": "string" - }, - "storage_type": { - "type": "string", - "enum": [ - "internal", - "external" - ] - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "doc_url": { - "type": "string" - }, - "app": { - "type": "string" - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeclarativeFormField" - } - } - } - }, - "DeclarativeFormField": { - "type": "object", - "required": [ - "id", - "title", - "type", - "default", - "value" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "text", - "password", - "email", - "tel", - "url", - "number", - "checkbox", - "multi-checkbox", - "radio", - "select", - "multi-select" - ] - }, - "placeholder": { - "type": "string" - }, - "label": { - "type": "string" - }, - "default": { - "type": "object" - }, - "options": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "object" - } - } - } - ] - } - }, - "value": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "format": "int64" - }, - { - "type": "number", - "format": "double" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "sensitive": { - "type": "boolean" - } - } - }, - "OCSMeta": { - "type": "object", - "required": [ - "status", - "statuscode" - ], - "properties": { - "status": { - "type": "string" - }, - "statuscode": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "totalitems": { - "type": "string" - }, - "itemsperpage": { - "type": "string" - } - } - } - } - }, - "paths": { - "/index.php/settings/admin/log/download": { - "get": { - "operationId": "log_settings-download", - "summary": "download logfile", - "description": "This endpoint requires admin access", - "tags": [ - "log_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "responses": { - "200": { - "description": "Logfile returned", - "headers": { - "Content-Disposition": { - "schema": { - "type": "string", - "enum": [ - "attachment; filename=\"nextcloud.log\"" - ] - } - } - }, - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string" - } - } - } - } - } - }, - "403": { - "description": "Logged in account must be an admin", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "message" - ], - "properties": { - "message": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/settings/api/declarative/value": { - "post": { - "operationId": "declarative_settings-set-value", - "summary": "Sets a declarative settings value", - "tags": [ - "declarative_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "app", - "formId", - "fieldId", - "value" - ], - "properties": { - "app": { - "type": "string", - "description": "ID of the app" - }, - "formId": { - "type": "string", - "description": "ID of the form" - }, - "fieldId": { - "type": "string", - "description": "ID of the field" - }, - "value": { - "type": "object", - "description": "Value to be saved" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Value set successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "nullable": true - } - } - } - } - } - } - } - }, - "500": { - "description": "Not logged in or not an admin user", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid arguments to save value", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/settings/api/declarative/value-sensitive": { - "post": { - "operationId": "declarative_settings-set-sensitive-value", - "summary": "Sets a declarative settings value. Password confirmation is required for sensitive values.", - "description": "This endpoint requires password confirmation", - "tags": [ - "declarative_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "app", - "formId", - "fieldId", - "value" - ], - "properties": { - "app": { - "type": "string", - "description": "ID of the app" - }, - "formId": { - "type": "string", - "description": "ID of the form" - }, - "fieldId": { - "type": "string", - "description": "ID of the field" - }, - "value": { - "type": "object", - "description": "Value to be saved" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Value set successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "nullable": true - } - } - } - } - } - } - } - }, - "500": { - "description": "Not logged in or not an admin user", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid arguments to save value", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/settings/api/declarative/forms": { - "get": { - "operationId": "declarative_settings-get-forms", - "summary": "Gets all declarative forms with the values prefilled.", - "tags": [ - "declarative_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Forms returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeclarativeForm" - } - } - } - } - } - } - } - } - }, - "500": { - "description": "", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - } - }, - "tags": [] -} diff --git a/apps/appstore/openapi.json b/apps/appstore/openapi.json index c23971fbe26..f51051554e2 100644 --- a/apps/appstore/openapi.json +++ b/apps/appstore/openapi.json @@ -1,9 +1,9 @@ { "openapi": "3.0.3", "info": { - "title": "settings", + "title": "appstore", "version": "0.0.1", - "description": "Nextcloud settings", + "description": "Nextcloud Appstore", "license": { "name": "agpl" } @@ -20,161 +20,6 @@ } }, "schemas": { - "DeclarativeForm": { - "type": "object", - "required": [ - "id", - "priority", - "section_type", - "section_id", - "storage_type", - "title", - "app", - "fields" - ], - "properties": { - "id": { - "type": "string" - }, - "priority": { - "type": "integer", - "format": "int64" - }, - "section_type": { - "type": "string", - "enum": [ - "admin", - "personal" - ] - }, - "section_id": { - "type": "string" - }, - "storage_type": { - "type": "string", - "enum": [ - "internal", - "external" - ] - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "doc_url": { - "type": "string" - }, - "app": { - "type": "string" - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeclarativeFormField" - } - } - } - }, - "DeclarativeFormField": { - "type": "object", - "required": [ - "id", - "title", - "type", - "default", - "value" - ], - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "text", - "password", - "email", - "tel", - "url", - "number", - "checkbox", - "multi-checkbox", - "radio", - "select", - "multi-select" - ] - }, - "placeholder": { - "type": "string" - }, - "label": { - "type": "string" - }, - "default": { - "type": "object" - }, - "options": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "object" - } - } - } - ] - } - }, - "value": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "format": "int64" - }, - { - "type": "number", - "format": "double" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "sensitive": { - "type": "boolean" - } - } - }, "OCSMeta": { "type": "object", "required": [ @@ -202,335 +47,13 @@ } }, "paths": { - "/ocs/v2.php/settings/api/declarative/value": { - "post": { - "operationId": "declarative_settings-set-value", - "summary": "Sets a declarative settings value", - "tags": [ - "declarative_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "app", - "formId", - "fieldId", - "value" - ], - "properties": { - "app": { - "type": "string", - "description": "ID of the app" - }, - "formId": { - "type": "string", - "description": "ID of the form" - }, - "fieldId": { - "type": "string", - "description": "ID of the field" - }, - "value": { - "type": "object", - "description": "Value to be saved" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Value set successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "nullable": true - } - } - } - } - } - } - } - }, - "500": { - "description": "Not logged in or not an admin user", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid arguments to save value", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/settings/api/declarative/value-sensitive": { - "post": { - "operationId": "declarative_settings-set-sensitive-value", - "summary": "Sets a declarative settings value. Password confirmation is required for sensitive values.", - "description": "This endpoint requires password confirmation", - "tags": [ - "declarative_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "app", - "formId", - "fieldId", - "value" - ], - "properties": { - "app": { - "type": "string", - "description": "ID of the app" - }, - "formId": { - "type": "string", - "description": "ID of the form" - }, - "fieldId": { - "type": "string", - "description": "ID of the field" - }, - "value": { - "type": "object", - "description": "Value to be saved" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Value set successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "nullable": true - } - } - } - } - } - } - } - }, - "500": { - "description": "Not logged in or not an admin user", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "Invalid arguments to save value", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/settings/api/declarative/forms": { + "/ocs/v2.php/apps/appstore/api/v1/apps/categories": { "get": { - "operationId": "declarative_settings-get-forms", - "summary": "Gets all declarative forms with the values prefilled.", + "operationId": "api-list-categories", + "summary": "Get all available categories", + "description": "This endpoint requires admin access", "tags": [ - "declarative_settings" + "api" ], "security": [ { @@ -554,7 +77,7 @@ ], "responses": { "200": { - "description": "Forms returned", + "description": "The categories were found successfully", "content": { "application/json": { "schema": { @@ -576,7 +99,19 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/DeclarativeForm" + "type": "object", + "required": [ + "id", + "displayName" + ], + "properties": { + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + } + } } } } @@ -586,12 +121,139 @@ } } }, - "500": { - "description": "", + "401": { + "description": "Current user is not logged in", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "string" + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/apps": { + "get": { + "operationId": "api-list-apps", + "summary": "Get all available apps", + "description": "This endpoint requires admin access", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The apps were found successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "description" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + } + } } } } @@ -623,6 +285,740 @@ } } } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/apps/enable": { + "post": { + "operationId": "api-enable-app", + "summary": "Enable one apps", + "description": "App will be enabled for specific groups only if $groups is defined\nThis endpoint requires admin access\nThis endpoint requires password confirmation", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "appId" + ], + "properties": { + "appId": { + "type": "string", + "description": "- The app to enable" + }, + "groups": { + "type": "array", + "default": [], + "description": "- The groups to enable the app for", + "items": { + "type": "string" + } + }, + "force": { + "type": "boolean", + "default": false, + "description": "- Whether to force enable the app even if Nextcloud version requirements are not met" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "App successfully enabled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "update_required" + ], + "properties": { + "update_required": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/apps/disable": { + "post": { + "operationId": "api-disable-app", + "summary": "Disable an app", + "description": "This endpoint requires admin access\nThis endpoint requires password confirmation", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "appId" + ], + "properties": { + "appId": { + "type": "string", + "description": "- The app to disable" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "App successfully disabled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/apps/uninstall": { + "post": { + "operationId": "api-uninstall-app", + "summary": "Uninstall an app. This will disable the app - if needed - and then remove the app from the system", + "description": "This endpoint requires admin access\nThis endpoint requires password confirmation", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "appId" + ], + "properties": { + "appId": { + "type": "string", + "description": "- The app to uninstall" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "App successfully uninstalled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/apps/update": { + "post": { + "operationId": "api-update-app", + "summary": "Update an app", + "description": "This endpoint requires admin access\nThis endpoint requires password confirmation", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "appId" + ], + "properties": { + "appId": { + "type": "string", + "description": "- The app to update" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "App successfully updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/appstore/api/v1/bundles/enable": { + "post": { + "operationId": "api-enable-bundle", + "summary": "Enable all apps of a bundle", + "description": "This endpoint requires admin access\nThis endpoint requires password confirmation", + "tags": [ + "api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "bundleId" + ], + "properties": { + "bundleId": { + "type": "string", + "description": "- The bundle to enable" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Bundle successfully enabled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } } } } diff --git a/apps/appstore/src/components/AppList.vue b/apps/appstore/src/components/AppList.vue index 4f52e622f07..78b2561c7cb 100644 --- a/apps/appstore/src/components/AppList.vue +++ b/apps/appstore/src/components/AppList.vue @@ -79,8 +79,8 @@ {{ t('appstore', 'Actions') }} - +