aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/lib/Controller/AppSettingsController.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/lib/Controller/AppSettingsController.php')
-rw-r--r--apps/settings/lib/Controller/AppSettingsController.php689
1 files changed, 689 insertions, 0 deletions
diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php
new file mode 100644
index 00000000000..a85ee8cc20a
--- /dev/null
+++ b/apps/settings/lib/Controller/AppSettingsController.php
@@ -0,0 +1,689 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Settings\Controller;
+
+use OC\App\AppManager;
+use OC\App\AppStore\Bundles\BundleFetcher;
+use OC\App\AppStore\Fetcher\AppDiscoverFetcher;
+use OC\App\AppStore\Fetcher\AppFetcher;
+use OC\App\AppStore\Fetcher\CategoryFetcher;
+use OC\App\AppStore\Version\VersionParser;
+use OC\App\DependencyAnalyzer;
+use OC\App\Platform;
+use OC\Installer;
+use OCA\AppAPI\Service\ExAppsPageService;
+use OCP\App\AppPathNotFoundException;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Http\Response;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Security\RateLimiting\ILimiter;
+use OCP\Server;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
+class AppSettingsController extends Controller {
+
+ /** @var array */
+ private $allApps = [];
+
+ private IAppData $appData;
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ IAppDataFactory $appDataFactory,
+ private IL10N $l10n,
+ private IConfig $config,
+ private INavigationManager $navigationManager,
+ private AppManager $appManager,
+ private CategoryFetcher $categoryFetcher,
+ private AppFetcher $appFetcher,
+ private IFactory $l10nFactory,
+ private BundleFetcher $bundleFetcher,
+ private Installer $installer,
+ private IURLGenerator $urlGenerator,
+ private LoggerInterface $logger,
+ private IInitialState $initialState,
+ private AppDiscoverFetcher $discoverFetcher,
+ private IClientService $clientService,
+ ) {
+ parent::__construct($appName, $request);
+ $this->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('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('settings', 'settings/empty', ['pageTitle' => $this->l10n->t('Settings')]);
+ $templateResponse->setContentSecurityPolicy($policy);
+
+ Util::addStyle('settings', 'settings');
+ Util::addScript('settings', 'vue-settings-apps-users-management');
+
+ return $templateResponse;
+ }
+
+ /**
+ * Get all active entries for the app discover section
+ */
+ #[NoCSRFRequired]
+ public function getAppDiscoverJSON(): JSONResponse {
+ $data = $this->discoverFetcher->get(true);
+ return new JSONResponse(array_values($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 = 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 ($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]
+ 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
+ /** @var Installer $installer */
+ $installer = Server::get(Installer::class);
+ $isDownloaded = $installer->isDownloaded($appId);
+
+ if (!$isDownloaded) {
+ $installer->downloadApp($appId);
+ }
+
+ $installer->installApp($appId);
+
+ if (count($groups) > 0) {
+ $this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
+ } else {
+ $this->appManager->enableApp($appId);
+ }
+ if (\OC_App::shouldUpgrade($appId)) {
+ $updateRequired = true;
+ }
+ }
+ 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();
+ }
+}