diff --git a/collabora_online.module b/collabora_online.module index 48ddad70..fd5e26ca 100644 --- a/collabora_online.module +++ b/collabora_online.module @@ -11,12 +11,14 @@ */ use Drupal\collabora_online\CollaboraUrl; -use Drupal\collabora_online\Cool\CoolUtils; +use Drupal\collabora_online\Discovery\DiscoveryFetcherInterface; +use Drupal\collabora_online\Exception\CollaboraNotAvailableException; use Drupal\collabora_online\MediaHelperInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Utility\Error; use Drupal\media\MediaInterface; /** @@ -116,15 +118,23 @@ function collabora_online_entity_operation(EntityInterface $entity): array { ], ]; - if ( - CoolUtils::canEditMimeType($type) && - $media->access('edit in collabora') - ) { - $entries['collabora_online_edit'] = [ - 'title' => t("Edit in Collabora Online"), - 'weight' => 50, - 'url' => CollaboraUrl::editMedia($media), - ]; + if ($media->access('edit in collabora')) { + /** @var \Drupal\collabora_online\Discovery\DiscoveryFetcherInterface $discovery_fetcher */ + $discovery_fetcher = \Drupal::service(DiscoveryFetcherInterface::class); + try { + $discovery = $discovery_fetcher->getDiscovery(); + $wopi_client_edit_url = $discovery->getWopiClientURL($type, 'edit'); + if ($wopi_client_edit_url !== NULL) { + $entries['collabora_online_edit'] = [ + 'title' => t("Edit in Collabora Online"), + 'weight' => 50, + 'url' => CollaboraUrl::editMedia($media), + ]; + } + } + catch (CollaboraNotAvailableException $e) { + Error::logException(\Drupal::logger('cool'), $e); + } } return $entries; diff --git a/src/Controller/ViewerController.php b/src/Controller/ViewerController.php index c448652e..209738ae 100644 --- a/src/Controller/ViewerController.php +++ b/src/Controller/ViewerController.php @@ -17,6 +17,7 @@ use Drupal\collabora_online\Discovery\DiscoveryFetcherInterface; use Drupal\collabora_online\Exception\CollaboraNotAvailableException; use Drupal\collabora_online\Jwt\JwtTranscoderInterface; +use Drupal\collabora_online\MediaHelperInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\AutowireTrait; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; @@ -43,6 +44,7 @@ public function __construct( protected readonly DiscoveryFetcherInterface $discoveryFetcher, protected readonly JwtTranscoderInterface $jwtTranscoder, protected readonly RendererInterface $renderer, + protected readonly MediaHelperInterface $mediaHelper, #[Autowire('logger.channel.collabora_online')] protected readonly LoggerInterface $logger, protected readonly ConfigFactoryInterface $configFactory, @@ -67,10 +69,27 @@ public function __construct( * Response suitable for iframe, without the usual page decorations. */ public function editor(MediaInterface $media, Request $request, $edit = FALSE): Response { + $file = $this->mediaHelper->getFileForMedia($media); + // Treat "no file" and "file without MIME type" the same. + $mimetype = $file?->getMimeType(); + if ($mimetype === NULL) { + return new Response( + (string) $this->t('The Collabora Online editor/viewer is not available for media without a file attached.'), + Response::HTTP_BAD_REQUEST, + ['content-type' => 'text/plain'], + ); + } try { - // @todo Get client url for the correct MIME type. $discovery = $this->discoveryFetcher->getDiscovery(); - $wopi_client_url = $discovery->getWopiClientURL(); + + $wopi_client_url = $edit + ? $discovery->getWopiClientURL($mimetype, 'edit') + : ($discovery->getWopiClientURL($mimetype, 'view') + // With the typical discovery.xml from Collabora, some MIME types that + // are viewable have an 'edit' or 'view_comment' action but no 'view' + // action. + ?? $discovery->getWopiClientURL($mimetype, 'edit') + ?? $discovery->getWopiClientURL($mimetype, 'view_comment')); } catch (CollaboraNotAvailableException $e) { $this->logger->warning( diff --git a/src/Cool/CoolUtils.php b/src/Cool/CoolUtils.php deleted file mode 100644 index 2956dcf7..00000000 --- a/src/Cool/CoolUtils.php +++ /dev/null @@ -1,43 +0,0 @@ - TRUE, - 'application/x-iwork-pages-sffpages' => TRUE, - 'application/x-iwork-numbers-sffnumbers' => TRUE, - ]; - - /** - * Determines if a MIME type is supported for editing. - * - * @param string $mimetype - * File MIME type. - * - * @return bool - * TRUE if the MIME type is supported for editing. - * FALSE if the MIME type can only be opened as read-only. - */ - public static function canEditMimeType(string $mimetype) { - return !array_key_exists($mimetype, static::READ_ONLY); - } - -} diff --git a/src/Discovery/Discovery.php b/src/Discovery/Discovery.php index baec6a6a..a990c621 100644 --- a/src/Discovery/Discovery.php +++ b/src/Discovery/Discovery.php @@ -32,8 +32,12 @@ public function __construct( /** * {@inheritdoc} */ - public function getWopiClientURL(string $mimetype = 'text/plain'): ?string { - $result = $this->parsedXml->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype)); + public function getWopiClientURL(string $mimetype, string $action): ?string { + $result = $this->parsedXml->xpath(sprintf( + "/wopi-discovery/net-zone/app[@name='%s']/action[@name='%s']", + $mimetype, + $action, + )); if (empty($result[0]['urlsrc'][0])) { return NULL; } diff --git a/src/Discovery/DiscoveryInterface.php b/src/Discovery/DiscoveryInterface.php index b6fd73a3..4fe3ed80 100644 --- a/src/Discovery/DiscoveryInterface.php +++ b/src/Discovery/DiscoveryInterface.php @@ -25,11 +25,15 @@ interface DiscoveryInterface { * @param string $mimetype * Mime type for which to get the WOPI client URL. * This refers to config entries in the discovery.xml file. + * @param string $action + * Name of the action/operation for which to get the url. + * Typical values are 'view', 'edit' or 'view_comment'. * * @return string|null - * The WOPI client URL, or NULL if none provided for the MIME type. + * The WOPI client URL, or NULL if none provided for the MIME type and + * operation. */ - public function getWopiClientURL(string $mimetype = 'text/plain'): ?string; + public function getWopiClientURL(string $mimetype, string $action): ?string; /** * Gets the public key used for proofing. diff --git a/tests/fixtures/discovery.mimetypes.xml b/tests/fixtures/discovery.mimetypes.xml index e8458fa6..d9842966 100644 --- a/tests/fixtures/discovery.mimetypes.xml +++ b/tests/fixtures/discovery.mimetypes.xml @@ -7,8 +7,14 @@ + + + - + + + + diff --git a/tests/fixtures/discovery.xml b/tests/fixtures/discovery.xml new file mode 100644 index 00000000..6d058aba --- /dev/null +++ b/tests/fixtures/discovery.xml @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/src/ExistingSite/FetchClientUrlTest.php b/tests/src/ExistingSite/FetchClientUrlTest.php index e07cbd96..d3f4b368 100644 --- a/tests/src/ExistingSite/FetchClientUrlTest.php +++ b/tests/src/ExistingSite/FetchClientUrlTest.php @@ -41,7 +41,7 @@ public function testFetchClientUrl(): void { /** @var \Drupal\collabora_online\Discovery\DiscoveryFetcherInterface $discovery_fetcher */ $discovery_fetcher = \Drupal::service(DiscoveryFetcherInterface::class); $discovery = $discovery_fetcher->getDiscovery(); - $client_url = $discovery->getWopiClientURL(); + $client_url = $discovery->getWopiClientURL('text/plain', 'edit'); $this->assertNotNull($client_url); // The protocol, domain and port are known when this test runs in the // docker-compose setup. diff --git a/tests/src/Kernel/DiscoveryFetcherTest.php b/tests/src/Kernel/DiscoveryFetcherTest.php index a3252ca3..f9c01fcd 100644 --- a/tests/src/Kernel/DiscoveryFetcherTest.php +++ b/tests/src/Kernel/DiscoveryFetcherTest.php @@ -117,7 +117,7 @@ public function testGetDiscovery(): void { $discovery = $fetcher->getDiscovery(); $this->assertSame( 'http://collabora.test:9980/browser/61cf2b4/cool.html?', - $discovery->getWopiClientURL(), + $discovery->getWopiClientURL('text/plain', 'view'), ); $this->assertSame( [ diff --git a/tests/src/Unit/CollaboraDiscoveryTest.php b/tests/src/Unit/CollaboraDiscoveryTest.php index 19b7aa22..95c995ef 100644 --- a/tests/src/Unit/CollaboraDiscoveryTest.php +++ b/tests/src/Unit/CollaboraDiscoveryTest.php @@ -6,6 +6,7 @@ use Drupal\collabora_online\Discovery\Discovery; use Drupal\collabora_online\Discovery\DiscoveryInterface; +use Drupal\Core\Serialization\Yaml; use Drupal\Tests\UnitTestCase; use Symfony\Component\ErrorHandler\ErrorHandler; @@ -24,16 +25,171 @@ public function testWopiClientUrl(): void { $discovery = $this->getDiscoveryFromFile($file); $this->assertSame( 'http://collabora.test:9980/browser/61cf2b4/cool.html?', - $discovery->getWopiClientURL(), + $discovery->getWopiClientURL('text/plain', 'view'), ); $this->assertSame( 'http://spreadsheet.collabora.test:9980/browser/61cf2b4/cool.html?', - $discovery->getWopiClientURL('text/spreadsheet'), + $discovery->getWopiClientURL('text/spreadsheet', 'edit'), ); // Test unknown mime type. - $this->assertNull( - $discovery->getWopiClientURL('text/unknown'), + $this->assertNull($discovery->getWopiClientURL('text/unknown', 'view')); + $this->assertSame( + 'http://csv.collabora.test:9980/browser/61cf2b4/cool.html?', + $discovery->getWopiClientURL('text/csv', 'edit'), + ); + $this->assertSame( + 'http://view.csv.collabora.test:9980/browser/61cf2b4/cool.html?', + $discovery->getWopiClientURL('text/csv', 'view'), ); + // Test the default MIME type 'text/plain' which has only 'edit' action in + // the example file, but no 'view' action. + $this->assertNull($discovery->getWopiClientURL('text/plain', 'edit')); + $this->assertNotNull($discovery->getWopiClientURL('text/plain', 'view')); + // Test a MIME type with no action name specified. + // This does not occur in the known discovery.xml, but we still want a + // well-defined behavior in that case. + $this->assertNull($discovery->getWopiClientURL('image/png', 'edit')); + $this->assertNull($discovery->getWopiClientURL('image/png', 'view')); + } + + /** + * Tests which MIME types are supported in a realistic discovery.xml. + * + * That file was generated with Collabora, but may not be the same in the + * latest version. + */ + public function testRealisticDiscoveryXml(): void { + $file = dirname(__DIR__, 2) . '/fixtures/discovery.xml'; + $xml = file_get_contents($file); + $this->assertSame(98, preg_match_all('@]+)* name="([^"]+)"@', $xml, $matches)); + $this->assertSame('application/vnd.ms-excel', $matches[2][9]); + $mimetypes = array_unique($matches[2]); + $mimetypes = array_diff($mimetypes, ['Capabilities']); + sort($mimetypes); + $discovery = $this->getDiscoveryFromXml($xml); + $known_url = 'http://collabora.test:9980/browser/61cf2b4/cool.html?'; + $supported_action_types = []; + foreach ($mimetypes as $mimetype) { + $type_supported_actions = []; + foreach (['edit', 'view_comment', 'view'] as $action) { + $url = $discovery->getWopiClientURL($mimetype, $action); + if ($url !== NULL) { + $this->assertSame($known_url, $url); + $type_supported_actions[] = $action; + } + } + sort($type_supported_actions); + $supported_action_types[implode(',', $type_supported_actions)][] = $mimetype; + } + ksort($supported_action_types); + $this->assertSame([ + '' => [ + 'application/vnd.oasis.opendocument.formula', + 'application/vnd.sun.xml.math', + 'math', + ], + 'edit' => [ + 'application/msword', + 'application/vnd.ms-excel', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'application/vnd.ms-word.document.macroEnabled.12', + 'application/vnd.oasis.opendocument.chart', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-flat-xml', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-flat-xml', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-flat-xml', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.oasis.opendocument.text-web', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.sun.xml.draw', + 'application/x-dbase', + 'application/x-dif-document', + 'text/csv', + 'text/plain', + 'text/rtf', + 'text/spreadsheet', + 'writer-web', + ], + 'edit,view' => [ + 'calc', + 'impress', + 'writer', + 'writer-global', + ], + 'edit,view,view_comment' => [ + 'draw', + ], + 'view' => [ + 'application/clarisworks', + 'application/coreldraw', + 'application/macwriteii', + 'application/vnd.lotus-1-2-3', + 'application/vnd.ms-excel.template.macroEnabled.12', + 'application/vnd.ms-powerpoint.template.macroEnabled.12', + 'application/vnd.ms-visio.drawing', + 'application/vnd.ms-word.template.macroEnabled.12', + 'application/vnd.ms-works', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.oasis.opendocument.text-master-template', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'application/vnd.sun.xml.calc', + 'application/vnd.sun.xml.calc.template', + 'application/vnd.sun.xml.chart', + 'application/vnd.sun.xml.draw.template', + 'application/vnd.sun.xml.impress', + 'application/vnd.sun.xml.impress.template', + 'application/vnd.sun.xml.writer', + 'application/vnd.sun.xml.writer.global', + 'application/vnd.sun.xml.writer.template', + 'application/vnd.visio', + 'application/vnd.visio2013', + 'application/vnd.wordperfect', + 'application/x-abiword', + 'application/x-aportisdoc', + 'application/x-fictionbook+xml', + 'application/x-gnumeric', + 'application/x-hwp', + 'application/x-iwork-keynote-sffkey', + 'application/x-iwork-numbers-sffnumbers', + 'application/x-iwork-pages-sffpages', + 'application/x-mspublisher', + 'application/x-mswrite', + 'application/x-pagemaker', + 'application/x-sony-bbeb', + 'application/x-t602', + 'image/bmp', + 'image/cgm', + 'image/gif', + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/svg+xml', + 'image/tiff', + 'image/vnd.dxf', + 'image/x-emf', + 'image/x-freehand', + 'image/x-wmf', + 'image/x-wpg', + ], + 'view_comment' => [ + 'application/pdf', + ], + ], $supported_action_types); } /**