mirror of
https://github.com/nextcloud/server.git
synced 2026-05-22 01:55:56 -04:00
refactor(Fetcher): properly type AppStore fetcher
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
e6a1bfd354
commit
e7a2f660c7
9 changed files with 224 additions and 32 deletions
|
|
@ -128,7 +128,7 @@ class AppSettingsController extends Controller {
|
|||
#[NoCSRFRequired]
|
||||
public function getAppDiscoverJSON(): JSONResponse {
|
||||
$data = $this->discoverFetcher->get(true);
|
||||
return new JSONResponse(array_values($data));
|
||||
return new JSONResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1161,6 +1161,7 @@ return array(
|
|||
'OC\\App\\AppStore\\Fetcher\\AppFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/AppFetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\Fetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/Fetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\ResponseDefinitions' => $baseDir . '/lib/private/App/AppStore/Fetcher/ResponseDefinitions.php',
|
||||
'OC\\App\\AppStore\\Version\\Version' => $baseDir . '/lib/private/App/AppStore/Version/Version.php',
|
||||
'OC\\App\\AppStore\\Version\\VersionParser' => $baseDir . '/lib/private/App/AppStore/Version/VersionParser.php',
|
||||
'OC\\App\\CompareVersion' => $baseDir . '/lib/private/App/CompareVersion.php',
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\App\\AppStore\\Fetcher\\AppFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/AppFetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\Fetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/Fetcher.php',
|
||||
'OC\\App\\AppStore\\Fetcher\\ResponseDefinitions' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/ResponseDefinitions.php',
|
||||
'OC\\App\\AppStore\\Version\\Version' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/Version.php',
|
||||
'OC\\App\\AppStore\\Version\\VersionParser' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/VersionParser.php',
|
||||
'OC\\App\\CompareVersion' => __DIR__ . '/../../..' . '/lib/private/App/CompareVersion.php',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ use OCP\IConfig;
|
|||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Fetch app discover section entries from the app store
|
||||
*
|
||||
* @psalm-import-type AppStoreFetcherDiscoverElement from ResponseDefinitions
|
||||
* @template-extends Fetcher<AppStoreFetcherDiscoverElement>
|
||||
*/
|
||||
class AppDiscoverFetcher extends Fetcher {
|
||||
|
||||
public const INVALIDATE_AFTER_SECONDS = 86400;
|
||||
|
|
@ -46,13 +52,14 @@ class AppDiscoverFetcher extends Fetcher {
|
|||
* Get the app discover section entries
|
||||
*
|
||||
* @param bool $allowUnstable Include also upcoming entries
|
||||
* @return list<AppStoreFetcherDiscoverElement>
|
||||
*/
|
||||
#[\Override]
|
||||
public function get($allowUnstable = false) {
|
||||
public function get($allowUnstable = false): array {
|
||||
$entries = parent::get(false);
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
return array_filter($entries, function (array $entry) use ($now, $allowUnstable) {
|
||||
return array_values(array_filter($entries, function (array $entry) use ($now, $allowUnstable) {
|
||||
// Always remove expired entries
|
||||
if (isset($entry['expiryDate'])) {
|
||||
try {
|
||||
|
|
@ -80,7 +87,7 @@ class AppDiscoverFetcher extends Fetcher {
|
|||
}
|
||||
// Otherwise the entry is not time limited and should stay
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public function getETag(): ?string {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ use OCP\IConfig;
|
|||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Fetch app data from the app store
|
||||
*
|
||||
* @psalm-import-type AppStoreFetcherApp from ResponseDefinitions
|
||||
* @psalm-import-type AppStoreFetcherAppReleasesEntry from ResponseDefinitions
|
||||
* @template-extends Fetcher<AppStoreFetcherApp>
|
||||
*/
|
||||
class AppFetcher extends Fetcher {
|
||||
/** @var bool */
|
||||
private $ignoreMaxVersion;
|
||||
|
|
@ -45,15 +52,10 @@ class AppFetcher extends Fetcher {
|
|||
/**
|
||||
* Only returns the latest compatible app release in the releases array
|
||||
*
|
||||
* @param string $ETag
|
||||
* @param string $content
|
||||
* @param bool [$allowUnstable] Allow unstable releases
|
||||
*
|
||||
* @return array
|
||||
* @inheritDoc
|
||||
*/
|
||||
#[\Override]
|
||||
protected function fetch($ETag, $content, $allowUnstable = false) {
|
||||
/** @var mixed[] $response */
|
||||
protected function fetch($ETag, $content, $allowUnstable = false): array {
|
||||
$response = parent::fetch($ETag, $content);
|
||||
|
||||
if (!isset($response['data']) || $response['data'] === null) {
|
||||
|
|
@ -65,6 +67,7 @@ class AppFetcher extends Fetcher {
|
|||
$allowNightly = $allowUnstable || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
|
||||
|
||||
foreach ($response['data'] as $dataKey => $app) {
|
||||
/** @var list<AppStoreFetcherAppReleasesEntry> $releases */
|
||||
$releases = [];
|
||||
|
||||
// Filter all compatible releases
|
||||
|
|
@ -113,7 +116,7 @@ class AppFetcher extends Fetcher {
|
|||
|
||||
if (empty($releases)) {
|
||||
// Remove apps that don't have a matching release
|
||||
$response['data'][$dataKey] = [];
|
||||
unset($response['data'][$dataKey]);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -141,11 +144,6 @@ class AppFetcher extends Fetcher {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @param string $fileName
|
||||
* @param bool $ignoreMaxVersion
|
||||
*/
|
||||
#[\Override]
|
||||
public function setVersion(string $version, string $fileName = 'apps.json', bool $ignoreMaxVersion = true) {
|
||||
parent::setVersion($version);
|
||||
|
|
@ -165,9 +163,9 @@ class AppFetcher extends Fetcher {
|
|||
|
||||
// If the admin specified a allow list, filter apps from the appstore
|
||||
if (is_array($allowList) && $this->registry->delegateHasValidSubscription()) {
|
||||
return array_filter($apps, function ($app) use ($allowList) {
|
||||
return array_values(array_filter($apps, function (array $app) use ($allowList) {
|
||||
return in_array($app['id'], $allowList);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
return $apps;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ use OCP\IConfig;
|
|||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Fetch categories from the app store server.
|
||||
* The categories are listed as an array containing the id and the translations for the category.
|
||||
* The key of the translations array is the language code and the value is an array containing the name and description of the category.
|
||||
*
|
||||
* @psalm-import-type AppStoreFetcherCategory from ResponseDefinitions
|
||||
* @template-extends Fetcher<AppStoreFetcherCategory>
|
||||
*/
|
||||
class CategoryFetcher extends Fetcher {
|
||||
public function __construct(
|
||||
Factory $appDataFactory,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ use OCP\ServerVersion;
|
|||
use OCP\Support\Subscription\IRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Base class for fetching app store data
|
||||
*
|
||||
* @template T of array
|
||||
*/
|
||||
abstract class Fetcher {
|
||||
public const INVALIDATE_AFTER_SECONDS = 3600;
|
||||
public const INVALIDATE_AFTER_SECONDS_UNSTABLE = 900;
|
||||
|
|
@ -53,18 +58,19 @@ abstract class Fetcher {
|
|||
/**
|
||||
* Fetches the response from the server
|
||||
*
|
||||
* @param string $ETag
|
||||
* @param string $content
|
||||
* @param string $ETag - The ETag of the cached response
|
||||
* @param string $content - The content of the response
|
||||
* @param bool $allowUnstable - Allow unstable releases
|
||||
*
|
||||
* @return array
|
||||
* @return array{data: list<T>, ETag?: string, timestamp: int, ncversion: string}|array<never, never>
|
||||
*/
|
||||
protected function fetch($ETag, $content, $allowUnstable = false) {
|
||||
$appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true);
|
||||
protected function fetch($ETag, $content, $allowUnstable = false): array {
|
||||
$appstoreEnabled = $this->config->getSystemValueBool('appstoreenabled', true);
|
||||
if ((int)$this->config->getAppValue('settings', 'appstore-fetcher-lastFailure', '0') > time() - self::RETRY_AFTER_FAILURE_SECONDS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$appstoreenabled) {
|
||||
if (!$appstoreEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -117,15 +123,15 @@ abstract class Fetcher {
|
|||
/**
|
||||
* Returns the array with the entries on the appstore server
|
||||
*
|
||||
* @param bool [$allowUnstable] Allow unstable releases
|
||||
* @return array
|
||||
* @param bool $allowUnstable - Allow unstable releases
|
||||
* @return list<T>
|
||||
*/
|
||||
public function get($allowUnstable = false) {
|
||||
$appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true);
|
||||
$internetavailable = $this->config->getSystemValueBool('has_internet_connection', true);
|
||||
public function get($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 && $isDefaultAppStore)) {
|
||||
if (!$appstoreEnabled || (!$internetAvailable && $isDefaultAppStore)) {
|
||||
$this->logger->info('AppStore is disabled or this instance has no Internet connection to access the default app store', ['app' => 'appstoreFetcher']);
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
147
lib/private/App/AppStore/Fetcher/ResponseDefinitions.php
Normal file
147
lib/private/App/AppStore/Fetcher/ResponseDefinitions.php
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\App\AppStore\Fetcher;
|
||||
|
||||
/**
|
||||
* @psalm-type AppStoreFetcherCategory = array{
|
||||
* id: string,
|
||||
* translations: array<string, array{
|
||||
* name: string,
|
||||
* description: string,
|
||||
* }>,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppAuthor = array{
|
||||
* name: string,
|
||||
* mail: string,
|
||||
* homepage: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppScreenshot = array{
|
||||
* url: string,
|
||||
* smallThumbnail: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppTranslationsEntry = array{
|
||||
* name: string,
|
||||
* summary: string,
|
||||
* description: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppTranslations = array{ en: AppStoreFetcherAppTranslationsEntry } & array<string, AppStoreFetcherAppTranslationsEntry>
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppReleasesEntryTranslations = array{ en: array{ changelog: string } } & array<string, array{ changelog: string }>
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppReleasesEntryRequirements = array{
|
||||
* id: string,
|
||||
* versionSpec: string,
|
||||
* rawVersionSpec: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherAppReleasesEntry = array{
|
||||
* version: string,
|
||||
* phpExtensions?: list<AppStoreFetcherAppReleasesEntryRequirements>,
|
||||
* databases?: list<AppStoreFetcherAppReleasesEntryRequirements>,
|
||||
* shellCommands?: list<string>,
|
||||
* phpVersionSpec: string,
|
||||
* platformVersionSpec: string,
|
||||
* minIntSize: int,
|
||||
* download: string,
|
||||
* created: string,
|
||||
* licenses?: list<string>,
|
||||
* lastModified: string,
|
||||
* isNightly: boolean,
|
||||
* rawPhpVersionSpec: string,
|
||||
* rawPlatformVersionSpec: string,
|
||||
* signature: string,
|
||||
* translations: AppStoreFetcherAppReleasesEntryTranslations,
|
||||
* signatureDigest: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherApp = array{
|
||||
* id: string,
|
||||
* authors?: list<AppStoreFetcherAppAuthor>,
|
||||
* categories: string[],
|
||||
* certificate: string,
|
||||
* created: string,
|
||||
* lastModified: string,
|
||||
* translations: AppStoreFetcherAppTranslations,
|
||||
* releases?: list<AppStoreFetcherAppReleasesEntry>,
|
||||
* screenshots?: list<AppStoreFetcherAppScreenshot>,
|
||||
* adminDocs: string,
|
||||
* userDocs: string,
|
||||
* developerDocs: string,
|
||||
* discussion: string,
|
||||
* issueTracker: string,
|
||||
* website: string,
|
||||
* isFeatured: boolean,
|
||||
* ratingRecent: float,
|
||||
* ratingOverall: float,
|
||||
* ratingNumRecent: int,
|
||||
* ratingNumOverall: int,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverLocalizedString = array{en: string} & array<string, string>
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverMediaSource = array{
|
||||
* mime: string,
|
||||
* src: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverMediaContent = array{
|
||||
* src: AppStoreFetcherDiscoverMediaSource|list<AppStoreFetcherDiscoverMediaSource>,
|
||||
* alt: string,
|
||||
* link?: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverLocalizedMediaContent = array{en: AppStoreFetcherDiscoverMediaContent} & array<string, AppStoreFetcherDiscoverMediaContent>
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverMediaObject = array{
|
||||
* content: AppStoreFetcherDiscoverLocalizedMediaContent,
|
||||
* alignment?: 'start'|'end'|'center',
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverContainerBase = array{
|
||||
* id?: string,
|
||||
* order?: int,
|
||||
* headline?: AppStoreFetcherDiscoverLocalizedString,
|
||||
* text?: AppStoreFetcherDiscoverLocalizedString,
|
||||
* link?: string,
|
||||
* date?: string,
|
||||
* expiryDate?: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverAppElement = array{
|
||||
* type: 'app',
|
||||
* appId: string,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverPostElement = AppStoreFetcherDiscoverContainerBase & array{
|
||||
* type: 'post',
|
||||
* media?: AppStoreFetcherDiscoverMediaObject,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverShowcaseElement = AppStoreFetcherDiscoverContainerBase & array{
|
||||
* type: 'showcase',
|
||||
* content: list<AppStoreFetcherDiscoverAppElement|AppStoreFetcherDiscoverPostElement>,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverCarouselElement = AppStoreFetcherDiscoverContainerBase & array{
|
||||
* type: 'carousel',
|
||||
* content: list<AppStoreFetcherDiscoverPostElement>,
|
||||
* }
|
||||
*
|
||||
* @psalm-type AppStoreFetcherDiscoverElement =
|
||||
* AppStoreFetcherDiscoverPostElement|
|
||||
* AppStoreFetcherDiscoverShowcaseElement|
|
||||
* AppStoreFetcherDiscoverCarouselElement
|
||||
*/
|
||||
final class ResponseDefinitions {
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ class DependencyAnalyzer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array of missing dependencies
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
public function analyze(array $appInfo, bool $ignoreMax = false): array {
|
||||
if (isset($appInfo['dependencies'])) {
|
||||
|
|
@ -104,6 +104,9 @@ class DependencyAnalyzer {
|
|||
return $this->compare($first, $second, '<');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzePhpVersion(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (isset($dependencies['php']['@attributes']['min-version'])) {
|
||||
|
|
@ -127,6 +130,9 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeArchitecture(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (!isset($dependencies['architecture'])) {
|
||||
|
|
@ -150,6 +156,9 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeDatabases(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (!isset($dependencies['database'])) {
|
||||
|
|
@ -176,6 +185,9 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeCommands(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (!isset($dependencies['command'])) {
|
||||
|
|
@ -202,6 +214,9 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeLibraries(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (!isset($dependencies['lib'])) {
|
||||
|
|
@ -243,6 +258,9 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeOS(array $dependencies): array {
|
||||
$missing = [];
|
||||
if (!isset($dependencies['os'])) {
|
||||
|
|
@ -267,10 +285,16 @@ class DependencyAnalyzer {
|
|||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
private function analyzeServer(array $appInfo, bool $ignoreMax): array {
|
||||
return $this->analyzeServerVersion($this->platform->getOcVersion(), $appInfo, $ignoreMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string> of missing dependencies
|
||||
*/
|
||||
public function analyzeServerVersion(string $serverVersion, array $appInfo, bool $ignoreMax): array {
|
||||
$missing = [];
|
||||
$minVersion = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue