Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #56: Refactor CoolRequest #68

Merged
merged 38 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
be3bd73
Issue #56: Distinguish readonly vs edit mode in the integration test.
donquixote Nov 14, 2024
7ada8f4
Issue #56: Add FetchClientUrlTest.
donquixote Nov 12, 2024
ca7103c
Issue #56: Drop strStartsWith(), use str_starts_with() instead.
donquixote Nov 12, 2024
5092711
Issue #56: Drop $wopi_src object property, use local var instead.
donquixote Nov 12, 2024
c02250d
Issue #56: Initialize $error_code property in declaration, drop const…
donquixote Nov 12, 2024
3455e57
Issue #56: Avoid string concat with t() strings.
donquixote Nov 12, 2024
847757f
Issue #56: Let CoolRequest throw an exception on failure.
donquixote Nov 12, 2024
d5eb1d4
Issue #56: The config object from Drupal::config() cannot be NULL.
donquixote Nov 13, 2024
54d5857
Issue #56: Use curl instead of file_get_contents() to get discovery.
donquixote Sep 12, 2024
e42664c
Issue #56: Refactor, then inline getWopiSrcUrl().
donquixote Nov 12, 2024
850a376
Issue #56: Throw exception directly from getDiscovery().
donquixote Nov 12, 2024
5da8269
Issue #56: Add param and return types.
donquixote Nov 29, 2024
84b91a1
Issue #56: Add php param and return types.
donquixote Nov 12, 2024
48b2bd3
Issue #56: Rename $_HOST_SCHEME -> $host_scheme, and move it to where…
donquixote Nov 28, 2024
adc362f
Issue #56: Support different MIME types.
donquixote Nov 14, 2024
abc2c23
Issue #56: Local var for $cool_settings.
donquixote Nov 13, 2024
fd918cf
Issue #56: Rename method to getDiscoveryXml, and var to $xml.
donquixote Nov 13, 2024
9547b2c
Issue #56: Move xml fetching into new class CollaboraDiscoveryFetcher.
donquixote Nov 29, 2024
3d0afac
Issue #56: Rename CoolRequest -> CollaboraDiscovery.
donquixote Nov 28, 2024
59a44b0
Issue #56: Convert CollaboraDiscovery and *Fetcher into services.
donquixote Nov 12, 2024
cf4b603
Issue #56: Replace Drupal::service() calls in new services.
donquixote Nov 29, 2024
04aac10
Issue #56: Use Guzzle to fetch discovery.xml.
donquixote Nov 12, 2024
ea8a16e
Issue #56: Extract new method loadSettings(), and fail if config is e…
donquixote Nov 28, 2024
f09af44
Issue #56: Extract getWopiClientServerBaseUrl().
donquixote Nov 12, 2024
3191945
Issue #56: Call getWopiClientServerBaseUrl() directly from getDiscove…
donquixote Nov 12, 2024
2023e61
Issue #56: Avoid php notice for missing config keys.
donquixote Nov 13, 2024
4985c31
Issue #56: Harden the protocol prefix condition for $wopi_client_server.
donquixote Nov 28, 2024
88594bc
Issue #56: Change exception messages.
donquixote Nov 13, 2024
1260936
Issue #56: Change log message.
donquixote Nov 13, 2024
dee080a
Issue #56: Use constructor property promotion, readonly, AutowireTrai…
donquixote Sep 9, 2024
8952021
Issue #56: Avoid Drupal::logger() in the controller.
donquixote Nov 13, 2024
e4d0cb9
Issue #56: Call ->getWopiClientURL() from the controller, and pass th…
donquixote Nov 13, 2024
fd198be
Issue #56: Validate client url scheme instead of server url scheme.
donquixote Nov 13, 2024
7a3eaea
Issue #56: Drop the numeric error codes.
donquixote Nov 21, 2024
705985c
Issue #56: Don't disclose exception message to visitor.
donquixote Nov 21, 2024
adc2b6d
Issue #56: Use MediaInterface as parameter type.
donquixote Nov 27, 2024
d9ce6af
Issue #56: Add strict_types declaration, don't rely on implicit cast.
donquixote Nov 27, 2024
e663b8d
Issue #56: Introduce interfaces.
donquixote Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions collabora_online.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
_defaults:
autowire: true
logger.channel.collabora_online:
parent: logger.channel_base
arguments: ['cool']
Drupal\collabora_online\Cool\CollaboraDiscoveryFetcherInterface:
class: Drupal\collabora_online\Cool\CollaboraDiscoveryFetcher
Drupal\collabora_online\Cool\CollaboraDiscoveryInterface:
class: Drupal\collabora_online\Cool\CollaboraDiscovery
3 changes: 0 additions & 3 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,5 @@
<exclude name="Drupal.NamingConventions.ValidFunctionName.InvalidName"/>
<exclude name="Drupal.NamingConventions.ValidVariableName.LowerCamelName"/>
<exclude name="Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps"/>

<!-- Fix translatable strings later. -->
<exclude name="Drupal.Semantics.FunctionT.WhiteSpace"/>
</rule>
</ruleset>
75 changes: 45 additions & 30 deletions src/Controller/ViewerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,80 +10,95 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

declare(strict_types=1);

namespace Drupal\collabora_online\Controller;

use Drupal\collabora_online\Cool\CollaboraDiscoveryInterface;
use Drupal\collabora_online\Cool\CoolUtils;
use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RendererInterface;
use Drupal\media\Entity\Media;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Utility\Error;
use Drupal\media\MediaInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* Provides route responses for the Collabora module.
*/
class ViewerController extends ControllerBase {

/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
private $renderer;

/**
* The controller constructor.
*
* @param \Drupal\collabora_online\Cool\CollaboraDiscoveryInterface $discovery
* Service to fetch a WOPI client URL.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(RendererInterface $renderer) {
$this->renderer = $renderer;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
$container->get('renderer'),
);
}
public function __construct(
protected readonly CollaboraDiscoveryInterface $discovery,
protected readonly RendererInterface $renderer,
) {}

/**
* Returns a raw page for the iframe embed.
*
* @param \Drupal\media\Entity\Media $media
* @param \Drupal\media\MediaInterface $media
* Media entity.
* @param \Symfony\Component\HttpFoundation\Request $request
* The incoming request.
* @param bool $edit
* TRUE to open Collabora Online in edit mode.
* FALSE to open Collabora Online in readonly mode.
*
* @return \Symfony\Component\HttpFoundation\Response
* Response suitable for iframe, without the usual page decorations.
*/
public function editor(Media $media, $edit = FALSE) {
public function editor(MediaInterface $media, Request $request, $edit = FALSE) {
$options = [
'closebutton' => 'true',
];

$render_array = CoolUtils::getViewerRender($media, $edit, $options);
try {
$wopi_client_url = $this->discovery->getWopiClientURL();
}
catch (CollaboraNotAvailableException $e) {
$this->getLogger('cool')->warning(
"Collabora Online is not available.<br>\n" . Error::DEFAULT_ERROR_MESSAGE,
Error::decodeException($e) + [],
);
return new Response(
(string) $this->t('The Collabora Online editor/viewer is not available.'),
Response::HTTP_BAD_REQUEST,
['content-type' => 'text/plain'],
);
}

if (!$render_array || array_key_exists('error', $render_array)) {
$error_msg = 'Viewer error: ' . ($render_array ? $render_array['error'] : 'NULL');
\Drupal::logger('cool')->error($error_msg);
$current_request_scheme = $request->getScheme();
if (!str_starts_with($wopi_client_url, $current_request_scheme . '://')) {
$this->getLogger('cool')->error($this->t(
"The current request uses '@current_request_scheme' url scheme, but the Collabora client url is '@wopi_client_url'.",
[
'@current_request_scheme' => $current_request_scheme,
'@wopi_client_url' => $wopi_client_url,
],
));
return new Response(
$error_msg,
(string) $this->t('Viewer error: Protocol mismatch.'),
Response::HTTP_BAD_REQUEST,
['content-type' => 'text/plain']
['content-type' => 'text/plain'],
);
}

$render_array = CoolUtils::getViewerRender($media, $wopi_client_url, $edit, $options);

$render_array['#theme'] = 'collabora_online_full';
$render_array['#attached']['library'][] = 'collabora_online/cool.frame';

$response = new Response();
$response->setContent($this->renderer->renderRoot($render_array));
$response->setContent((string) $this->renderer->renderRoot($render_array));

return $response;
}
Expand Down
53 changes: 53 additions & 0 deletions src/Cool/CollaboraDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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\Cool;

use Drupal\collabora_online\Exception\CollaboraNotAvailableException;

/**
* Service to get a WOPI client url for a given MIME type.
*/
class CollaboraDiscovery implements CollaboraDiscoveryInterface {

/**
* Constructor.
*
* @param \Drupal\collabora_online\Cool\CollaboraDiscoveryFetcherInterface $discoveryFetcher
* Service to load the discovery.xml from the Collabora server.
*/
public function __construct(
protected readonly CollaboraDiscoveryFetcherInterface $discoveryFetcher,
) {}

/**
* {@inheritdoc}
*/
public function getWopiClientURL(string $mimetype = 'text/plain'): string {
$xml = $this->discoveryFetcher->getDiscoveryXml();

$discovery_parsed = simplexml_load_string($xml);
if (!$discovery_parsed) {
throw new CollaboraNotAvailableException('The retrieved discovery.xml file is not a valid XML file.');
}

$result = $discovery_parsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if (empty($result[0]['urlsrc'][0])) {
throw new CollaboraNotAvailableException('The requested mime type is not handled.');
}

return (string) $result[0]['urlsrc'][0];
}

}
122 changes: 122 additions & 0 deletions src/Cool/CollaboraDiscoveryFetcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?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\Cool;

use Drupal\collabora_online\Exception\CollaboraNotAvailableException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
* Service to load the discovery.xml from the Collabora server.
*/
class CollaboraDiscoveryFetcher implements CollaboraDiscoveryFetcherInterface {

/**
* Constructor.
*
* @param \Drupal\Core\Logger\LoggerChannelInterface $logger
* Logger channel.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* Config factory.
* @param \GuzzleHttp\ClientInterface $httpClient
* Http client.
*/
public function __construct(
#[Autowire(service: 'logger.channel.collabora_online')]
protected readonly LoggerChannelInterface $logger,
protected readonly ConfigFactoryInterface $configFactory,
protected readonly ClientInterface $httpClient,
) {}

/**
* {@inheritdoc}
*/
public function getDiscoveryXml(): string {
$discovery_url = $this->getWopiClientServerBaseUrl() . '/hosting/discovery';

$cool_settings = $this->loadSettings();
$disable_checks = !empty($cool_settings['disable_cert_check']);

try {
$response = $this->httpClient->get($discovery_url, [
RequestOptions::VERIFY => !$disable_checks,
]);
$xml = $response->getBody()->getContents();
}
catch (ClientExceptionInterface $e) {
// The backtrace of a client exception is typically not very
// interesting. Just log the message.
$this->logger->error("Failed to fetch from '@url': @message.", [
'@url' => $discovery_url,
'@message' => $e->getMessage(),
]);
throw new CollaboraNotAvailableException(
'Not able to retrieve the discovery.xml file from the Collabora Online server.',
previous: $e,
);
}
return $xml;
}

/**
* Loads the WOPI server url from configuration.
*
* @return string
* Base URL to access the WOPI server from Drupal.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The WOPI server url is misconfigured, or the protocol does not match
* that of the current Drupal request.
*/
protected function getWopiClientServerBaseUrl(): string {
$cool_settings = $this->loadSettings();
$wopi_client_server = $cool_settings['server'] ?? NULL;
if (!$wopi_client_server) {
throw new CollaboraNotAvailableException('The configured Collabora Online server address is empty.');
}
$wopi_client_server = trim($wopi_client_server);

if (!str_starts_with($wopi_client_server, 'http://') && !str_starts_with($wopi_client_server, 'https://')) {
throw new CollaboraNotAvailableException(sprintf(
"The configured Collabora Online server address must begin with 'http://' or 'https://'. Found '%s'.",
$wopi_client_server,
));
}

return $wopi_client_server;
}

/**
* Loads the relevant configuration.
*
* @return array
* Configuration.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The module is not configured.
*/
protected function loadSettings(): array {
$cool_settings = $this->configFactory->get('collabora_online.settings')->get('cool');
if (!$cool_settings) {
throw new CollaboraNotAvailableException('The Collabora Online connection is not configured.');
}
return $cool_settings;
}

}
33 changes: 33 additions & 0 deletions src/Cool/CollaboraDiscoveryFetcherInterface.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\Cool;

/**
* Service to load the discovery.xml from the Collabora server.
*/
interface CollaboraDiscoveryFetcherInterface {

/**
* Gets the contents of discovery.xml from the Collabora server.
*
* @return string
* The full contents of discovery.xml.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The client url cannot be retrieved.
*/
public function getDiscoveryXml(): string;

}
37 changes: 37 additions & 0 deletions src/Cool/CollaboraDiscoveryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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\Cool;

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

/**
* Gets the URL for the WOPI client.
*
* @param string $mimetype
* Mime type for which to get the WOPI client url.
* This refers to config entries in the discovery.xml file.
*
* @return string
* The WOPI client url.
*
* @throws \Drupal\collabora_online\Exception\CollaboraNotAvailableException
* The client url cannot be retrieved.
*/
public function getWopiClientURL(string $mimetype = 'text/plain'): string;

}
Loading
Loading