Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: sync changes for Apps management migration to settings #417

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
1 change: 0 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
'requirements' => ['other' => '.+'], 'defaults' => ['other' => '']],

// ExApps actions
['name' => 'ExAppsPage#viewApps', 'url' => '/apps', 'verb' => 'GET' , 'root' => '/apps'],
['name' => 'ExAppsPage#listCategories', 'url' => '/apps/categories', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#listApps', 'url' => '/apps/list', 'verb' => 'GET' , 'root' => ''],
['name' => 'ExAppsPage#enableApp', 'url' => '/apps/enable/{appId}', 'verb' => 'GET' , 'root' => ''],
Expand Down
2 changes: 1 addition & 1 deletion js/app_api-adminSettings.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/app_api-adminSettings.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/app_api-filesplugin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/app_api-filesplugin.js.map

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions js/app_api-main.js

This file was deleted.

55 changes: 0 additions & 55 deletions js/app_api-main.js.LICENSE.txt

This file was deleted.

1 change: 0 additions & 1 deletion js/app_api-main.js.map

This file was deleted.

2 changes: 0 additions & 2 deletions js/app_api-src_views_Apps_vue.js

This file was deleted.

1 change: 0 additions & 1 deletion js/app_api-src_views_Apps_vue.js.map

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

This file was deleted.

This file was deleted.

37 changes: 0 additions & 37 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@
use OCP\Files\Events\Node\NodeTouchedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\SabrePluginEvent;
use OCP\Settings\Events\DeclarativeSettingsGetValueEvent;
use OCP\Settings\Events\DeclarativeSettingsRegisterFormEvent;
Expand Down Expand Up @@ -110,9 +104,7 @@ public function register(IRegistrationContext $context): void {
}

public function boot(IBootContext $context): void {
$server = $context->getServerContainer();
try {
$context->injectFn($this->registerExAppsManagementNavigation(...));
$context->injectFn($this->registerExAppsMenuEntries(...));
} catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable) {
}
Expand All @@ -127,35 +119,6 @@ public function registerDavAuth(): void {
});
}

/**
* Register ExApps management navigation entry right after default Apps management link.
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function registerExAppsManagementNavigation(IUserSession $userSession): void {
$container = $this->getContainer();
/** @var IGroupManager $groupManager */
$groupManager = $container->get(IGroupManager::class);
/** @var IUser $user */
$user = $userSession->getUser();
if ($groupManager->isInGroup($user->getUID(), 'admin')) {
$container->get(INavigationManager::class)->add(function () use ($container) {
$urlGenerator = $container->get(IURLGenerator::class);
$l10n = $container->get(IL10N::class);
return [
'id' => self::APP_ID,
'type' => 'settings',
'order' => 6,
'href' => $urlGenerator->linkToRoute('app_api.ExAppsPage.viewApps'),
'icon' => $urlGenerator->imagePath('app_api', 'app-dark.svg'),
'target' => '_blank',
'name' => $l10n->t('External Apps'),
];
});
}
}

private function registerExAppsMenuEntries(): void {
$container = $this->getContainer();
$menuEntryService = $container->get(TopMenuService::class);
Expand Down
55 changes: 7 additions & 48 deletions lib/Controller/ExAppsPageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
Expand Down Expand Up @@ -79,49 +77,6 @@ public function __construct(
$this->appManager = $appManager;
}

#[NoCSRFRequired]
public function viewApps(): TemplateResponse {
$defaultDaemonConfigName = $this->config->getAppValue(Application::APP_ID, 'default_daemon_config');

$appInitialData = [
'appstoreEnabled' => $this->config->getSystemValueBool('appstoreenabled', true),
'updateCount' => count($this->getExAppsWithUpdates()),
];

if ($defaultDaemonConfigName !== '') {
$daemonConfig = $this->daemonConfigService->getDaemonConfigByName($defaultDaemonConfigName);
if ($daemonConfig !== null) {
$this->dockerActions->initGuzzleClient($daemonConfig);
$daemonConfigAccessible = $this->dockerActions->ping($this->dockerActions->buildDockerUrl($daemonConfig));
$appInitialData['daemon_config_accessible'] = $daemonConfigAccessible;
$appInitialData['default_daemon_config'] = $daemonConfig->jsonSerialize();
unset($appInitialData['default_daemon_config']['deploy_config']['haproxy_password']); // do not expose password
if (!$daemonConfigAccessible) {
$this->logger->error(sprintf('Deploy daemon "%s" is not accessible by Nextcloud. Please verify its configuration', $daemonConfig->getName()));
}
}
}

$this->initialStateService->provideInitialState('apps', $appInitialData);

$templateResponse = new TemplateResponse(Application::APP_ID, 'main');
$policy = new ContentSecurityPolicy();
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
$templateResponse->setContentSecurityPolicy($policy);

return $templateResponse;
}

private function getExAppsWithUpdates(): array {
$apps = $this->exAppFetcher->get();
$appsWithUpdates = array_filter($apps, function (array $app) {
$exApp = $this->exAppService->getExApp($app['id']);
$newestVersion = $app['releases'][0]['version'];
return $exApp !== null && isset($app['releases'][0]['version']) && version_compare($newestVersion, $exApp->getVersion(), '>');
});
return array_values($appsWithUpdates);
}

/**
* Using the same algorithm of ExApps listing as for regular apps.
* Returns all apps for a category from the App Store
Expand Down Expand Up @@ -195,6 +150,7 @@ private function getAppsForCategory(string $requestedCategory = ''): array {

$formattedApps[] = [
'id' => $app['id'],
'app_api' => true,
'installed' => $exApp !== null, // if ExApp registered then it's assumed that it was already deployed (installed)
'appstore' => true,
'name' => $app['translations'][$currentLanguage]['name'] ?? $app['translations']['en']['name'],
Expand Down Expand Up @@ -228,6 +184,7 @@ private function getAppsForCategory(string $requestedCategory = ''): array {
'removable' => $existsLocally,
'active' => $exApp !== null && $exApp->getEnabled() === 1,
'needsDownload' => !$existsLocally,
'groups' => [],
'fromAppStore' => true,
'appstoreData' => $app,
'daemon' => $daemon,
Expand All @@ -242,7 +199,7 @@ private function getAppsForCategory(string $requestedCategory = ''): array {
#[NoCSRFRequired]
public function listApps(): JSONResponse {
$apps = $this->getAppsForCategory('');
$appsWithUpdate = $this->getExAppsWithUpdates();
$appsWithUpdate = $this->exAppFetcher->getExAppsWithUpdates();

$exApps = $this->exAppService->getExAppsList('all');
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
Expand Down Expand Up @@ -325,6 +282,7 @@ private function buildLocalAppsList(array $apps, array $exApps): array {

$formattedLocalApps[] = [
'id' => $app['id'],
'app_api' => true,
'appstore' => false,
'installed' => true,
'name' => $exApp->getName(),
Expand Down Expand Up @@ -357,6 +315,7 @@ private function buildLocalAppsList(array $apps, array $exApps): array {
'removable' => true, // to allow "remove" command for manual-install
'active' => $exApp->getEnabled() === 1,
'needsDownload' => false,
'groups' => [],
'fromAppStore' => false,
'appstoreData' => $app,
'daemon' => $daemon,
Expand Down Expand Up @@ -391,7 +350,7 @@ public function enableApp(string $appId): JSONResponse {
return new JSONResponse([]);
}

$appsWithUpdate = $this->getExAppsWithUpdates();
$appsWithUpdate = $this->exAppFetcher->getExAppsWithUpdates();
$appIdsWithUpdate = array_map(function (array $appWithUpdate) {
return $appWithUpdate['id'];
}, $appsWithUpdate);
Expand Down Expand Up @@ -426,7 +385,7 @@ public function disableApp(string $appId): JSONResponse {
#[PasswordConfirmationRequired]
#[NoCSRFRequired]
public function updateApp(string $appId): JSONResponse {
$appsWithUpdate = $this->getExAppsWithUpdates();
$appsWithUpdate = $this->exAppFetcher->getExAppsWithUpdates();
$appIdsWithUpdate = array_map(function (array $appWithUpdate) {
return $appWithUpdate['id'];
}, $appsWithUpdate);
Expand Down
38 changes: 21 additions & 17 deletions lib/Fetcher/AppAPIFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
use OCP\Files\NotPermittedException;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\Server;
use OCP\ServerVersion;
use OCP\Support\Subscription\IRegistry;
use Psr\Log\LoggerInterface;

abstract class AppAPIFetcher {
public const INVALIDATE_AFTER_SECONDS = 3600;
public const INVALIDATE_AFTER_SECONDS_UNSTABLE = 900;
public const RETRY_AFTER_FAILURE_SECONDS = 300;
public const APP_STORE_URL = 'https://apps.nextcloud.com/api/v1';

protected IAppData $appData;

Expand All @@ -37,15 +40,12 @@ public function __construct(
protected IConfig $config,
protected LoggerInterface $logger,
protected IRegistry $registry,
protected ServerVersion $serverVersion,
) {
$this->appData = $appDataFactory->get('appstore');
}

/**
* Fetches the response from the server
*
* @throws Exception
*/
protected function fetch(string $ETag, string $content): array {
$appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true);
Expand All @@ -72,7 +72,8 @@ protected function fetch(string $ETag, string $content): array {
$response = $client->get($this->getEndpoint(), $options);
} catch (ConnectException $e) {
$this->config->setAppValue(Application::APP_ID, 'appstore-appapi-fetcher-lastFailure', (string)time());
throw $e;
$this->logger->error('Failed to connect to the appstore', ['exception' => $e, 'app' => 'appstoreExAppFetcher']);
return [];
}

$responseJson = [];
Expand Down Expand Up @@ -102,8 +103,10 @@ protected function fetch(string $ETag, string $content): array {
public function get(bool $allowUnstable = false): array {
$appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true);
$internetavailable = $this->config->getSystemValueBool('has_internet_connection', true);
$isDefaultAppStore = $this->config->getSystemValueString('appstoreurl', self::APP_STORE_URL) === self::APP_STORE_URL;

if (!$appstoreenabled || !$internetavailable) {
if (!$appstoreenabled || (!$internetavailable && $isDefaultAppStore)) {
$this->logger->info('AppStore is disabled or this instance has no Internet connection to access the default app store', ['app' => 'appstoreExAppFetcher']);
return [];
}

Expand All @@ -117,12 +120,18 @@ public function get(bool $allowUnstable = false): array {
$file = $rootFolder->getFile($this->fileName);
$jsonBlob = json_decode($file->getContent(), true);

// Always get latests apps info if $allowUnstable
if (!$allowUnstable && is_array($jsonBlob)) {

if (is_array($jsonBlob)) {
// No caching when the version has been updated
if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) {
// If the timestamp is older than 3600 seconds request the files new
if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) {
$invalidateAfterSeconds = self::INVALIDATE_AFTER_SECONDS;

if ($allowUnstable) {
$invalidateAfterSeconds = self::INVALIDATE_AFTER_SECONDS_UNSTABLE;
}

if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - $invalidateAfterSeconds)) {
return $jsonBlob['data'];
}

Expand All @@ -141,24 +150,19 @@ public function get(bool $allowUnstable = false): array {
try {
$responseJson = $this->fetch($ETag, $content, $allowUnstable);

if (empty($responseJson)) {
if (empty($responseJson) || empty($responseJson['data'])) {
return [];
}

// Don't store the apps request file
if ($allowUnstable) {
return $responseJson['data'];
}

$file->putContent(json_encode($responseJson));
return json_decode($file->getContent(), true)['data'];
} catch (ConnectException $e) {
$this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
$this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreExAppFetcher']);
return [];
} catch (Exception $e) {
$this->logger->warning($e->getMessage(), [
'exception' => $e,
'app' => 'appstoreFetcher',
'app' => 'appstoreExAppFetcher',
]);
return [];
}
Expand Down Expand Up @@ -186,7 +190,7 @@ public function setVersion(string $version): void {
*/
protected function getChannel(): string {
if ($this->channel === null) {
$this->channel = $this->serverVersion->getChannel();
$this->channel = Server::get(ServerVersion::class)->getChannel();
}
return $this->channel;
}
Expand Down
Loading
Loading