Files
service_wopi/lib/Discovery/DiscoveryDocument.php
T
2026-09-06 21:14:22 -04:00

63 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\ServiceWopi\Discovery;
use DOMDocument;
use DOMElement;
use DOMXPath;
use RuntimeException;
final readonly class DiscoveryDocument
{
public const MAX_BYTES = 2 * 1024 * 1024;
/** @param list<Action> $actions */
private function __construct(public array $actions, public bool $hasProofKeys) {}
public static function parse(string $xml, ServerAddress $server): self
{
if ($xml === '' || strlen($xml) > self::MAX_BYTES || preg_match('/<!\s*(DOCTYPE|ENTITY)/i', $xml)) {
throw new RuntimeException('Discovery XML is empty, oversized, or contains a forbidden document type/entity.');
}
$previous = libxml_use_internal_errors(true);
try {
$document = new DOMDocument();
if (!$document->loadXML($xml, LIBXML_NONET | LIBXML_NOBLANKS)
|| $document->doctype !== null || $document->documentElement?->nodeName !== 'wopi-discovery') {
throw new RuntimeException('The server did not return valid WOPI discovery XML.');
}
$xpath = new DOMXPath($document);
$nodes = $xpath->query('/wopi-discovery/net-zone[@name="external-https"]/app/action');
// Euro-Office behind TLS termination can retain this zone label while
// advertising HTTPS action URLs. Validate every URL independently.
if ($nodes->length === 0) {
$nodes = $xpath->query('/wopi-discovery/net-zone[@name="external-http"]/app/action');
}
$actions = [];
foreach ($nodes as $node) {
if (!$node instanceof DOMElement) { continue; }
$name = $node->getAttribute('name');
$extension = strtolower($node->getAttribute('ext'));
if (!in_array($name, ['view', 'edit'], true) || !preg_match('/^[a-z0-9]+$/D', $extension)) {
continue;
}
$url = $node->getAttribute('urlsrc');
$server->assertTrustedAction($url);
$requirements = preg_split('/\s*,\s*/', trim($node->getAttribute('requires')), -1, PREG_SPLIT_NO_EMPTY);
$actions[] = new Action($extension, $name, $url, $requirements);
}
if ($actions === []) {
throw new RuntimeException('Discovery has no external view/edit actions. Check WOPI enablement and forwarded HTTPS headers.');
}
$proof = $xpath->query('/wopi-discovery/proof-key')->item(0);
return new self($actions, $proof instanceof DOMElement
&& $proof->getAttribute('modulus') !== '' && $proof->getAttribute('exponent') !== '');
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
}
}