feat: implement initial read only access

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-09-07 21:58:35 -04:00
parent 661e0e1213
commit 92e85aed47
17 changed files with 863 additions and 4 deletions
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace KTXM\ServiceWopi\Console;
use KTXM\ServiceWopi\Host\SessionStore;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'wopi:cleanup', description: 'Remove expired WOPI sessions and discovery cache records')]
class CleanupCommand extends Command
{
public function __construct(private readonly SessionStore $store) { parent::__construct(); }
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->store->cleanup(time());
$output->writeln('Expired WOPI records removed.');
return Command::SUCCESS;
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace KTXM\ServiceWopi\Console;
use KTXC\Service\TenantService;
use KTXM\ServiceWopi\Discovery\DiscoveryClient;
use KTXM\ServiceWopi\Host\Configuration;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'wopi:configure', description: 'Configure tenant office viewing and verify server discovery')]
class ConfigureCommand extends Command
{
public function __construct(private readonly TenantService $tenants, private readonly DiscoveryClient $discovery)
{
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('tenant', InputArgument::REQUIRED, 'Tenant identifier or domain')
->addArgument('office-server', InputArgument::REQUIRED, 'Office server HTTPS base URL')
->addOption('origin-server', null, InputOption::VALUE_REQUIRED, 'Application HTTPS origin reachable from the office server (defaults to the tenant domain)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
try {
$tenantArgument = $input->getArgument('tenant');
$tenant = $this->tenants->fetchById($tenantArgument);
$matchedByDomain = $tenant === null;
$tenant ??= $this->tenants->fetchByDomain($tenantArgument);
if ($tenant === null || !$tenant->getEnabled()) {
$io->error('An enabled tenant is required.');
return Command::FAILURE;
}
$origin = $input->getOption('origin-server');
if ($origin === null) {
$domains = array_values(array_unique($tenant->getDomains()?->getArrayCopy() ?? []));
if ($matchedByDomain) {
$origin = 'https://' . $tenantArgument;
} elseif (count($domains) === 1) {
$origin = 'https://' . $domains[0];
} else {
$io->error('Specify --origin-server or select the tenant by domain; its origin server cannot be determined unambiguously.');
return Command::FAILURE;
}
}
$wopi = $this->tenants->fetchServiceConfiguration($tenant->getIdentifier(), 'wopi') ?? [];
unset($wopi['origin_server']);
$wopi = array_replace($wopi, ['enabled' => true, 'office_server' => $input->getArgument('office-server'), 'origin_server' => $origin]);
$config = new Configuration($wopi);
$document = $this->discovery->fetch($config->office);
$viewable = array_filter($document->actions, static fn ($action) => $action->extension === 'docx' && $action->name === 'view' && $action->supportedBy([]));
if (!$document->hasProofKeys || $viewable === []) {
$io->error('The office server must advertise DOCX viewing and proof keys. Settings were not changed.');
return Command::FAILURE;
}
$wopi['office_server'] = $config->office->url;
$wopi['origin_server'] = $config->origin->origin;
if ($input->getOption('origin-server') === null && count(array_unique($tenant->getDomains()?->getArrayCopy() ?? [])) === 1) {
unset($wopi['origin_server']);
}
// Preserve the selected origin when a tenant has multiple aliases.
if (!$this->tenants->storeServiceConfiguration($tenant->getIdentifier(), 'wopi', $wopi)) {
$io->error('Could not save tenant office settings.');
return Command::FAILURE;
}
$io->success('Office viewing configured in the tenant database. Reload the application to register the viewer.');
$io->note('The office server must resolve the application hostname and trust its HTTPS certificate. Discovery does not test that return connection.');
return Command::SUCCESS;
} catch (\Throwable) {
$io->error('Configuration failed. Check the tenant, HTTPS URLs, and office discovery.');
return Command::FAILURE;
}
}
}