f7cloud_client/apps/richdocuments/lib/Service/FederationService.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

231 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Richdocuments\Service;
use OCA\Federation\TrustedServers;
use OCA\Files_Sharing\External\Storage as SharingExternalStorage;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Db\Direct;
use OCA\Richdocuments\Db\Wopi;
use OCA\Richdocuments\TokenManager;
use OCP\AutoloadNotAllowedException;
use OCP\Files\File;
use OCP\Files\InvalidPathException;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Security\ITrustedDomainHelper;
use OCP\Share\IShare;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class FederationService {
/** @var ICache */
private $cache;
/** @var TrustedServers */
private $trustedServers;
public function __construct(
ICacheFactory $cacheFactory,
private IClientService $clientService,
private LoggerInterface $logger,
private TokenManager $tokenManager,
private AppConfig $appConfig,
private IRequest $request,
private IURLGenerator $urlGenerator,
private ITrustedDomainHelper $trustedDomainHelper,
) {
$this->cache = $cacheFactory->createDistributed('richdocuments_remote/');
try {
$this->trustedServers = \OCP\Server::get(\OCA\Federation\TrustedServers::class);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface|AutoloadNotAllowedException) {
}
}
public function getTrustedServers(): array {
if (!$this->trustedServers) {
return [];
}
return array_map(fn (array $server) => $server['url'], $this->trustedServers->getServers());
}
/**
* @param $remote
* @return string
* @throws \Exception
*/
public function getRemoteCollaboraURL($remote) {
// If no protocol is provided we default to https
if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) {
$remote = 'https://' . $remote;
}
if (!$this->isTrustedRemote($remote)) {
throw new \Exception('Unable to determine collabora URL of remote server ' . $remote . ' - Remote is not a trusted server');
}
if ($this->trustedDomainHelper->isTrustedUrl($remote)) {
return $this->appConfig->getCollaboraUrlInternal();
}
$remoteCollabora = $this->cache->get('richdocuments_remote/' . $remote);
if ($remoteCollabora !== null) {
return $remoteCollabora;
}
try {
$client = $this->clientService->newClient();
$response = $client->get($remote . '/ocs/v2.php/apps/richdocuments/api/v1/federation?format=json', ['timeout' => 30]);
$data = \json_decode($response->getBody(), true);
$remoteCollabora = $data['ocs']['data']['wopi_url'];
$this->cache->set('richdocuments_remote/' . $remote, $remoteCollabora, 3600);
return $remoteCollabora;
} catch (\Throwable $e) {
$this->logger->info('Unable to determine collabora URL of remote server ' . $remote, ['exception' => $e]);
$this->cache->set('richdocuments_remote/' . $remote, '', 300);
}
return '';
}
public function isTrustedRemote($domainWithPort) {
if (str_starts_with($domainWithPort, 'http://') || str_starts_with($domainWithPort, 'https://')) {
$port = parse_url($domainWithPort, PHP_URL_PORT);
$domainWithPort = parse_url($domainWithPort, PHP_URL_HOST) . ($port ? ':' . $port : '');
}
if ($this->appConfig->isTrustedDomainAllowedForFederation() && $this->trustedServers !== null && $this->trustedServers->isTrustedServer($domainWithPort)) {
return true;
}
$domain = $this->getDomainWithoutPort($domainWithPort);
$trustedList = array_merge($this->appConfig->getGlobalScaleTrustedHosts(), [$this->request->getServerHost()]);
if (!is_array($trustedList)) {
return false;
}
foreach ($trustedList as $trusted) {
if (!is_string($trusted)) {
break;
}
// This regular expression ensures that wildcards for trusted domains
// are parsed properly in order to match subdomains:
// *.example.com => /^[-\.a-zA-Z0-9]*\.example\.com$/i
$regex = '/^' . implode('[-\.a-zA-Z0-9]*', array_map(fn ($v) => preg_quote($v, '/'), explode('*', $trusted))) . '$/i';
if (preg_match($regex, $domain) || preg_match($regex, $domainWithPort)) {
return true;
}
}
return false;
}
/**
* Strips a potential port from a domain (in format domain:port)
* @param string $host
* @return string $host without appended port
*/
private function getDomainWithoutPort($host) {
$pos = strrpos($host, ':');
if ($pos !== false) {
$port = substr($host, $pos + 1);
if (is_numeric($port)) {
$host = substr($host, 0, $pos);
}
}
return $host;
}
/** @return Wopi|null */
public function getRemoteFileDetails(string $remote, string $remoteToken) {
$cacheKey = md5($remote . $remoteToken);
$remoteWopi = $this->cache->get($cacheKey);
if ($remoteWopi !== null) {
return Wopi::fromParams($remoteWopi);
}
if (!$this->isTrustedRemote($remote)) {
$this->logger->info('COOL-Federation-Source: Unable to determine collabora URL of remote server ' . $remote . ' for token ' . $remoteToken . ' - Remote is not a trusted server');
return null;
}
try {
$this->logger->debug('COOL-Federation-Source: Fetching remote file details from ' . $remote . ' for token ' . $remoteToken);
$client = $this->clientService->newClient();
$response = $client->post($remote . '/ocs/v2.php/apps/richdocuments/api/v1/federation?format=json', [
'timeout' => 30,
'body' => [
'token' => $remoteToken
]
]);
$responseBody = $response->getBody();
$data = \json_decode($responseBody, true, 512);
$this->logger->debug('COOL-Federation-Source: Received remote file details for ' . $remoteToken . ' from ' . $remote . ': ' . json_encode($data['ocs']['data']));
$this->cache->set($cacheKey, $data['ocs']['data']);
return Wopi::fromParams($data['ocs']['data']);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['message' => 'COOL-Federation-Source: Unable to fetch remote file details for ' . $remoteToken . ' from ' . $remote, 'exception' => $e]);
}
return null;
}
/**
* @param File $item
* @return string|null
* @throws NotFoundException
* @throws InvalidPathException
*/
public function getRemoteRedirectURL(File $item, ?Direct $direct = null, ?IShare $share = null) {
if (!$item->getStorage()->instanceOfStorage(SharingExternalStorage::class)) {
return null;
}
$remote = $item->getStorage()->getRemote();
$remoteCollabora = $this->getRemoteCollaboraURL($remote);
if ($remoteCollabora !== '') {
$shareToken = $share ? $share->getToken() : null;
$wopi = $this->tokenManager->newInitiatorToken($remote, $item, $shareToken, ($direct !== null), ($direct ? $direct->getUid() : null));
$initiatorServer = $this->urlGenerator->getAbsoluteURL('/');
$initiatorToken = $wopi->getToken();
/**
* If the request to open a file originates from a direct token we might need to fetch the initiator user details when the initiator wopi token is accessed
* as the user might origin on a 3rd instance
*/
if ($direct && !empty($direct->getInitiatorHost()) && !empty($direct->getInitiatorToken())) {
$this->tokenManager->extendWithInitiatorUserToken($wopi, $direct->getInitiatorHost(), $direct->getInitiatorToken());
}
$url = rtrim($remote, '/') . '/index.php/apps/richdocuments/remote';
$params = [
'shareToken' => $item->getStorage()->getToken(),
'remoteServer' => $initiatorServer,
'remoteServerToken' => $initiatorToken,
];
if ($item->getInternalPath() !== '') {
$params['filePath'] = $item->getInternalPath();
}
return $url . '?' . http_build_query($params);
}
throw new NotFoundException('Failed to connect to remote collabora instance for ' . $item->getId());
}
}