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.php122
-rw-r--r--lib/private/App/AppStore/Fetcher/CategoryFetcher.php54
-rw-r--r--lib/private/App/AppStore/Fetcher/Fetcher.php138
4 files changed, 233 insertions, 181 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 4dc517879e8..bbf4b00245b 100644
--- a/lib/private/App/AppStore/Fetcher/AppFetcher.php
+++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php
@@ -1,33 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ (skjnldsv) <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;
use OC\App\AppStore\Version\VersionParser;
@@ -36,41 +12,33 @@ use OC\Files\AppData\Factory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
-use OCP\ILogger;
+use OCP\Support\Subscription\IRegistry;
+use Psr\Log\LoggerInterface;
class AppFetcher extends Fetcher {
-
- /** @var CompareVersion */
- private $compareVersion;
-
/** @var bool */
private $ignoreMaxVersion;
- /**
- * @param Factory $appDataFactory
- * @param IClientService $clientService
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- * @param CompareVersion $compareVersion
- * @param ILogger $logger
- */
- public function __construct(Factory $appDataFactory,
- IClientService $clientService,
- ITimeFactory $timeFactory,
- IConfig $config,
- CompareVersion $compareVersion,
- ILogger $logger) {
+ public function __construct(
+ Factory $appDataFactory,
+ IClientService $clientService,
+ ITimeFactory $timeFactory,
+ IConfig $config,
+ private CompareVersion $compareVersion,
+ LoggerInterface $logger,
+ protected IRegistry $registry,
+ ) {
parent::__construct(
$appDataFactory,
$clientService,
$timeFactory,
$config,
- $logger
+ $logger,
+ $registry
);
$this->fileName = 'apps.json';
$this->endpointName = 'apps.json';
- $this->compareVersion = $compareVersion;
$this->ignoreMaxVersion = true;
}
@@ -87,8 +55,13 @@ class AppFetcher extends Fetcher {
/** @var mixed[] $response */
$response = parent::fetch($ETag, $content);
- $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily';
- $allowNightly = $allowUnstable || $this->getChannel() === 'daily';
+ 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 [];
+ }
+
+ $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
+ $allowNightly = $allowUnstable || $this->getChannel() === 'daily' || $this->getChannel() === 'git';
foreach ($response['data'] as $dataKey => $app) {
$releases = [];
@@ -97,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();
@@ -106,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;
}
@@ -130,7 +103,9 @@ class AppFetcher extends Fetcher {
$releases[] = $release;
}
} catch (\InvalidArgumentException $e) {
- $this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
+ $this->logger->warning($e->getMessage(), [
+ 'exception' => $e,
+ ]);
}
}
}
@@ -146,7 +121,9 @@ class AppFetcher extends Fetcher {
foreach ($releases as $release) {
$versions[] = $release['version'];
}
- usort($versions, 'version_compare');
+ usort($versions, function ($version1, $version2) {
+ return version_compare($version1, $version2);
+ });
$versions = array_reverse($versions);
if (isset($versions[0])) {
$highestVersion = $versions[0];
@@ -173,4 +150,23 @@ class AppFetcher extends Fetcher {
$this->fileName = $fileName;
$this->ignoreMaxVersion = $ignoreMaxVersion;
}
+
+ 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
+ if (is_array($allowList) && $this->registry->delegateHasValidSubscription()) {
+ return array_filter($apps, function ($app) use ($allowList) {
+ return in_array($app['id'], $allowList);
+ });
+ }
+
+ return $apps;
+ }
}
diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php
index e34fbee8c58..d7857d41bee 100644
--- a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php
+++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php
@@ -1,58 +1,36 @@
<?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;
use OC\Files\AppData\Factory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
-use OCP\ILogger;
+use OCP\Support\Subscription\IRegistry;
+use Psr\Log\LoggerInterface;
class CategoryFetcher extends Fetcher {
- /**
- * @param Factory $appDataFactory
- * @param IClientService $clientService
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- * @param ILogger $logger
- */
- public function __construct(Factory $appDataFactory,
- IClientService $clientService,
- ITimeFactory $timeFactory,
- IConfig $config,
- ILogger $logger) {
+ public function __construct(
+ Factory $appDataFactory,
+ IClientService $clientService,
+ ITimeFactory $timeFactory,
+ IConfig $config,
+ LoggerInterface $logger,
+ IRegistry $registry,
+ ) {
parent::__construct(
$appDataFactory,
$clientService,
$timeFactory,
$config,
- $logger
+ $logger,
+ $registry
);
+
$this->fileName = 'categories.json';
$this->endpointName = 'categories.json';
}
diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php
index c5d83fcb1df..24876675d60 100644
--- a/lib/private/App/AppStore/Fetcher/Fetcher.php
+++ b/lib/private/App/AppStore/Fetcher/Fetcher.php
@@ -1,34 +1,9 @@
<?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) <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;
use GuzzleHttp\Exception\ConnectException;
@@ -39,48 +14,38 @@ use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
-use OCP\ILogger;
+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 Ilogger */
- protected $logger;
+
/** @var string */
protected $fileName;
/** @var string */
protected $endpointName;
- /** @var string */
- protected $version;
- /** @var string */
- protected $channel;
-
- /**
- * @param Factory $appDataFactory
- * @param IClientService $clientService
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- * @param ILogger $logger
- */
- public function __construct(Factory $appDataFactory,
- IClientService $clientService,
- ITimeFactory $timeFactory,
- IConfig $config,
- ILogger $logger) {
+ /** @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;
}
/**
@@ -91,8 +56,8 @@ abstract class Fetcher {
*
* @return array
*/
- protected function fetch($ETag, $content) {
- $appstoreenabled = $this->config->getSystemValue('appstoreenabled', true);
+ 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 [];
}
@@ -111,12 +76,22 @@ abstract class Fetcher {
];
}
+ 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();
try {
$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 = [];
@@ -138,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->getSystemValue('appstoreenabled', true);
- $internetavailable = $this->config->getSystemValue('has_internet_connection', true);
+ $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' => 'appstoreFetcher']);
return [];
}
@@ -161,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'];
}
@@ -187,22 +167,20 @@ 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) {
$this->logger->warning('Could not connect to appstore: ' . $e->getMessage(), ['app' => 'appstoreFetcher']);
return [];
} catch (\Exception $e) {
- $this->logger->logException($e, ['app' => 'appstoreFetcher', 'level' => ILogger::WARN]);
+ $this->logger->warning($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'appstoreFetcher',
+ ]);
return [];
}
}
@@ -213,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;
}
@@ -232,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;
}
@@ -246,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;
}
}