generated from Nodarx/template
92e85aed47
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
124 lines
6.0 KiB
PHP
124 lines
6.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace KTXM\ServiceWopi\Host;
|
|
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Service\TenantService;
|
|
use KTXM\ServiceWopi\Discovery\DiscoveryClient;
|
|
use KTXM\ServiceWopi\Discovery\DiscoveryDocument;
|
|
use KTXM\ServiceWopi\Discovery\ServerAddress;
|
|
|
|
class WopiService
|
|
{
|
|
private const WOPI_PATH_PREFIX = '/m/service_wopi/wopi/files/';
|
|
|
|
public const FORMATS = [
|
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly TenantService $tenants,
|
|
private readonly SessionStore $store,
|
|
private readonly Documents $documents,
|
|
private readonly DiscoveryClient $client,
|
|
private readonly ProofValidator $proof,
|
|
) {}
|
|
|
|
public function configuration(string $tenant): Configuration
|
|
{
|
|
$configuration = $this->tenants->fetchServiceConfiguration($tenant, 'wopi') ?? [];
|
|
$domains = $this->tenants->fetchById($tenant)?->getDomains()?->getArrayCopy() ?? [];
|
|
return new Configuration($configuration, $domains);
|
|
}
|
|
|
|
public function capabilities(string $tenant, string $user): array
|
|
{
|
|
$this->documents->authorize($tenant, $user);
|
|
$config = $this->configuration($tenant);
|
|
$discovery = $this->discovery($tenant, $config->office);
|
|
if (!$discovery->hasProofKeys) { throw new HostException('The office server must publish proof keys.', 503); }
|
|
$formats = [];
|
|
foreach ($discovery->actions as $action) {
|
|
if ($action->name === 'view' && $action->supportedBy([]) && isset(self::FORMATS[$action->extension])) {
|
|
$formats[$action->extension] = self::FORMATS[$action->extension];
|
|
}
|
|
}
|
|
return ['mode' => 'view', 'mimeTypes' => array_values($formats), 'storage' => [['provider' => 'default', 'service' => 'personal']]];
|
|
}
|
|
|
|
public function launch(string $tenant, string $user, string $resource): array
|
|
{
|
|
$config = $this->configuration($tenant);
|
|
$file = $this->documents->read($tenant, $user, $resource, $config->maxBytes);
|
|
$extension = strtolower(pathinfo($file['label'], PATHINFO_EXTENSION));
|
|
if (!isset(self::FORMATS[$extension])) { throw new HostException('This document format is not available for office viewing.', 415); }
|
|
$discovery = $this->discovery($tenant, $config->office);
|
|
if (!$discovery->hasProofKeys) { throw new HostException('The office server must publish proof keys.', 503); }
|
|
$selected = null;
|
|
foreach ($discovery->actions as $action) {
|
|
if ($action->extension === $extension && $action->name === 'view' && $action->supportedBy([])) {
|
|
$selected = $action;
|
|
break;
|
|
}
|
|
}
|
|
if ($selected === null) { throw new HostException('The office server cannot view this document format.', 415); }
|
|
$id = $this->store->fileId($tenant, $user, $resource);
|
|
$token = bin2hex(random_bytes(32));
|
|
$expires = time() + $config->lifetime;
|
|
$this->store->save($token, [
|
|
'tenant' => $tenant, 'user' => $user, 'resource' => $resource, 'file' => $id,
|
|
'mode' => 'view', 'expires' => $expires, 'server' => $config->office->url,
|
|
'host' => $config->origin->origin, 'maxBytes' => $config->maxBytes,
|
|
'version' => $file['version'],
|
|
]);
|
|
return [
|
|
'actionUrl' => $selected->launchUrl($config->origin->origin . self::WOPI_PATH_PREFIX . $id),
|
|
'accessToken' => $token, 'accessTokenTtl' => $expires * 1000,
|
|
'officeOrigin' => $config->office->origin, 'mode' => 'view',
|
|
];
|
|
}
|
|
|
|
/** @return array{session: array, file: array} */
|
|
public function access(string $tenant, string $fileId, Request $request): array
|
|
{
|
|
$token = $request->query->all()['access_token'] ?? '';
|
|
if (!is_string($token) || !preg_match('/^[a-f0-9]{64}$/D', $token)) {
|
|
throw new HostException('Invalid access token.', 401);
|
|
}
|
|
$session = $this->store->find($tenant, $fileId, $token, time());
|
|
if ($session === null) { throw new HostException('Invalid access token.', 401); }
|
|
// Exact origin and raw query reconstruction avoids proxy-normalized proof URLs.
|
|
$uri = $request->getRequestUri();
|
|
if (!str_starts_with($uri, self::WOPI_PATH_PREFIX . $fileId)) { throw new HostException('Invalid file path.', 404); }
|
|
$url = $session['host'] . $uri;
|
|
$server = new ServerAddress($session['server']);
|
|
$valid = fn (DiscoveryDocument $discovery): bool => $this->proof->valid(
|
|
$discovery->proofKeys, $token, $url,
|
|
$request->headers->get('X-WOPI-TimeStamp', ''),
|
|
$request->headers->get('X-WOPI-Proof', ''),
|
|
$request->headers->get('X-WOPI-ProofOld', ''), time(),
|
|
);
|
|
$discovery = $this->discovery($tenant, $server);
|
|
if (!$valid($discovery) && !$valid($this->discovery($tenant, $server, true))) {
|
|
throw new HostException('Invalid WOPI proof.', 500);
|
|
}
|
|
$file = $this->documents->read($tenant, $session['user'], $session['resource'], $session['maxBytes']);
|
|
if (!hash_equals($session['version'], $file['version'])) {
|
|
throw new HostException('The document changed. Reopen it to view the current version.', 404);
|
|
}
|
|
return ['session' => $session, 'file' => $file];
|
|
}
|
|
|
|
private function discovery(string $tenant, ServerAddress $server, bool $refresh = false): DiscoveryDocument
|
|
{
|
|
$xml = $refresh ? null : $this->store->cached($tenant, $server->url, time());
|
|
if ($xml !== null) { return DiscoveryDocument::parse($xml, $server); }
|
|
$xml = $this->client->fetchXml($server);
|
|
$document = DiscoveryDocument::parse($xml, $server);
|
|
$this->store->cache($tenant, $server->url, $xml, time() + 3600);
|
|
return $document;
|
|
}
|
|
}
|