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

[stable30] chore: sync changes for Apps management migration to settings #428

Merged
merged 14 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

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
103 changes: 19 additions & 84 deletions lib/Controller/ExAppsPageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +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;
use OCP\IRequest;
Expand All @@ -37,89 +34,22 @@
* ExApps actions controller similar to default one with project-specific changes and additions
*/
class ExAppsPageController extends Controller {
private IInitialState $initialStateService;
private IConfig $config;
private AppAPIService $service;
private DaemonConfigService $daemonConfigService;
private DockerActions $dockerActions;
private CategoryFetcher $categoryFetcher;
private IFactory $l10nFactory;
private ExAppFetcher $exAppFetcher;
private IL10N $l10n;
private LoggerInterface $logger;
private IAppManager $appManager;

public function __construct(
IRequest $request,
IConfig $config,
IInitialState $initialStateService,
AppAPIService $service,
DaemonConfigService $daemonConfigService,
DockerActions $dockerActions,
CategoryFetcher $categoryFetcher,
IFactory $l10nFactory,
ExAppFetcher $exAppFetcher,
IL10N $l10n,
LoggerInterface $logger,
IAppManager $appManager,
private readonly IConfig $config,
private readonly AppAPIService $service,
private readonly DaemonConfigService $daemonConfigService,
private readonly DockerActions $dockerActions,
private readonly CategoryFetcher $categoryFetcher,
private readonly IFactory $l10nFactory,
private readonly ExAppFetcher $exAppFetcher,
private readonly IL10N $l10n,
private readonly LoggerInterface $logger,
private readonly IAppManager $appManager,
private readonly ExAppService $exAppService,
) {
parent::__construct(Application::APP_ID, $request);

$this->initialStateService = $initialStateService;
$this->config = $config;
$this->service = $service;
$this->daemonConfigService = $daemonConfigService;
$this->dockerActions = $dockerActions;
$this->categoryFetcher = $categoryFetcher;
$this->l10nFactory = $l10nFactory;
$this->l10n = $l10n;
$this->exAppFetcher = $exAppFetcher;
$this->logger = $logger;
$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);
}

/**
Expand Down Expand Up @@ -195,6 +125,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 +159,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 +174,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,12 +257,14 @@ private function buildLocalAppsList(array $apps, array $exApps): array {

$formattedLocalApps[] = [
'id' => $app['id'],
'app_api' => true,
'appstore' => false,
'installed' => true,
'name' => $exApp->getName(),
'description' => '',
'description' => $this->l10n->t('This app is not installed from the AppStore. No extra information available. Only enable/disable and remove actions are allowed.'),
'summary' => '',
'license' => '',
'licence' => '',
'author' => '',
'shipped' => false,
'version' => $exApp->getVersion(),
Expand All @@ -357,6 +291,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 +326,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 +361,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
34 changes: 19 additions & 15 deletions lib/Fetcher/AppAPIFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

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 @@ -42,8 +44,6 @@ public function __construct(

/**
* Fetches the response from the server
*
* @throws Exception
*/
protected function fetch(string $ETag, string $content): array {
$appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true);
Expand All @@ -70,7 +70,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 @@ -100,8 +101,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 @@ -115,12 +118,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 @@ -139,24 +148,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
Loading
Loading