From 18a300f932e2486ff376c7ca6adc50f27f5b15cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 11 Jun 2024 17:35:17 +0200 Subject: [PATCH] fix: Avoid requesting remote endpoints during bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will add some abstraction layer for cached request data as used by capabilities and discovery endpoints. By default it will always return the cached data which will never expire (backed by a file in app data in case the memory cache vanishes). Signed-off-by: Julius Härtl --- lib/AppInfo/Application.php | 8 +- lib/Backgroundjobs/ObtainCapabilities.php | 25 ++- lib/Service/CachedRequestService.php | 179 ++++++++++++++++++++++ lib/Service/CapabilitiesService.php | 110 ++++--------- lib/Service/ConnectivityService.php | 4 +- lib/Service/DiscoveryService.php | 125 +++------------ 6 files changed, 263 insertions(+), 188 deletions(-) create mode 100644 lib/Service/CachedRequestService.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f1b063817c..3b7449ea00 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -120,12 +120,18 @@ public function checkAndEnableCODEServer() { $appConfig->setAppValue('wopi_url', $new_wopi_url); $appConfig->setAppValue('disable_certificate_verification', 'yes'); + /** @var DiscoveryService $discoveryService */ $discoveryService = $this->getContainer()->get(DiscoveryService::class); + /** @var CapabilitiesService $capabilitiesService */ $capabilitiesService = $this->getContainer()->get(CapabilitiesService::class); $discoveryService->resetCache(); $capabilitiesService->resetCache(); - $capabilitiesService->fetchFromRemote(); + try { + $capabilitiesService->fetch(); + $discoveryService->fetch(); + } catch (\Exception $e) { + } } } } diff --git a/lib/Backgroundjobs/ObtainCapabilities.php b/lib/Backgroundjobs/ObtainCapabilities.php index ff62e75183..3156d77b93 100644 --- a/lib/Backgroundjobs/ObtainCapabilities.php +++ b/lib/Backgroundjobs/ObtainCapabilities.php @@ -7,21 +7,34 @@ namespace OCA\Richdocuments\Backgroundjobs; use OCA\Richdocuments\Service\CapabilitiesService; +use OCA\Richdocuments\Service\DiscoveryService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; class ObtainCapabilities extends TimedJob { - /** @var CapabilitiesService */ - private $capabilitiesService; - - public function __construct(ITimeFactory $time, CapabilitiesService $capabilitiesService) { + public function __construct( + ITimeFactory $time, + private LoggerInterface $logger, + private CapabilitiesService $capabilitiesService, + private DiscoveryService $discoveryService, + ) { parent::__construct($time); - $this->capabilitiesService = $capabilitiesService; $this->setInterval(60 * 60); } protected function run($argument) { - $this->capabilitiesService->fetchFromRemote(); + try { + $this->capabilitiesService->fetch(); + } catch (\Exception $e) { + $this->logger->error('Failed to fetch capabilities: ' . $e->getMessage(), ['exception' => $e]); + } + + try { + $this->discoveryService->fetch(); + } catch (\Exception $e) { + $this->logger->error('Failed to fetch discovery: ' . $e->getMessage(), ['exception' => $e]); + } } } diff --git a/lib/Service/CachedRequestService.php b/lib/Service/CachedRequestService.php new file mode 100644 index 0000000000..5cf67ad59d --- /dev/null +++ b/lib/Service/CachedRequestService.php @@ -0,0 +1,179 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +namespace OCA\Richdocuments\Service; + +use OCA\Richdocuments\AppInfo\Application; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\IAppConfig; +use OCP\ICacheFactory; +use Psr\Log\LoggerInterface; + +abstract class CachedRequestService { + + public function __construct( + private IClientService $clientService, + private ICacheFactory $cacheFactory, + private IAppDataFactory $appDataFactory, + private IAppConfig $appConfig, + private LoggerInterface $logger, + private string $cacheKey, + ) { + } + + /** + * Method to implement sending a request and returning the result as a string + * @throw \Exception in case the request fails + */ + abstract protected function sendRequest(IClient $client): string; + + public function get(): ?string { + $cache = $this->cacheFactory->createDistributed('richdocuments'); + + if ($cached = $cache->get($this->cacheKey)) { + return $cached; + } + + $folder = $this->getAppDataFolder(); + if ($folder->fileExists($this->cacheKey)) { + $value = $folder->getFile($this->cacheKey)->getContent(); + $cache->set($this->cacheKey, $value, 3600); + return $value; + } + + return null; + } + + public function getLastUpdate(): ?int { + $folder = $this->getAppDataFolder(); + if (!$folder->fileExists($this->cacheKey)) { + return null; + } + return $folder->getFile($this->cacheKey)->getMTime(); + } + + /** + * Cached value will be kept if the request fails + * + * @return string + * @throws \Exception + */ + final public function fetch(): string { + $cache = $this->cacheFactory->createDistributed('richdocuments'); + $client = $this->clientService->newClient(); + + $startTime = microtime(true); + $response = $this->sendRequest($client); + $duration = round(((microtime(true) - $startTime)), 3); + $this->logger->info('Fetched remote endpoint from ' . $this->cacheKey . ' in ' . $duration . ' seconds'); + + $this->getAppDataFolder()->newFile($this->cacheKey, $response); + $cache->set($this->cacheKey, $response); + return $response; + } + + public function resetCache(): void { + $cache = $this->cacheFactory->createDistributed('richdocuments'); + $cache->remove($this->cacheKey); + $folder = $this->getAppDataFolder(); + if ($folder->fileExists($this->cacheKey)) { + $folder->getFile($this->cacheKey)->delete(); + } + } + + protected function getDefaultRequestOptions(): array { + $options = [ + 'timeout' => 5, + 'nextcloud' => [ + 'allow_local_address' => true + ] + ]; + + if ($this->appConfig->getValueString('richdocuments', 'disable_certificate_verification') === 'yes') { + $options['verify'] = false; + } + + if ($this->isProxyStarting()) { + $options['timeout'] = 180; + } + + return $options; + } + + private function getAppDataFolder(): ISimpleFolder { + $appData = $this->appDataFactory->get(Application::APPNAME); + try { + $folder = $appData->getFolder('remoteData'); + } catch (NotFoundException $e) { + $folder = $appData->newFolder('remoteData'); + } + return $folder; + } + + /** + * @return boolean indicating if proxy.php is in initialize or false otherwise + */ + private function isProxyStarting(): bool { + $url = $this->appConfig->getValueString('richdocuments', 'wopi_url', ''); + $usesProxy = false; + $proxyPos = strrpos($url, 'proxy.php'); + if ($proxyPos !== false) { + $usesProxy = true; + } + + if ($usesProxy === true) { + $statusUrl = substr($url, 0, $proxyPos); + $statusUrl = $statusUrl . 'proxy.php?status'; + + $client = $this->clientService->newClient(); + $options = ['timeout' => 5, 'nextcloud' => ['allow_local_address' => true]]; + + if ($this->appConfig->getValueString('richdocuments', 'disable_certificate_verification') === 'yes') { + $options['verify'] = false; + } + + try { + $response = $client->get($statusUrl, $options); + + if ($response->getStatusCode() === 200) { + $body = json_decode($response->getBody(), true); + + if ($body['status'] === 'starting' + || $body['status'] === 'stopped' + || $body['status'] === 'restarting') { + return true; + } + } + } catch (\Exception $e) { + // ignore + } + } + + return false; + } +} diff --git a/lib/Service/CapabilitiesService.php b/lib/Service/CapabilitiesService.php index 1bf4652733..f61cf0f0c4 100644 --- a/lib/Service/CapabilitiesService.php +++ b/lib/Service/CapabilitiesService.php @@ -8,52 +8,42 @@ use OCA\Richdocuments\AppInfo\Application; use OCP\App\IAppManager; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; -use OCP\ICache; +use OCP\IAppConfig; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use Psr\Log\LoggerInterface; -class CapabilitiesService { - /** @var IConfig */ - private $config; - /** @var IClientService */ - private $clientService; - /** @var ICache */ - private $cache; - /** @var IAppManager */ - private $appManager; - /** @var IL10N */ - private $l10n; - /** @var LoggerInterface */ - private $logger; - - /** @var array */ - private $capabilities; - - - public function __construct(IConfig $config, IClientService $clientService, ICacheFactory $cacheFactory, IAppManager $appManager, IL10N $l10n, LoggerInterface $logger) { - $this->config = $config; - $this->clientService = $clientService; - $this->cache = $cacheFactory->createDistributed('richdocuments'); - $this->appManager = $appManager; - $this->l10n = $l10n; - $this->logger = $logger; +class CapabilitiesService extends CachedRequestService { + + private ?array $capabilities = null; + + public function __construct( + private IClientService $clientService, + private ICacheFactory $cacheFactory, + private IAppDataFactory $appDataFactory, + private IAppConfig $appConfig, + private LoggerInterface $logger, + private IConfig $config, + private IAppManager $appManager, + private IL10N $l10n, + ) { + parent::__construct( + $this->clientService, + $this->cacheFactory, + $this->appDataFactory, + $this->appConfig, + $this->logger, + 'capabilities', + ); } public function getCapabilities() { if (!$this->capabilities) { - $this->capabilities = $this->cache->get('capabilities'); - } - - $isARM64 = php_uname('m') === 'aarch64'; - $CODEAppID = $isARM64 ? 'richdocumentscode_arm64' : 'richdocumentscode'; - $isCODEInstalled = $this->appManager->isEnabledForUser($CODEAppID); - $isCODEEnabled = strpos($this->config->getAppValue('richdocuments', 'wopi_url'), 'proxy.php?req=') !== false; - $shouldRecheckCODECapabilities = $isCODEInstalled && $isCODEEnabled && ($this->capabilities === null || count($this->capabilities) === 0); - if ($this->capabilities === null || $shouldRecheckCODECapabilities) { - $this->fetchFromRemote(); + $this->capabilities = $this->getParsedCapabilities(); } if (!is_array($this->capabilities)) { @@ -119,10 +109,6 @@ public function hasOtherOOXMLApps(): bool { return false; } - public function resetCache(): void { - $this->cache->remove('capabilities'); - } - public function getCapabilitiesEndpoint(): ?string { $remoteHost = $this->config->getAppValue('richdocuments', 'wopi_url'); if ($remoteHost === '') { @@ -131,43 +117,13 @@ public function getCapabilitiesEndpoint(): ?string { return rtrim($remoteHost, '/') . '/hosting/capabilities'; } - public function fetchFromRemote($throw = false): void { - if (!$this->getCapabilitiesEndpoint()) { - return; - } - - $client = $this->clientService->newClient(); - $options = ['timeout' => 45, 'nextcloud' => ['allow_local_address' => true]]; - - if ($this->config->getAppValue('richdocuments', 'disable_certificate_verification') === 'yes') { - $options['verify'] = false; - } - - try { - $startTime = microtime(true); - $response = $client->get($this->getCapabilitiesEndpoint(), $options); - $duration = round(((microtime(true) - $startTime)), 3); - $this->logger->info('Fetched capabilities endpoint from ' . $this->getCapabilitiesEndpoint(). ' in ' . $duration . ' seconds'); - $responseBody = $response->getBody(); - $capabilities = \json_decode($responseBody, true); - - if (!is_array($capabilities)) { - $capabilities = []; - } - } catch (\Exception $e) { - $this->logger->error('Failed to fetch the Collabora capabilities endpoint: ' . $e->getMessage(), [ 'exception' => $e ]); - if ($throw) { - throw $e; - } - $capabilities = []; - } - - $this->capabilities = $capabilities; - $ttl = 3600; - if (count($capabilities) === 0) { - $ttl = 60; - } + protected function sendRequest(IClient $client): string { + $response = $client->get($this->getCapabilitiesEndpoint(), $this->getDefaultRequestOptions()); + return (string)$response->getBody(); + } - $this->cache->set('capabilities', $capabilities, $ttl); + private function getParsedCapabilities() { + $response = $this->get(); + return json_decode($response, true); } } diff --git a/lib/Service/ConnectivityService.php b/lib/Service/ConnectivityService.php index 8e687593fe..113c7dfff6 100644 --- a/lib/Service/ConnectivityService.php +++ b/lib/Service/ConnectivityService.php @@ -25,7 +25,7 @@ public function __construct( */ public function testDiscovery(OutputInterface $output): void { $this->discoveryService->resetCache(); - $this->discoveryService->fetchFromRemote(); + $this->discoveryService->fetch(); $output->writeln('✓ Fetched /hosting/discovery endpoint'); $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); @@ -38,7 +38,7 @@ public function testDiscovery(OutputInterface $output): void { public function testCapabilities(OutputInterface $output): void { $this->capabilitiesService->resetCache(); - $this->capabilitiesService->fetchFromRemote(true); + $this->capabilitiesService->fetch(true); $output->writeln('✓ Fetched /hosting/capabilities endpoint'); if ($this->capabilitiesService->getCapabilities() === []) { diff --git a/lib/Service/DiscoveryService.php b/lib/Service/DiscoveryService.php index 3d85055ecf..7cd10be775 100644 --- a/lib/Service/DiscoveryService.php +++ b/lib/Service/DiscoveryService.php @@ -28,120 +28,41 @@ namespace OCA\Richdocuments\Service; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; -use OCP\Http\Client\IResponse; -use OCP\ICache; +use OCP\IAppConfig; use OCP\ICacheFactory; use OCP\IConfig; use Psr\Log\LoggerInterface; -class DiscoveryService { - private IClientService $clientService; - private ICache $cache; - private IConfig $config; - private LoggerInterface $logger; - - private ?string $discovery = null; - +class DiscoveryService extends CachedRequestService { public function __construct( - IClientService $clientService, - ICacheFactory $cacheFactory, - IConfig $config, - LoggerInterface $logger + private IClientService $clientService, + private ICacheFactory $cacheFactory, + private IAppDataFactory $appDataFactory, + private IAppConfig $appConfig, + private LoggerInterface $logger, + private IConfig $config, ) { - $this->clientService = $clientService; - $this->cache = $cacheFactory->createDistributed('richdocuments'); - $this->config = $config; - $this->logger = $logger; + parent::__construct( + $this->clientService, + $this->cacheFactory, + $this->appDataFactory, + $this->appConfig, + $this->logger, + 'discovery', + ); } - public function get(): ?string { - if ($this->discovery) { - return $this->discovery; - } - - $this->discovery = $this->cache->get('discovery'); - if (!$this->discovery) { - $response = $this->fetchFromRemote(); - $responseBody = $response->getBody(); - $this->discovery = $responseBody; - $this->cache->set('discovery', $this->discovery, 3600); - } - - return $this->discovery; + protected function sendRequest(IClient $client): string { + $response = $client->get($this->getDiscoveryEndpoint(), $this->getDefaultRequestOptions()); + return (string)$response->getBody(); } - /** - * @throws \Exception if a network error occurs - */ - public function fetchFromRemote(): IResponse { + private function getDiscoveryEndpoint(): string { $remoteHost = $this->config->getAppValue('richdocuments', 'wopi_url'); - $wopiDiscovery = rtrim($remoteHost, '/') . '/hosting/discovery'; - - $client = $this->clientService->newClient(); - $options = ['timeout' => 45, 'nextcloud' => ['allow_local_address' => true]]; - - if ($this->config->getAppValue('richdocuments', 'disable_certificate_verification') === 'yes') { - $options['verify'] = false; - } - - if ($this->isProxyStarting($wopiDiscovery)) { - $options['timeout'] = 180; - } - - $startTime = microtime(true); - $response = $client->get($wopiDiscovery, $options); - $duration = round(((microtime(true) - $startTime)), 3); - $this->logger->info('Fetched discovery endpoint from ' . $wopiDiscovery . ' in ' . $duration . ' seconds'); - - return $response; - } - - public function resetCache(): void { - $this->cache->remove('discovery'); - $this->discovery = null; - } - - /** - * @return boolean indicating if proxy.php is in initialize or false otherwise - */ - private function isProxyStarting(string $url): bool { - $usesProxy = false; - $proxyPos = strrpos($url, 'proxy.php'); - if ($proxyPos === false) { - $usesProxy = false; - } else { - $usesProxy = true; - } - - if ($usesProxy === true) { - $statusUrl = substr($url, 0, $proxyPos); - $statusUrl = $statusUrl . 'proxy.php?status'; - - $client = $this->clientService->newClient(); - $options = ['timeout' => 5, 'nextcloud' => ['allow_local_address' => true]]; - - if ($this->config->getAppValue('richdocuments', 'disable_certificate_verification') === 'yes') { - $options['verify'] = false; - } - - try { - $response = $client->get($statusUrl, $options); - - if ($response->getStatusCode() === 200) { - $body = json_decode($response->getBody(), true); - - if ($body['status'] === 'starting' - || $body['status'] === 'stopped' - || $body['status'] === 'restarting') { - return true; - } - } - } catch (\Exception $e) { - // ignore - } - } + return rtrim($remoteHost, '/') . '/hosting/discovery'; - return false; } }