Skip to content

Commit

Permalink
Discovery: Add DiscoveryLoader, convert CollaboraDiscovery to a value…
Browse files Browse the repository at this point in the history
… object.
  • Loading branch information
donquixote committed Jan 17, 2025
1 parent 104b6b9 commit 4cab629
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 160 deletions.
4 changes: 2 additions & 2 deletions collabora_online.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ services:
class: Drupal\collabora_online\Discovery\CollaboraDiscoveryFetcher
Drupal\collabora_online\Discovery\CachingDiscoveryFetcher:
decorates: Drupal\collabora_online\Discovery\CollaboraDiscoveryFetcherInterface
Drupal\collabora_online\Discovery\CollaboraDiscoveryInterface:
class: Drupal\collabora_online\Discovery\CollaboraDiscovery
Drupal\collabora_online\Discovery\DiscoveryLoaderInterface:
class: Drupal\collabora_online\Discovery\DiscoveryLoader
Drupal\collabora_online\Jwt\JwtTranscoderInterface:
class: Drupal\collabora_online\Jwt\JwtTranscoder
Drupal\collabora_online\MediaHelperInterface:
Expand Down
9 changes: 5 additions & 4 deletions src/Access/WopiProofAccessCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Drupal\collabora_online\Access;

use Drupal\collabora_online\Discovery\CollaboraDiscoveryInterface;
use Drupal\collabora_online\Discovery\DiscoveryLoaderInterface;
use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\collabora_online\Util\DotNetTime;
use Drupal\Component\Datetime\TimeInterface;
Expand All @@ -41,7 +41,7 @@
class WopiProofAccessCheck implements AccessInterface {

public function __construct(
protected readonly CollaboraDiscoveryInterface $discovery,
protected readonly DiscoveryLoaderInterface $discoveryLoader,
#[Autowire(service: 'logger.channel.collabora_online')]
protected readonly LoggerInterface $logger,
protected readonly ConfigFactoryInterface $configFactory,
Expand Down Expand Up @@ -201,12 +201,13 @@ protected function getSubject(Request $request): string {
* The discovery cannot be loaded.
*/
protected function getKeys(): array {
$discovery = $this->discoveryLoader->getDiscovery();
// Get current and old key.
// Remove empty values.
// If both are the same, keep only the current one.
$public_keys = array_unique(array_filter([
'current' => $this->discovery->getProofKey(),
'old' => $this->discovery->getProofKeyOld(),
'current' => $discovery->getProofKey(),
'old' => $discovery->getProofKeyOld(),
]));
$key_objects = [];
foreach ($public_keys as $key_name => $key_str) {
Expand Down
7 changes: 4 additions & 3 deletions src/Controller/ViewerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Drupal\collabora_online\Controller;

use Drupal\collabora_online\Discovery\CollaboraDiscoveryInterface;
use Drupal\collabora_online\Discovery\DiscoveryLoaderInterface;
use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\collabora_online\Jwt\JwtTranscoderInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
Expand All @@ -40,7 +40,7 @@ class ViewerController implements ContainerInjectionInterface {
use StringTranslationTrait;

public function __construct(
protected readonly CollaboraDiscoveryInterface $discovery,
protected readonly DiscoveryLoaderInterface $discoveryLoader,
protected readonly JwtTranscoderInterface $jwtTranscoder,
protected readonly RendererInterface $renderer,
#[Autowire('logger.channel.collabora_online')]
Expand Down Expand Up @@ -69,7 +69,8 @@ public function __construct(
public function editor(MediaInterface $media, Request $request, $edit = FALSE) {
try {
// @todo Get client url for the correct MIME type.
$wopi_client_url = $this->discovery->getWopiClientURL();
$discovery = $this->discoveryLoader->getDiscovery();
$wopi_client_url = $discovery->getWopiClientURL();
}
catch (CollaboraNotAvailableException $e) {
$this->logger->warning(
Expand Down
2 changes: 1 addition & 1 deletion src/Discovery/CachingDiscoveryFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function getDiscoveryXml(RefinableCacheableDependencyInterface $cacheabil
$max_age = ($expire === Cache::PERMANENT)
? Cache::PERMANENT
: $expire - $this->time->getRequestTime();
$cacheability->setCacheMaxAge($max_age);
$cacheability->mergeCacheMaxAge($max_age);
return $cached->data;
}
// In theory, the $cacheability could already contain unrelated cache
Expand Down
107 changes: 29 additions & 78 deletions src/Discovery/CollaboraDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,32 @@

namespace Drupal\collabora_online\Discovery;

use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Symfony\Component\ErrorHandler\ErrorHandler;

/**
* Service to get values from the discovery.xml.
* Value object to get values from the discovery.xml.
*/
class CollaboraDiscovery implements CollaboraDiscoveryInterface {

protected const DEFAULT_CID = 'collabora_online.discovery.parsed';

/**
* Constructor.
*
* @param \SimpleXMLElement $parsedXml
* Parsed XML content.
* @param list<string> $cacheTags
* Cache tags.
* @param int $cacheMaxAge
* Cache max age in seconds.
*/
public function __construct(
protected readonly CollaboraDiscoveryFetcherInterface $discoveryFetcher,
protected readonly MemoryCacheInterface $cache,
protected readonly TimeInterface $time,
protected readonly string $cid = self::DEFAULT_CID,
protected readonly \SimpleXMLElement $parsedXml,
protected readonly array $cacheTags,
protected readonly int $cacheMaxAge,
) {}

/**
* {@inheritdoc}
*/
public function getWopiClientURL(string $mimetype = 'text/plain'): ?string {
$discovery_parsed = $this->getParsedXml();

$result = $discovery_parsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
$result = $this->parsedXml->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if (empty($result[0]['urlsrc'][0])) {
return NULL;
}
Expand All @@ -53,84 +51,37 @@ public function getWopiClientURL(string $mimetype = 'text/plain'): ?string {
* {@inheritdoc}
*/
public function getProofKey(): ?string {
$discovery_parsed = $this->getParsedXml();
$attribute = $discovery_parsed->xpath('/wopi-discovery/proof-key/@value')[0] ?? NULL;
$attribute = $this->parsedXml->xpath('/wopi-discovery/proof-key/@value')[0] ?? NULL;
return $attribute?->__toString();
}

/**
* {@inheritdoc}
*/
public function getProofKeyOld(): ?string {
$discovery_parsed = $this->getParsedXml();
$attribute = $discovery_parsed->xpath('/wopi-discovery/proof-key/@oldvalue')[0] ?? NULL;
$attribute = $this->parsedXml->xpath('/wopi-discovery/proof-key/@oldvalue')[0] ?? NULL;
return $attribute?->__toString();
}

/**
* Fetches the discovery.xml, and gets the parsed contents.
*
* @return \SimpleXMLElement
* Parsed xml from the discovery.xml.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* Fetching the discovery.xml failed, or the result is not valid xml.
* {@inheritdoc}
*/
protected function getParsedXml(): \SimpleXMLElement {
$cached = $this->cache->get($this->cid);
if ($cached) {
return $cached->data;
}
$cacheability = new CacheableMetadata();
$xml = $this->discoveryFetcher->getDiscoveryXml($cacheability);
$parsed_xml = $this->parseXml($xml);
public function getCacheContexts(): array {
return [];
}

$max_age = $cacheability->getCacheMaxAge();
/* @see \Drupal\Core\Cache\VariationCache::maxAgeToExpire() */
$expire = ($max_age === Cache::PERMANENT)
? Cache::PERMANENT
: $max_age + $this->time->getRequestTime();
$this->cache->set(
$this->cid,
$parsed_xml,
$expire,
$cacheability->getCacheTags(),
);
return $parsed_xml;
/**
* {@inheritdoc}
*/
public function getCacheTags(): array {
return $this->cacheTags;
}

/**
* Parses an XML string.
*
* @param string $xml
* XML string.
*
* @return \SimpleXMLElement
* Parsed XML.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The XML is invalid or empty.
* {@inheritdoc}
*/
protected function parseXml(string $xml): \SimpleXMLElement {
try {
// Avoid errors from XML parsing hitting the regular error handler.
// An alternative would be libxml_use_internal_errors(), but then we would
// have to deal with the results from libxml_get_errors().
$parsed_xml = ErrorHandler::call(
fn () => simplexml_load_string($xml),
);
}
catch (\ErrorException $e) {
throw new CollaboraNotAvailableException('Error in the retrieved discovery.xml file: ' . $e->getMessage(), previous: $e);
}
if ($parsed_xml === FALSE) {
// The parser returned FALSE, but no error was raised.
// This is known to happen when $xml is an empty string.
// Instead we could check for $xml === '' earlier, but we don't know for
// sure if this is, and always will be, the only such case.
throw new CollaboraNotAvailableException('The discovery.xml file is empty.');
}
return $parsed_xml;
public function getCacheMaxAge() {
return $this->cacheMaxAge;
}

}
4 changes: 3 additions & 1 deletion src/Discovery/CollaboraDiscoveryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

namespace Drupal\collabora_online\Discovery;

use Drupal\Core\Cache\CacheableDependencyInterface;

/**
* Service to get a WOPI client URL for a given MIME type.
*/
interface CollaboraDiscoveryInterface {
interface CollaboraDiscoveryInterface extends CacheableDependencyInterface {

/**
* Gets the URL for the WOPI client.
Expand Down
117 changes: 117 additions & 0 deletions src/Discovery/DiscoveryLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

declare(strict_types=1);

namespace Drupal\collabora_online\Discovery;

use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Symfony\Component\ErrorHandler\ErrorHandler;

/**
* Creates a discovery value object.
*/
class DiscoveryLoader implements DiscoveryLoaderInterface {

protected const DEFAULT_CID = 'collabora_online.discovery.value';

public function __construct(
protected readonly CollaboraDiscoveryFetcherInterface $discoveryFetcher,
protected readonly MemoryCacheInterface $cache,
protected readonly TimeInterface $time,
protected readonly string $cid = self::DEFAULT_CID,
) {}

/**
* {@inheritdoc}
*/
public function getDiscovery(): CollaboraDiscoveryInterface {
$cached = $this->cache->get($this->cid);
if ($cached) {
return $cached->data;
}
$discovery = $this->loadDiscovery();

$max_age = $discovery->getCacheMaxAge();
/* @see \Drupal\Core\Cache\VariationCache::maxAgeToExpire() */
$expire = ($max_age === Cache::PERMANENT)
? Cache::PERMANENT
: $max_age + $this->time->getRequestTime();
$this->cache->set(
$this->cid,
$discovery,
$expire,
$discovery->getCacheTags(),
);
return $discovery;
}

/**
* Loads the discovery.
*
* @return \Drupal\collabora_online\Discovery\CollaboraDiscoveryInterface
* Discovery object.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* Fetching the discovery.xml failed, or the result is not valid xml.
*/
protected function loadDiscovery(): CollaboraDiscoveryInterface {
$cacheability = new CacheableMetadata();
$xml = $this->discoveryFetcher->getDiscoveryXml($cacheability);
assert($cacheability->getCacheContexts() === []);
$parsed_xml = $this->parseXml($xml);
return new CollaboraDiscovery(
$parsed_xml,
$cacheability->getCacheTags(),
$cacheability->getCacheMaxAge(),
);
}

/**
* Parses an XML string.
*
* @param string $xml
* XML string.
*
* @return \SimpleXMLElement
* Parsed XML.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The XML is invalid or empty.
*/
protected function parseXml(string $xml): \SimpleXMLElement {
try {
// Avoid errors from XML parsing hitting the regular error handler.
// An alternative would be libxml_use_internal_errors(), but then we would
// have to deal with the results from libxml_get_errors().
$parsed_xml = ErrorHandler::call(
fn () => simplexml_load_string($xml),
);
}
catch (\ErrorException $e) {
throw new CollaboraNotAvailableException('Error in the retrieved discovery.xml file: ' . $e->getMessage(), previous: $e);
}
if ($parsed_xml === FALSE) {
// The parser returned FALSE, but no error was raised.
// This is known to happen when $xml is an empty string.
// Instead we could check for $xml === '' earlier, but we don't know for
// sure if this is, and always will be, the only such case.
throw new CollaboraNotAvailableException('The discovery.xml file is empty.');
}
return $parsed_xml;
}

}
33 changes: 33 additions & 0 deletions src/Discovery/DiscoveryLoaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

declare(strict_types=1);

namespace Drupal\collabora_online\Discovery;

/**
* Creates a WOPI discovery value object.
*/
interface DiscoveryLoaderInterface {

/**
* Gets a discovery value object.
*
* @return \Drupal\collabora_online\Discovery\CollaboraDiscoveryInterface
* Discovery value object.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* Fetching the discovery.xml failed, or the result is not valid xml.
*/
public function getDiscovery(): CollaboraDiscoveryInterface;

}
Loading

0 comments on commit 4cab629

Please sign in to comment.