aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/App/AppStore/Fetcher
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/App/AppStore/Fetcher')
-rw-r--r--lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php100
-rw-r--r--lib/private/App/AppStore/Fetcher/AppFetcher.php86
-rw-r--r--lib/private/App/AppStore/Fetcher/CategoryFetcher.php40
-rw-r--r--lib/private/App/AppStore/Fetcher/Fetcher.php123
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;
}
}