Skip to content

Commit

Permalink
Merge pull request #3749 from nextcloud/fix/remote-request-lazy
Browse files Browse the repository at this point in the history
fix: Avoid requesting remote endpoints during bootstrap
  • Loading branch information
juliushaertl committed Jun 11, 2024
2 parents ab324d8 + ea60d0e commit 974b8c0
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 188 deletions.
1 change: 1 addition & 0 deletions composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
'OCA\\Richdocuments\\Preview\\OpenDocument' => $baseDir . '/../lib/Preview/OpenDocument.php',
'OCA\\Richdocuments\\Preview\\Pdf' => $baseDir . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => $baseDir . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CachedRequestService' => $baseDir . '/../lib/Service/CachedRequestService.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => $baseDir . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\ConnectivityService' => $baseDir . '/../lib/Service/ConnectivityService.php',
'OCA\\Richdocuments\\Service\\DemoService' => $baseDir . '/../lib/Service/DemoService.php',
Expand Down
1 change: 1 addition & 0 deletions composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Preview\\OpenDocument' => __DIR__ . '/..' . '/../lib/Preview/OpenDocument.php',
'OCA\\Richdocuments\\Preview\\Pdf' => __DIR__ . '/..' . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => __DIR__ . '/..' . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CachedRequestService' => __DIR__ . '/..' . '/../lib/Service/CachedRequestService.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => __DIR__ . '/..' . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\ConnectivityService' => __DIR__ . '/..' . '/../lib/Service/ConnectivityService.php',
'OCA\\Richdocuments\\Service\\DemoService' => __DIR__ . '/..' . '/../lib/Service/DemoService.php',
Expand Down
8 changes: 7 additions & 1 deletion lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
}
}
}
25 changes: 19 additions & 6 deletions lib/Backgroundjobs/ObtainCapabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
}
}
163 changes: 163 additions & 0 deletions lib/Service/CachedRequestService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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;
}
}
110 changes: 33 additions & 77 deletions lib/Service/CapabilitiesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 === '') {
Expand All @@ -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);
}
}
4 changes: 2 additions & 2 deletions lib/Service/ConnectivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function __construct(
*/
public function testDiscovery(OutputInterface $output): void {
$this->discoveryService->resetCache();
$this->discoveryService->fetchFromRemote();
$this->discoveryService->fetch();
$output->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');

$this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
Expand All @@ -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('<info>✓ Fetched /hosting/capabilities endpoint</info>');

if ($this->capabilitiesService->getCapabilities() === []) {
Expand Down
Loading

0 comments on commit 974b8c0

Please sign in to comment.