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

Merged
merged 12 commits into from
Oct 29, 2024
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
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
30 changes: 19 additions & 11 deletions lib/Fetcher/ExAppFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,29 @@

namespace OCA\AppAPI\Fetcher;

use Exception;
use InvalidArgumentException;
use OC\App\AppStore\Version\VersionParser;
use OC\App\CompareVersion;
use OC\Files\AppData\Factory;
use OCA\AppAPI\Service\ExAppService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\ServerVersion;
use OCP\Server;
use OCP\Support\Subscription\IRegistry;
use Psr\Log\LoggerInterface;

class ExAppFetcher extends AppAPIFetcher {
private CompareVersion $compareVersion;
private bool $ignoreMaxVersion;

public function __construct(
Factory $appDataFactory,
IClientService $clientService,
ITimeFactory $timeFactory,
IConfig $config,
CompareVersion $compareVersion,
private CompareVersion $compareVersion,
LoggerInterface $logger,
protected IRegistry $registry,
protected ServerVersion $serverVersion,
) {
parent::__construct(
$appDataFactory,
Expand All @@ -37,26 +35,22 @@ public function __construct(
$config,
$logger,
$registry,
$serverVersion
);

$this->compareVersion = $compareVersion;

$this->fileName = 'appapi_apps.json';
$this->endpointName = 'appapi_apps.json';
$this->ignoreMaxVersion = true;
}

/**
* Only returns the latest compatible app release in the releases array
*
* @throws Exception
*/
protected function fetch(string $ETag, string $content, bool $allowUnstable = false): array {
/** @var mixed[] $response */
$response = parent::fetch($ETag, $content);

if (empty($response)) {
if (!isset($response['data']) || $response['data'] === null) {
$this->logger->warning('Response from appstore is invalid, ExApps could not be retrieved. Try again later.', ['app' => 'appstoreExAppFetcher']);
return [];
}

Expand Down Expand Up @@ -150,6 +144,9 @@ public function get($allowUnstable = false): array {
$allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';

$apps = parent::get($allowPreReleases);
if (empty($apps)) {
return [];
}
$allowList = $this->config->getSystemValue('appsallowlist');

// If the admin specified a allow list, filter apps from the appstore
Expand All @@ -161,4 +158,15 @@ public function get($allowUnstable = false): array {

return $apps;
}

public function getExAppsWithUpdates(): array {
$apps = $this->get();
$appsWithUpdates = array_filter($apps, function (array $app) {
$exAppService = Server::get(ExAppService::class);
$exApp = $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);
}
}
Loading
Loading