diff options
Diffstat (limited to 'lib/private/App/AppStore/Fetcher')
-rw-r--r-- | lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php | 100 | ||||
-rw-r--r-- | lib/private/App/AppStore/Fetcher/AppFetcher.php | 86 | ||||
-rw-r--r-- | lib/private/App/AppStore/Fetcher/CategoryFetcher.php | 40 | ||||
-rw-r--r-- | lib/private/App/AppStore/Fetcher/Fetcher.php | 123 |
4 files changed, 187 insertions, 162 deletions
diff --git a/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php new file mode 100644 index 00000000000..8389e525750 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php @@ -0,0 +1,100 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\App\AppStore\Fetcher; + +use DateTimeImmutable; +use OC\App\CompareVersion; +use OC\Files\AppData\Factory; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\Support\Subscription\IRegistry; +use Psr\Log\LoggerInterface; + +class AppDiscoverFetcher extends Fetcher { + + public const INVALIDATE_AFTER_SECONDS = 86400; + + public function __construct( + Factory $appDataFactory, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config, + LoggerInterface $logger, + IRegistry $registry, + private CompareVersion $compareVersion, + ) { + parent::__construct( + $appDataFactory, + $clientService, + $timeFactory, + $config, + $logger, + $registry + ); + + $this->fileName = 'discover.json'; + $this->endpointName = 'discover.json'; + } + + /** + * Get the app discover section entries + * + * @param bool $allowUnstable Include also upcoming entries + */ + public function get($allowUnstable = false) { + $entries = parent::get(false); + $now = new DateTimeImmutable(); + + return array_filter($entries, function (array $entry) use ($now, $allowUnstable) { + // Always remove expired entries + if (isset($entry['expiryDate'])) { + try { + $expiryDate = new DateTimeImmutable($entry['expiryDate']); + if ($expiryDate < $now) { + return false; + } + } catch (\Throwable $e) { + // Invalid expiryDate format + return false; + } + } + + // If not include upcoming entries, check for upcoming dates and remove those entries + if (!$allowUnstable && isset($entry['date'])) { + try { + $date = new DateTimeImmutable($entry['date']); + if ($date > $now) { + return false; + } + } catch (\Throwable $e) { + // Invalid date format + return false; + } + } + // Otherwise the entry is not time limited and should stay + return true; + }); + } + + public function getETag(): ?string { + $rootFolder = $this->appData->getFolder('/'); + + try { + $file = $rootFolder->getFile($this->fileName); + $jsonBlob = json_decode($file->getContent(), true); + + if (is_array($jsonBlob) && isset($jsonBlob['ETag'])) { + return (string)$jsonBlob['ETag']; + } + } catch (\Throwable $e) { + // ignore + } + return null; + } +} diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index 579f350b5bb..bbf4b00245b 100644 --- a/lib/private/App/AppStore/Fetcher/AppFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -1,31 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jakub Onderka <ahoj@jakubonderka.cz> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -39,23 +16,18 @@ use OCP\Support\Subscription\IRegistry; use Psr\Log\LoggerInterface; class AppFetcher extends Fetcher { - - /** @var CompareVersion */ - private $compareVersion; - - /** @var IRegistry */ - protected $registry; - /** @var bool */ private $ignoreMaxVersion; - public function __construct(Factory $appDataFactory, - IClientService $clientService, - ITimeFactory $timeFactory, - IConfig $config, - CompareVersion $compareVersion, - LoggerInterface $logger, - IRegistry $registry) { + public function __construct( + Factory $appDataFactory, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config, + private CompareVersion $compareVersion, + LoggerInterface $logger, + protected IRegistry $registry, + ) { parent::__construct( $appDataFactory, $clientService, @@ -65,9 +37,6 @@ class AppFetcher extends Fetcher { $registry ); - $this->compareVersion = $compareVersion; - $this->registry = $registry; - $this->fileName = 'apps.json'; $this->endpointName = 'apps.json'; $this->ignoreMaxVersion = true; @@ -86,7 +55,8 @@ class AppFetcher extends Fetcher { /** @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, apps could not be retrieved. Try again later.', ['app' => 'appstoreFetcher']); return []; } @@ -100,7 +70,7 @@ class AppFetcher extends Fetcher { foreach ($app['releases'] as $release) { // Exclude all nightly and pre-releases if required if (($allowNightly || $release['isNightly'] === false) - && ($allowPreReleases || strpos($release['version'], '-') === false)) { + && ($allowPreReleases || !str_contains($release['version'], '-'))) { // Exclude all versions not compatible with the current version try { $versionParser = new VersionParser(); @@ -109,23 +79,23 @@ class AppFetcher extends Fetcher { $minServerVersion = $serverVersion->getMinimumVersion(); $maxServerVersion = $serverVersion->getMaximumVersion(); $minFulfilled = $this->compareVersion->isCompatible($ncVersion, $minServerVersion, '>='); - $maxFulfilled = $maxServerVersion !== '' && - $this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<='); + $maxFulfilled = $maxServerVersion !== '' + && $this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<='); $isPhpCompatible = true; if (($release['rawPhpVersionSpec'] ?? '*') !== '*') { $phpVersion = $versionParser->getVersion($release['rawPhpVersionSpec']); $minPhpVersion = $phpVersion->getMinimumVersion(); $maxPhpVersion = $phpVersion->getMaximumVersion(); $minPhpFulfilled = $minPhpVersion === '' || $this->compareVersion->isCompatible( - PHP_VERSION, - $minPhpVersion, - '>=' - ); + PHP_VERSION, + $minPhpVersion, + '>=' + ); $maxPhpFulfilled = $maxPhpVersion === '' || $this->compareVersion->isCompatible( - PHP_VERSION, - $maxPhpVersion, - '<=' - ); + PHP_VERSION, + $maxPhpVersion, + '<=' + ); $isPhpCompatible = $minPhpFulfilled && $maxPhpFulfilled; } @@ -181,11 +151,13 @@ class AppFetcher extends Fetcher { $this->ignoreMaxVersion = $ignoreMaxVersion; } - - public function get($allowUnstable = false) { + 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 diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php index afe051e6281..d7857d41bee 100644 --- a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -34,12 +14,14 @@ use OCP\Support\Subscription\IRegistry; use Psr\Log\LoggerInterface; class CategoryFetcher extends Fetcher { - public function __construct(Factory $appDataFactory, - IClientService $clientService, - ITimeFactory $timeFactory, - IConfig $config, - LoggerInterface $logger, - IRegistry $registry) { + public function __construct( + Factory $appDataFactory, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config, + LoggerInterface $logger, + IRegistry $registry, + ) { parent::__construct( $appDataFactory, $clientService, diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index 788f15c183f..24876675d60 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -1,32 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Steffen Lindner <mail@steffen-lindner.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -38,47 +14,38 @@ use OCP\Files\IAppData; use OCP\Files\NotFoundException; 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 Fetcher { 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'; /** @var IAppData */ protected $appData; - /** @var IClientService */ - protected $clientService; - /** @var ITimeFactory */ - protected $timeFactory; - /** @var IConfig */ - protected $config; - /** @var LoggerInterface */ - protected $logger; - /** @var IRegistry */ - protected $registry; /** @var string */ protected $fileName; /** @var string */ protected $endpointName; - /** @var string */ - protected $version; - /** @var string */ - protected $channel; - - public function __construct(Factory $appDataFactory, - IClientService $clientService, - ITimeFactory $timeFactory, - IConfig $config, - LoggerInterface $logger, - IRegistry $registry) { + /** @var ?string */ + protected $version = null; + /** @var ?string */ + protected $channel = null; + + public function __construct( + Factory $appDataFactory, + protected IClientService $clientService, + protected ITimeFactory $timeFactory, + protected IConfig $config, + protected LoggerInterface $logger, + protected IRegistry $registry, + ) { $this->appData = $appDataFactory->get('appstore'); - $this->clientService = $clientService; - $this->timeFactory = $timeFactory; - $this->config = $config; - $this->logger = $logger; - $this->registry = $registry; } /** @@ -89,7 +56,7 @@ abstract class Fetcher { * * @return array */ - protected function fetch($ETag, $content) { + protected function fetch($ETag, $content, $allowUnstable = false) { $appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true); if ((int)$this->config->getAppValue('settings', 'appstore-fetcher-lastFailure', '0') > time() - self::RETRY_AFTER_FAILURE_SECONDS) { return []; @@ -109,10 +76,13 @@ abstract class Fetcher { ]; } - // If we have a valid subscription key, send it to the appstore - $subscriptionKey = $this->config->getAppValue('support', 'subscription_key'); - if ($this->registry->delegateHasValidSubscription() && $subscriptionKey) { - $options['headers']['X-NC-Subscription-Key'] = $subscriptionKey; + if ($this->config->getSystemValueString('appstoreurl', self::APP_STORE_URL) === self::APP_STORE_URL) { + // If we have a valid subscription key, send it to the appstore + $subscriptionKey = $this->config->getAppValue('support', 'subscription_key'); + if ($this->registry->delegateHasValidSubscription() && $subscriptionKey) { + $options['headers'] ??= []; + $options['headers']['X-NC-Subscription-Key'] = $subscriptionKey; + } } $client = $this->clientService->newClient(); @@ -120,7 +90,8 @@ abstract class Fetcher { $response = $client->get($this->getEndpoint(), $options); } catch (ConnectException $e) { $this->config->setAppValue('settings', 'appstore-fetcher-lastFailure', (string)time()); - throw $e; + $this->logger->error('Failed to connect to the app store', ['exception' => $e]); + return []; } $responseJson = []; @@ -142,16 +113,18 @@ abstract class Fetcher { } /** - * Returns the array with the categories on the appstore server + * Returns the array with the entries on the appstore server * * @param bool [$allowUnstable] Allow unstable releases * @return array */ public function get($allowUnstable = false) { $appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true); - $internetavailable = $this->config->getSystemValue('has_internet_connection', 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' => 'appstoreFetcher']); return []; } @@ -165,14 +138,17 @@ abstract class Fetcher { $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']; } @@ -191,15 +167,10 @@ abstract class Fetcher { 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) { @@ -220,7 +191,7 @@ abstract class Fetcher { */ protected function getVersion() { if ($this->version === null) { - $this->version = $this->config->getSystemValue('version', '0.0.0'); + $this->version = $this->config->getSystemValueString('version', '0.0.0'); } return $this->version; } @@ -239,7 +210,7 @@ abstract class Fetcher { */ protected function getChannel() { if ($this->channel === null) { - $this->channel = \OC_Util::getChannel(); + $this->channel = Server::get(ServerVersion::class)->getChannel(); } return $this->channel; } @@ -253,6 +224,6 @@ abstract class Fetcher { } protected function getEndpoint(): string { - return $this->config->getSystemValue('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName; + return $this->config->getSystemValueString('appstoreurl', 'https://apps.nextcloud.com/api/v1') . '/' . $this->endpointName; } } |