fix: Properly handle internal urls

Improve handling of different URLs. From now on we will allow to
configure:

- wopi_url Used by Nextcloud to connect to Collabora in the backend
- wop_callback_url Passed to collabora to connect back to Nextcloud
  (optional, determined from the browser URL if not set)

The public_wopi_url which was only partly working is no longer ment to
be manually set and will be overwritten depending on the
/hosting/discovery response.

Further this PR improves:
- Add setup check on occ and admin page
- Give proper error on individual failures of the setup check
- Display configured and detected URLs to make setup issue debugging
  easier
- Refactor services to have a cleaner structure for setup checks,
  discovery and capabilities fetching

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl 2023-11-23 10:24:37 +01:00 коммит произвёл backportbot-nextcloud[bot]
Родитель 7bc8406531
Коммит 1551027aef
23 изменённых файлов: 464 добавлений и 301 удалений

Просмотреть файл

@ -9,7 +9,8 @@
}
},
"require": {
"ext-json": "*"
"ext-json": "*",
"ext-simplexml": "*"
},
"require-dev": {
"roave/security-advisories": "dev-master",

Просмотреть файл

@ -61,7 +61,9 @@ return array(
'OCA\\Richdocuments\\Preview\\Pdf' => $baseDir . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => $baseDir . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => $baseDir . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\ConnectivityService' => $baseDir . '/../lib/Service/ConnectivityService.php',
'OCA\\Richdocuments\\Service\\DemoService' => $baseDir . '/../lib/Service/DemoService.php',
'OCA\\Richdocuments\\Service\\DiscoveryService' => $baseDir . '/../lib/Service/DiscoveryService.php',
'OCA\\Richdocuments\\Service\\FederationService' => $baseDir . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => $baseDir . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php',
@ -75,6 +77,5 @@ return array(
'OCA\\Richdocuments\\Template\\CollaboraTemplateProvider' => $baseDir . '/../lib/Template/CollaboraTemplateProvider.php',
'OCA\\Richdocuments\\TokenManager' => $baseDir . '/../lib/TokenManager.php',
'OCA\\Richdocuments\\UploadException' => $baseDir . '/../lib/UploadException.php',
'OCA\\Richdocuments\\WOPI\\DiscoveryManager' => $baseDir . '/../lib/WOPI/DiscoveryManager.php',
'OCA\\Richdocuments\\WOPI\\Parser' => $baseDir . '/../lib/WOPI/Parser.php',
);

Просмотреть файл

@ -76,7 +76,9 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Preview\\Pdf' => __DIR__ . '/..' . '/../lib/Preview/Pdf.php',
'OCA\\Richdocuments\\Reference\\OfficeTargetReferenceProvider' => __DIR__ . '/..' . '/../lib/Reference/OfficeTargetReferenceProvider.php',
'OCA\\Richdocuments\\Service\\CapabilitiesService' => __DIR__ . '/..' . '/../lib/Service/CapabilitiesService.php',
'OCA\\Richdocuments\\Service\\ConnectivityService' => __DIR__ . '/..' . '/../lib/Service/ConnectivityService.php',
'OCA\\Richdocuments\\Service\\DemoService' => __DIR__ . '/..' . '/../lib/Service/DemoService.php',
'OCA\\Richdocuments\\Service\\DiscoveryService' => __DIR__ . '/..' . '/../lib/Service/DiscoveryService.php',
'OCA\\Richdocuments\\Service\\FederationService' => __DIR__ . '/..' . '/../lib/Service/FederationService.php',
'OCA\\Richdocuments\\Service\\FileTargetService' => __DIR__ . '/..' . '/../lib/Service/FileTargetService.php',
'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php',
@ -90,7 +92,6 @@ class ComposerStaticInitRichdocuments
'OCA\\Richdocuments\\Template\\CollaboraTemplateProvider' => __DIR__ . '/..' . '/../lib/Template/CollaboraTemplateProvider.php',
'OCA\\Richdocuments\\TokenManager' => __DIR__ . '/..' . '/../lib/TokenManager.php',
'OCA\\Richdocuments\\UploadException' => __DIR__ . '/..' . '/../lib/UploadException.php',
'OCA\\Richdocuments\\WOPI\\DiscoveryManager' => __DIR__ . '/..' . '/../lib/WOPI/DiscoveryManager.php',
'OCA\\Richdocuments\\WOPI\\Parser' => __DIR__ . '/..' . '/../lib/WOPI/Parser.php',
);

Просмотреть файл

@ -18,8 +18,13 @@ use OCP\App\IAppManager;
use OCP\GlobalScale\IConfig as GlobalScaleConfig;
class AppConfig {
// URL that Nextcloud will use to connect to Collabora
public const WOPI_URL = 'wopi_url';
// URL that the browser will use to connect to Collabora (inherited from the discovery endpoint of Collabora,
// either wopi_url or what is configured as server_name)
public const PUBLIC_WOPI_URL = 'public_wopi_url';
// URL that should be used by Collabora to connect back to Nextcloud (defaults to the users browser host)
public const WOPI_CALLBACK_URL = 'wopi_callback_url';
public const FEDERATION_USE_TRUSTED_DOMAINS = 'federation_use_trusted_domains';
@ -144,6 +149,10 @@ class AppConfig {
return $this->config->getAppValue(Application::APPNAME, self::WOPI_URL, '');
}
public function getNextcloudUrl(): string {
return $this->config->getAppValue(Application::APPNAME, self::WOPI_CALLBACK_URL, '');
}
public function getDisableCertificateValidation(): bool {
return $this->config->getAppValue(Application::APPNAME, 'disable_certificate_verification', 'no') === 'yes';
}
@ -235,7 +244,7 @@ class AppConfig {
/**
* Strips the path and query parameters from the URL.
*/
private function domainOnly(string $url): string {
public function domainOnly(string $url): string {
$parsedUrl = parse_url($url);
$scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
$host = $parsedUrl['host'] ?? '';

Просмотреть файл

@ -45,8 +45,8 @@ use OCA\Richdocuments\Preview\OpenDocument;
use OCA\Richdocuments\Preview\Pdf;
use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\DiscoveryService;
use OCA\Richdocuments\Template\CollaboraTemplateProvider;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\App;
@ -220,10 +220,10 @@ class Application extends App implements IBootstrap {
$appConfig->setAppValue('wopi_url', $new_wopi_url);
$appConfig->setAppValue('disable_certificate_verification', 'yes');
$discoveryManager = $this->getContainer()->get(DiscoveryManager::class);
$discoveryService = $this->getContainer()->get(DiscoveryService::class);
$capabilitiesService = $this->getContainer()->get(CapabilitiesService::class);
$discoveryManager->refetch();
$discoveryService->refetch();
$capabilitiesService->clear();
$capabilitiesService->refetch();
}

Просмотреть файл

@ -37,6 +37,6 @@ class ObtainCapabilities extends TimedJob {
}
protected function run($argument) {
$this->capabilitiesService->refetch();
$this->capabilitiesService->fetchFromRemote();
}
}

Просмотреть файл

@ -154,8 +154,9 @@ class Capabilities implements ICapability {
'productName' => $this->capabilitiesService->getProductName(),
'editonline_endpoint' => $this->urlGenerator->linkToRouteAbsolute('richdocuments.document.editOnline'),
'config' => [
'wopi_url' => $this->config->getAppValue('wopi_url'),
'public_wopi_url' => $this->config->getAppValue('public_wopi_url'),
'wopi_url' => $this->config->getCollaboraUrlInternal(),
'public_wopi_url' => $this->config->getCollaboraUrlPublic(),
'wopi_callback_url' => $this->config->getNextcloudUrl(),
'disable_certificate_verification' => $this->config->getAppValue('disable_certificate_verification'),
'edit_groups' => $this->config->getAppValue('edit_groups'),
'use_groups' => $this->config->getAppValue('use_groups'),

Просмотреть файл

@ -26,54 +26,95 @@ namespace OCA\Richdocuments\Command;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use OCA\Richdocuments\WOPI\Parser;
use OCA\Richdocuments\Service\ConnectivityService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ActivateConfig extends Command {
/** @var AppConfig */
private $appConfig;
/** @var CapabilitiesService */
private $capabilitiesService;
/** @var DiscoveryManager */
private $discoveryManager;
/** @var Parser */
private $wopiParser;
public function __construct(AppConfig $appConfig, CapabilitiesService $capabilitiesService, DiscoveryManager $discoveryManager, Parser $wopiParser) {
public function __construct(
private AppConfig $appConfig,
private ConnectivityService $connectivityService,
private CapabilitiesService $capabilitiesService,
) {
parent::__construct();
$this->appConfig = $appConfig;
$this->capabilitiesService = $capabilitiesService;
$this->discoveryManager = $discoveryManager;
$this->wopiParser = $wopiParser;
}
protected function configure() {
$this
->setName('richdocuments:activate-config')
->setAliases(['richdocuments:setup'])
->addOption('wopi-url', 'w', InputOption::VALUE_REQUIRED, 'URL that the Nextcloud server will use to connect to Collabora', null)
->addOption('callback-url', 'c', InputOption::VALUE_REQUIRED, 'URL that is passed to Collabora to connect back to Nextcloud', null)
->setDescription('Activate config changes');
}
protected function execute(InputInterface $input, OutputInterface $output) {
try {
$this->discoveryManager->refetch();
$this->capabilitiesService->clear();
$capaUrlSrc = $this->wopiParser->getUrlSrc('Capabilities');
if (is_array($capaUrlSrc) && $capaUrlSrc['action'] === 'getinfo') {
$public_wopi_url = str_replace('/hosting/capabilities', '', $capaUrlSrc['urlsrc']);
if ($public_wopi_url !== null) {
$this->appConfig->setAppValue('public_wopi_url', $public_wopi_url);
if ($input->getOption('wopi-url') !== null) {
$wopiUrl = $input->getOption('wopi-url');
$this->appConfig->setAppValue(AppConfig::WOPI_URL, $wopiUrl);
$output->writeln('<info>✓ Set WOPI url to ' . $wopiUrl . '</info>');
}
if ($input->getOption('callback-url') !== null) {
$callbackUrl = $input->getOption('callback-url');
$this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, $callbackUrl);
$output->writeln('<info>✓ Set callback url to ' . $callbackUrl . '</info>');
} else {
$this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, '');
$output->writeln('<info>✓ Reset callback url autodetect</info>');
}
$this->capabilitiesService->clear();
$this->capabilitiesService->refetch();
$output->writeln('<info>Activated any config changes</info>');
$output->writeln('Checking configuration');
$output->writeln('🛈 Configured WOPI URL: ' . $this->appConfig->getCollaboraUrlInternal());
$output->writeln('🛈 Configured public WOPI URL: ' . $this->appConfig->getCollaboraUrlPublic());
$output->writeln('🛈 Configured callback URL: ' . $this->appConfig->getNextcloudUrl());
$output->writeln('');
try {
$this->connectivityService->testDiscovery($output);
} catch (\Throwable $e) {
$output->writeln('<error>Failed to fetch discovery endpoint from ' . $this->appConfig->getCollaboraUrlInternal());
$output->writeln($e->getMessage());
return 1;
}
try {
$this->connectivityService->testCapabilities($output);
} catch (\Throwable $e) {
// FIXME: Optional when allowing generic WOPI servers
$output->writeln('<error>Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
$output->writeln($e->getMessage());
return 1;
}
try {
$this->connectivityService->autoConfigurePublicUrl();
} catch (\Throwable $e) {
$output->writeln('<error>Failed to determine public URL from discovery response</error>');
$output->writeln($e->getMessage());
return 1;
}
// Summarize URLs for easier debugging
$output->writeln('');
$output->writeln('Collabora URL (used for Nextcloud to contact the Collabora server):');
$output->writeln(' ' . $this->appConfig->getCollaboraUrlInternal());
$output->writeln('Collabora public URL (used in the browser to open Collabora):');
$output->writeln(' ' . $this->appConfig->getCollaboraUrlPublic());
$output->writeln('Callback URL (used by Collabora to connect back to Nextcloud):');
$callbackUrl = $this->appConfig->getNextcloudUrl();
if ($callbackUrl === '') {
$output->writeln(' autodetected (will use the same URL as your user for browsing Nextcloud)');
} else {
$output->writeln(' ' . $this->appConfig->getNextcloudUrl());
}
return 0;
} catch (\Exception $e) {
$output->writeln('<error>Failed to activate any config changes</error>');

Просмотреть файл

@ -29,21 +29,13 @@ use Doctrine\DBAL\Types\Type;
use OC\DB\Connection;
use OC\DB\SchemaWrapper;
use OCP\DB\Types;
use OCP\IDBConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class ConvertToBigInt extends Command {
/** @var IDBConnection */
private $connection;
/**
* @param IDBConnection $connection
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
public function __construct(private Connection $connection) {
parent::__construct();
}

Просмотреть файл

@ -11,28 +11,31 @@
namespace OCA\Richdocuments\Controller;
use \OCP\AppFramework\Controller;
use \OCP\IL10N;
use \OCP\IRequest;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Service\CapabilitiesService;
use OCA\Richdocuments\Service\ConnectivityService;
use OCA\Richdocuments\Service\DemoService;
use OCA\Richdocuments\Service\DiscoveryService;
use OCA\Richdocuments\Service\FontService;
use OCA\Richdocuments\UploadException;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use OCA\Richdocuments\WOPI\Parser;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\PreConditionNotMetException;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\NullOutput;
class SettingsController extends Controller {
// TODO adapt overview generation if we add more font mimetypes
@ -45,120 +48,88 @@ class SettingsController extends Controller {
'application/vnd.ms-opentype',
];
/** @var IL10N */
private $l10n;
/** @var AppConfig */
private $appConfig;
/** @var IConfig */
private $config;
/** @var DiscoveryManager */
private $discoveryManager;
/** @var Parser */
private $wopiParser;
/** @var string */
private $userId;
/** @var CapabilitiesService */
private $capabilitiesService;
/** @var DemoService */
private $demoService;
/** @var LoggerInterface */
private $logger;
/**
* @var FontService
*/
private $fontService;
public function __construct($appName,
IRequest $request,
IL10N $l10n,
AppConfig $appConfig,
IConfig $config,
DiscoveryManager $discoveryManager,
Parser $wopiParser,
CapabilitiesService $capabilitiesService,
DemoService $demoService,
FontService $fontService,
LoggerInterface $logger,
$userId
private IL10N $l10n,
private AppConfig $appConfig,
private IConfig $config,
private ConnectivityService $connectivityService,
private DiscoveryService $discoveryService,
private CapabilitiesService $capabilitiesService,
private DemoService $demoService,
private FontService $fontService,
private LoggerInterface $logger,
private ?string $userId
) {
parent::__construct($appName, $request);
$this->l10n = $l10n;
$this->appConfig = $appConfig;
$this->config = $config;
$this->discoveryManager = $discoveryManager;
$this->wopiParser = $wopiParser;
$this->capabilitiesService = $capabilitiesService;
$this->demoService = $demoService;
$this->logger = $logger;
$this->userId = $userId;
$this->fontService = $fontService;
$this->request = $request;
}
/**
* @PublicPage
* @NoCSRFRequired
* @throws \Exception
*/
public function checkSettings() {
#[PublicPage]
#[NoCSRFRequired]
public function checkSettings(): DataResponse {
try {
$response = $this->discoveryManager->fetchFromRemote();
$output = new NullOutput();
$this->connectivityService->testDiscovery($output);
$this->connectivityService->testCapabilities($output);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([
'status' => $e->getCode(),
'message' => 'Could not fetch discovery details'
'data' => [
// FIXME ONLY AS ADMIN
'message' => 'Failed to connect to the remote server: ' . $e->getMessage(),
'settings' => $this->getSettingsData(),
],
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new DataResponse();
return new DataResponse([
'status' => 'success',
'data' => [
'settings' => $this->getSettingsData(),
]
]);
}
public function demoServers() {
public function demoServers(): DataResponse {
$demoServers = $this->demoService->fetchDemoServers(true);
if (count($demoServers) > 0) {
return new DataResponse($demoServers);
}
return new NotFoundResponse();
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
/**
* @NoAdminRequired
*
* @return JSONResponse
*/
public function getSettings() {
return new JSONResponse([
'wopi_url' => $this->appConfig->getAppValue('wopi_url'),
#[NoAdminRequired]
public function getSettings(): JSONResponse {
return new JSONResponse($this->getSettingsData());
}
private function getSettingsData(): array {
return [
'wopi_url' => $this->appConfig->getCollaboraUrlInternal(),
'public_wopi_url' => $this->appConfig->getCollaboraUrlPublic(),
'wopi_callback_url' => $this->appConfig->getNextcloudUrl(),
'wopi_allowlist' => $this->appConfig->getAppValue('wopi_allowlist'),
'public_wopi_url' => $this->appConfig->getAppValue('public_wopi_url'),
'disable_certificate_verification' => $this->appConfig->getAppValue('disable_certificate_verification') === 'yes',
'edit_groups' => $this->appConfig->getAppValue('edit_groups'),
'use_groups' => $this->appConfig->getAppValue('use_groups'),
'doc_format' => $this->appConfig->getAppValue('doc_format'),
]);
'product_name' => $this->capabilitiesService->getServerProductName(),
'product_version' => $this->capabilitiesService->getProductVersion(),
'product_hash' => $this->capabilitiesService->getProductHash(),
];
}
/**
* @param string $wopi_url
* @param string $disable_certificate_verification
* @param string $edit_groups
* @param string $use_groups
* @param string $doc_format
* @param string $external_apps
* @param string $canonical_webroot
* @return JSONResponse
*/
public function setSettings($wopi_url,
$wopi_allowlist,
$disable_certificate_verification,
$edit_groups,
$use_groups,
$doc_format,
$external_apps,
$canonical_webroot) {
$message = $this->l10n->t('Saved');
public function setSettings(
?string $wopi_url,
?string $wopi_allowlist,
?bool $disable_certificate_verification,
?string $edit_groups,
?string $use_groups,
?string $doc_format,
?string $external_apps,
?string $canonical_webroot
): JSONResponse {
if ($wopi_url !== null) {
$this->appConfig->setAppValue('wopi_url', $wopi_url);
}
@ -170,7 +141,7 @@ class SettingsController extends Controller {
if ($disable_certificate_verification !== null) {
$this->appConfig->setAppValue(
'disable_certificate_verification',
$disable_certificate_verification === true ? 'yes' : ''
$disable_certificate_verification ? 'yes' : ''
);
}
@ -194,47 +165,30 @@ class SettingsController extends Controller {
$this->appConfig->setAppValue('canonical_webroot', $canonical_webroot);
}
$this->discoveryManager->refetch();
$this->capabilitiesService->clear();
try {
$capaUrlSrc = $this->wopiParser->getUrlSrc('Capabilities');
if (is_array($capaUrlSrc) && $capaUrlSrc['action'] === 'getinfo') {
$public_wopi_url = str_replace('/hosting/capabilities', '', $capaUrlSrc['urlsrc']);
if ($public_wopi_url !== null) {
$this->appConfig->setAppValue('public_wopi_url', $public_wopi_url);
$colon = strpos($public_wopi_url, ':', 0);
if ($this->request->getServerProtocol() !== substr($public_wopi_url, 0, $colon)) {
$message = $this->l10n->t('Saved with error: Collabora Online should expose the same protocol as the server installation. Please check the ssl.enable and ssl.termination settings of your Collabora Online server.');
}
}
}
} catch (\Exception $e) {
if ($wopi_url !== null) {
$output = new NullOutput();
$this->connectivityService->testDiscovery($output);
$this->connectivityService->testCapabilities($output);
$this->connectivityService->autoConfigurePublicUrl();
} catch (\Throwable $e) {
return new JSONResponse([
'status' => 'error',
'data' => ['message' => 'Failed to connect to the remote server']
], 500);
}
}
$this->capabilitiesService->clear();
$this->capabilitiesService->refetch();
if ($this->capabilitiesService->getCapabilities() === []) {
return new JSONResponse([
'status' => 'error',
'data' => ['message' => 'Failed to connect to the remote server', 'hint' => 'missing_capabilities']
'data' => ['message' => 'Failed to connect to the remote server: ' . $e->getMessage()]
], 500);
}
$response = [
'status' => 'success',
'data' => ['message' => $message]
'data' => [
'message' => $this->l10n->t('Saved'),
'settings' => $this->getSettingsData(),
]
];
return new JSONResponse($response);
}
public function updateWatermarkSettings($settings = []) {
public function updateWatermarkSettings($settings = []): JSONResponse {
$supportedOptions = [
'watermark_text',
'watermark_enabled',

Просмотреть файл

@ -97,6 +97,13 @@ class WOPIMiddleware extends Middleware {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
if ($controller instanceof WopiController) {
$this->logger->error('Uncaught error: ' . $exception->getMessage(), [ 'exception' => $exception ]);
return new JSONResponse([
'message' => 'Error'
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
throw $exception;
}

Просмотреть файл

@ -70,7 +70,7 @@ class CapabilitiesService {
$isCODEEnabled = strpos($this->config->getAppValue('richdocuments', 'wopi_url'), 'proxy.php?req=') !== false;
$shouldRecheckCODECapabilities = $isCODEInstalled && $isCODEEnabled && ($this->capabilities === null || count($this->capabilities) === 0);
if ($this->capabilities === null || $shouldRecheckCODECapabilities) {
$this->refetch();
$this->fetchFromRemote();
}
if (!is_array($this->capabilities)) {
@ -80,6 +80,18 @@ class CapabilitiesService {
return $this->capabilities;
}
public function getProductVersion(): ?string {
return $this->getCapabilities()['productVersion'] ?? null;
}
public function getProductHash(): ?string {
return $this->getCapabilities()['productVersionHash'] ?? null;
}
public function getServerProductName(): ?string {
return $this->getCapabilities()['productName'] ?? null;
}
public function hasNextcloudBranding(): bool {
$productVersion = $this->getCapabilities()['productVersion'] ?? '0.0.0.0';
return version_compare($productVersion, '21.11', '>=');
@ -124,16 +136,22 @@ class CapabilitiesService {
return false;
}
public function clear(): void {
public function resetCache(): void {
$this->cache->remove('capabilities');
}
public function refetch(): void {
public function getCapabilitiesEndpoint(): ?string {
$remoteHost = $this->config->getAppValue('richdocuments', 'wopi_url');
if ($remoteHost === '') {
return null;
}
return rtrim($remoteHost, '/') . '/hosting/capabilities';
}
public function fetchFromRemote($throw = false): void {
if (!$this->getCapabilitiesEndpoint()) {
return;
}
$capabilitiesEndpoint = rtrim($remoteHost, '/') . '/hosting/capabilities';
$client = $this->clientService->newClient();
$options = ['timeout' => 45, 'nextcloud' => ['allow_local_address' => true]];
@ -144,9 +162,9 @@ class CapabilitiesService {
try {
$startTime = microtime(true);
$response = $client->get($capabilitiesEndpoint, $options);
$response = $client->get($this->getCapabilitiesEndpoint(), $options);
$duration = round(((microtime(true) - $startTime)), 3);
$this->logger->info('Fetched capabilities endpoint from ' . $capabilitiesEndpoint. ' in ' . $duration . ' seconds');
$this->logger->info('Fetched capabilities endpoint from ' . $this->getCapabilitiesEndpoint(). ' in ' . $duration . ' seconds');
$responseBody = $response->getBody();
$capabilities = \json_decode($responseBody, true);
@ -155,6 +173,9 @@ class CapabilitiesService {
}
} catch (\Exception $e) {
$this->logger->error('Failed to fetch the Collabora capabilities endpoint: ' . $e->getMessage(), [ 'exception' => $e ]);
if ($throw) {
throw $e;
}
$capabilities = [];
}

Просмотреть файл

@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Richdocuments\Service;
use Exception;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\WOPI\Parser;
use Symfony\Component\Console\Output\OutputInterface;
class ConnectivityService {
public function __construct(
private AppConfig $appConfig,
private DiscoveryService $discoveryService,
private CapabilitiesService $capabilitiesService,
private Parser $parser,
) {
}
/**
* @throws Exception
*/
public function testDiscovery(OutputInterface $output): void {
$this->discoveryService->resetCache();
$this->discoveryService->fetchFromRemote();
$output->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');
$this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
$output->writeln('<info>✓ Valid mimetype response</info>');
// FIXME: Optional when allowing generic WOPI servers
$this->parser->getUrlSrcValue('Capabilities');
$output->writeln('<info>✓ Valid capabilities entry</info>');
}
public function testCapabilities(OutputInterface $output): void {
$this->capabilitiesService->resetCache();
$this->capabilitiesService->fetchFromRemote(true);
$output->writeln('<info>✓ Fetched /hosting/capabilities endpoint</info>');
if ($this->capabilitiesService->getCapabilities() === []) {
throw new \Exception('Empty capabilities, unexpected result from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
}
$output->writeln('<info>✓ Detected WOPI server: ' . $this->capabilitiesService->getServerProductName() . ' ' . $this->capabilitiesService->getProductVersion() . '</info>');
}
/**
* Detect public URL of the WOPI server for setting CSP on Nextcloud
*
* This value is not meant to be set manually. If this turns out to be the wrong URL
* it is likely a misconfiguration on your WOPI server. Collabora will inherit the URL to use
* form the request and the ssl.enable/ssl.termination settings and server_name (if configured)
*/
public function autoConfigurePublicUrl(): void {
$determinedUrl = $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
}
}

Просмотреть файл

@ -1,4 +1,24 @@
<?php
/**
* @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
@ -22,7 +42,7 @@ declare(strict_types=1);
*
*/
namespace OCA\Richdocuments\WOPI;
namespace OCA\Richdocuments\Service;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
@ -31,7 +51,7 @@ use OCP\ICacheFactory;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
class DiscoveryManager {
class DiscoveryService {
private IClientService $clientService;
private ICache $cache;
private IConfig $config;
@ -93,7 +113,7 @@ class DiscoveryManager {
return $response;
}
public function refetch(): void {
public function resetCache(): void {
$this->cache->remove('discovery');
$this->discovery = null;
}

Просмотреть файл

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OCA\Richdocuments\Service;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\Db\Wopi;
use OCP\AppFramework\Services\IInitialState;
@ -37,6 +38,7 @@ class InitialStateService {
public function __construct(
private IInitialState $initialState,
private AppConfig $appConfig,
private CapabilitiesService $capabilitiesService,
private IURLGenerator $urlGenerator,
private Defaults $themingDefaults,
@ -54,6 +56,7 @@ class InitialStateService {
$this->initialState->provideInitialState('hasDrawSupport', $this->capabilitiesService->hasDrawSupport());
$this->initialState->provideInitialState('hasNextcloudBranding', $this->capabilitiesService->hasNextcloudBranding());
$this->initialState->provideInitialState('instanceId', $this->config->getSystemValue('instanceid'));
$this->initialState->provideInitialState('wopi_callback_url', $this->appConfig->getNextcloudUrl());
$this->provideOptions();

Просмотреть файл

@ -34,54 +34,27 @@ use OCP\IConfig;
use OCP\Settings\ISettings;
class Admin implements ISettings {
/** @var IConfig */
private $config;
/** @var AppConfig */
private $appConfig;
/** @var TemplateManager */
private $manager;
/** @var CapabilitiesService */
private $capabilitiesService;
/** @var DemoService */
private $demoService;
/** @var InitialStateService */
private $initialState;
/**
* @var FontService
*/
private $fontService;
public function __construct(
IConfig $config,
AppConfig $appConfig,
TemplateManager $manager,
CapabilitiesService $capabilitiesService,
DemoService $demoService,
FontService $fontService,
InitialStateService $initialStateService
private IConfig $config,
private AppConfig $appConfig,
private TemplateManager $manager,
private CapabilitiesService $capabilitiesService,
private DemoService $demoService,
private FontService $fontService,
private InitialStateService $initialStateService
) {
$this->config = $config;
$this->appConfig = $appConfig;
$this->manager = $manager;
$this->capabilitiesService = $capabilitiesService;
$this->demoService = $demoService;
$this->initialState = $initialStateService;
$this->fontService = $fontService;
}
public function getForm() {
$this->initialState->provideCapabilities();
public function getForm(): TemplateResponse {
$this->initialStateService->provideCapabilities();
return new TemplateResponse(
'richdocuments',
'admin',
[
'settings' => [
'wopi_url' => $this->config->getAppValue('richdocuments', 'wopi_url'),
'wopi_url' => $this->appConfig->getCollaboraUrlInternal(),
'public_wopi_url' => $this->appConfig->getCollaboraUrlPublic(),
'wopi_callback_url' => $this->appConfig->getNextcloudUrl(),
'wopi_allowlist' => $this->config->getAppValue('richdocuments', 'wopi_allowlist'),
'edit_groups' => $this->config->getAppValue('richdocuments', 'edit_groups'),
'use_groups' => $this->config->getAppValue('richdocuments', 'use_groups'),

Просмотреть файл

@ -313,6 +313,6 @@ class TokenManager {
}
public function getUrlSrc(File $file): string {
return $this->wopiParser->getUrlSrc($file->getMimeType())['urlsrc'];
return $this->wopiParser->getUrlSrcValue($file->getMimeType());
}
}

Просмотреть файл

@ -21,40 +21,37 @@
namespace OCA\Richdocuments\WOPI;
use Exception;
use OCA\Richdocuments\Service\DiscoveryService;
use Psr\Log\LoggerInterface;
class Parser {
/** @var DiscoveryManager */
private $discoveryManager;
/** @var LoggerInterface */
private $logger;
/**
* @param DiscoveryManager $discoveryManager
* @param LoggerInterface $logger
*/
public function __construct(DiscoveryManager $discoveryManager, LoggerInterface $logger) {
$this->discoveryManager = $discoveryManager;
$this->logger = $logger;
public function __construct(
private DiscoveryService $discoveryService,
private LoggerInterface $logger
) {
}
/**
* @param $mimetype
* @return array
* @throws \Exception
* @throws Exception
*/
public function getUrlSrc($mimetype) {
$discovery = $this->discoveryManager->get();
public function getUrlSrcValue(string $appName): string {
$result = $this->getUrlSrc($appName)['urlsrc'];
// Fix for potentially escaped urls that are misconfigured on the Collabora docker image
// https://github.com/nextcloud/richdocuments/issues/3262
$result = str_replace('\.', '.', $result);
return (string)$result;
}
/**
* @throws Exception
*/
private function getUrlSrc(string $mimetype): array {
$discovery = $this->discoveryService->get();
$this->logger->debug('WOPI::getUrlSrc discovery: {discovery}', ['discovery' => $discovery]);
if (\PHP_VERSION_ID < 80000) {
$loadEntities = libxml_disable_entity_loader(true);
$discoveryParsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
} else {
$discoveryParsed = simplexml_load_string($discovery);
}
$result = $discoveryParsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if ($result && count($result) > 0) {
@ -65,6 +62,6 @@ class Parser {
}
$this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response: {discovery}', ['mimetype' => $mimetype, 'discovery' => $discovery]);
throw new \Exception('Could not find urlsrc in WOPI');
throw new Exception('Could not find urlsrc for ' . $mimetype . ' in WOPI discovery response');
}
}

Просмотреть файл

@ -24,50 +24,57 @@
<div>
<div class="section">
<h2>{{ productName }}</h2>
<p v-if="hasNextcloudBranding">
<p v-if="hasNextcloudBranding" class="description">
{{ t('richdocuments', 'Nextcloud Office is a powerful Collabora Online based online office suite with collaborative editing, which supports all major documents, spreadsheet and presentation file formats and works together with all modern browsers.') }}
</p>
<p v-else>
<p v-else class="description">
{{ t('richdocuments', 'Collabora Online is a powerful LibreOffice-based online office suite with collaborative editing, which supports all major documents, spreadsheet and presentation file formats and works together with all modern browsers.') }}
</p>
<!-- FIXME: Update this to NcNoteCard for NC25+ once we update the Nextcloud Vue-component library -->
<div v-if="settings.wopi_url && settings.wopi_url !== '' && !settings.wopi_allowlist" id="security-warning-state-warning">
<span class="icon icon-error-white" />
<span class="message">
<div v-if="settings.wopi_url && settings.wopi_url !== ''">
<NcNoteCard v-if="serverError == 2" type="error">
<p>{{ t('richdocuments', 'Could not establish connection to the Collabora Online server.') }}</p>
<p>{{ errorMessage }}</p>
<p v-if="isNginx && serverMode === 'builtin'">
{{ t('richdocuments', 'This might be due to a missing configuration of your web server. For more information, please visit: ') }}
<a title="Connecting Collabora Online Single Click with Nginx"
href="https://www.collaboraoffice.com/online/connecting-collabora-online-single-click-with-nginx/"
target="_blank"
rel="noopener"
class="external">{{ t('richdocuments', 'Connecting Collabora Online Single Click with Nginx') }}</a>
</p>
</NcNoteCard>
<NcNoteCard v-else-if="serverError == 1" type="warning">
<p>{{ t('richdocuments', 'Setting up a new server') }}</p>
</NcNoteCard>
<NcNoteCard v-else-if="serverError == 3" type="error">
<p>{{ t('richdocuments', 'Collabora Online should use the same protocol as the server installation.') }}</p>
</NcNoteCard>
<NcNoteCard v-else type="success">
<p>{{ t('richdocuments', 'Collabora Online server is reachable.') }}</p>
<p>{{ settings.product_name }} {{ settings.product_version }} {{ settings.product_hash }}</p>
<p class="description">
<strong>{{ t('richdocuments', 'URL used by the browser:') }}</strong> <code>{{ settings.public_wopi_url }}</code><br>
<strong>{{ t('richdocuments', 'Nextcloud URL used by Collabora:') }}</strong> <code>{{ callbackUrl }}</code>
<i>{{ settings.wopi_callback_url ? '' : '(Determined from the browser URL)' }}</i>
</p>
</NcNoteCard>
</div>
<NcNoteCard v-else type="warning">
<p>{{ t('richdocuments', 'Please configure a Collabora Online server to start editing documents') }}</p>
</NcNoteCard>
<NcNoteCard v-if="settings.wopi_url && settings.wopi_url !== '' && !settings.wopi_allowlist" type="warning">
<p>
{{ t('richdocuments', 'You have not configured the allow-list for WOPI requests. Without this setting users may download restricted files via WOPI requests to the Nextcloud server.') }}
<a title="WOPI settings documentation"
href="https://docs.nextcloud.com/server/latest/admin_manual/office/configuration.html#wopi-settings"
target="_blank"
rel="noopener"
class="external">{{ t('richdocuments', 'Click here for more info') }}</a>
</span>
</div>
<div v-if="settings.wopi_url && settings.wopi_url !== ''">
<div v-if="serverError == 2 && isNginx && serverMode === 'builtin'" id="security-warning-state-failure">
<span class="icon icon-close-white" /><span class="message">{{ t('richdocuments', 'Could not establish connection to the Collabora Online server. This might be due to a missing configuration of your web server. For more information, please visit: ') }}<a title="Connecting Collabora Online Single Click with Nginx"
href="https://www.collaboraoffice.com/online/connecting-collabora-online-single-click-with-nginx/"
target="_blank"
rel="noopener"
class="external">{{ t('richdocuments', 'Connecting Collabora Online Single Click with Nginx') }}</a></span>
</div>
<div v-else-if="serverError == 2" id="security-warning-state-failure">
<span class="icon icon-close-white" /><span class="message">{{ t('richdocuments', 'Could not establish connection to the Collabora Online server.') }}</span>
</div>
<div v-else-if="serverError == 1" id="security-warning-state-failure">
<span class="icon icon-loading" /><span class="message">{{ t('richdocuments', 'Setting up a new server') }}</span>
</div>
<div v-else-if="serverError == 3" id="security-warning-state-failure">
<span class="icon icon-close-white" /><span class="message">{{ t('richdocuments', 'Collabora Online should use the same protocol as the server installation.') }}</span>
</div>
<div v-else id="security-warning-state-ok">
<span class="icon icon-checkmark-white" /><span class="message">{{ t('richdocuments', 'Collabora Online server is reachable.') }}</span>
</div>
</div>
<div v-else id="security-warning-state-warning">
<span class="icon icon-error-white" /><span class="message">{{ t('richdocuments', 'Please configure a Collabora Online server to start editing documents') }}</span>
</div>
</p>
</NcNoteCard>
<fieldset>
<div>
@ -101,7 +108,7 @@
:disabled="updating"
@change="updateServer">
<label for="disable_certificate_verification">{{ t('richdocuments', 'Disable certificate verification (insecure)') }}</label><br>
<em>{{ t('Enable if your Collabora Online server uses a self signed certificate') }}</em>
<em>{{ t('richdocuments', 'Enable if your Collabora Online server uses a self signed certificate') }}</em>
</p>
</form>
</div>
@ -398,7 +405,7 @@ import Vue from 'vue'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl, generateFilePath } from '@nextcloud/router'
import { showWarning, showError } from '@nextcloud/dialogs'
import { NcModal, NcMultiselect } from '@nextcloud/vue'
import { NcModal, NcMultiselect, NcNoteCard } from '@nextcloud/vue'
import axios from '@nextcloud/axios'
import SettingsCheckbox from './SettingsCheckbox.vue'
import SettingsInputText from './SettingsInputText.vue'
@ -409,6 +416,7 @@ import SettingsInputFile from './SettingsInputFile.vue'
import SettingsFontList from './SettingsFontList.vue'
import '@nextcloud/dialogs/dist/index.css'
import { getCallbackBaseUrl } from '../helpers/url.js'
const SERVER_STATE_OK = 0
const SERVER_STATE_LOADING = 1
@ -433,6 +441,7 @@ export default {
SettingsInputFile,
SettingsFontList,
NcModal,
NcNoteCard,
},
props: {
initial: {
@ -446,7 +455,8 @@ export default {
hasNextcloudBranding: loadState('richdocuments', 'hasNextcloudBranding', true),
serverMode: '',
serverError: Object.values(OC.getCapabilities().richdocuments.collabora).length > 0 ? SERVER_STATE_OK : SERVER_STATE_CONNECTION_ERROR,
serverError: SERVER_STATE_LOADING,
errorMessage: null,
hostErrors: [window.location.host === 'localhost' || window.location.host === '127.0.0.1', window.location.protocol !== 'https:', false],
demoServers: null,
CODEInstalled: 'richdocumentscode' in OC.appswebroots,
@ -512,9 +522,12 @@ export default {
</remote_font_config>
`
},
callbackUrl() {
return this.settings.wopi_callback_url ? this.settings.wopi_callback_url : getCallbackBaseUrl()
},
},
watch: {
'settings.wopi_url'(newVal, oldVal) {
'settings.public_wopi_url'(newVal, oldVal) {
if (newVal !== oldVal) {
const protocol = this.checkUrlProtocol(newVal)
const nextcloudProtocol = this.checkUrlProtocol(window.location.href)
@ -522,6 +535,9 @@ export default {
else this.serverError = Object.values(OC.getCapabilities().richdocuments.collabora).length > 0 ? SERVER_STATE_OK : SERVER_STATE_CONNECTION_ERROR
}
},
isSetup() {
this.toggleTemplateSettings()
},
},
beforeMount() {
for (const key in this.initial.settings) {
@ -566,8 +582,34 @@ export default {
this.CODEAppID = 'richdocumentscode_arm64'
}
this.checkIfDemoServerIsActive()
this.checkSettings()
this.toggleTemplateSettings()
},
methods: {
async checkSettings() {
this.errorMessage = null
this.updating = true
this.serverError = SERVER_STATE_LOADING
let result
try {
result = await axios.get(generateUrl('/apps/richdocuments/settings/check'))
this.serverError = SERVER_STATE_OK
} catch (e) {
this.serverError = SERVER_STATE_CONNECTION_ERROR
result = e.response
const { message } = e.response.data.data
this.errorMessage = message
}
this.updating = false
const { settings } = result?.data?.data || {}
for (const settingKey in settings) {
this.settings[settingKey] = settings[settingKey]
}
},
async fetchDemoServers() {
try {
const result = await axios.get(generateUrl('/apps/richdocuments/settings/demo'))
@ -654,6 +696,7 @@ export default {
this.checkIfDemoServerIsActive()
},
async updateSettings(data) {
this.errorMessage = null
this.updating = true
try {
const result = await axios.post(
@ -663,15 +706,17 @@ export default {
this.updating = false
const { message } = result?.data?.data || {}
const { settings } = result?.data?.data || {}
if (message && message.length > 0) {
showWarning(message)
for (const settingKey in settings) {
this.settings[settingKey] = settings[settingKey]
}
return result
} catch (e) {
this.updating = false
const { message } = e.response.data.data
this.errorMessage = message
throw e
}
},
@ -748,6 +793,13 @@ export default {
this.settings.fonts.splice(index, 1)
}
},
toggleTemplateSettings() {
if (this.isSetup) {
document.getElementById('richdocuments-templates').classList.remove('hidden')
} else {
document.getElementById('richdocuments-templates').classList.add('hidden')
}
},
},
}
</script>
@ -757,6 +809,14 @@ export default {
margin-bottom: 15px;
}
.notecard:deep(p:last-child) {
margin-bottom: 0;
}
.description {
color: var(--color-text-maxcontrast);
}
p.checkbox-details {
margin-left: 25px;
margin-top: -10px;
@ -782,13 +842,6 @@ export default {
border-bottom: 1px solid var(--color-border);
}
#security-warning-state-failure,
#security-warning-state-warning,
#security-warning-state-ok {
margin-top: 10px;
margin-bottom: 20px;
}
.option-inline {
margin-left: 25px;
&:not(.multiselect) {

Просмотреть файл

@ -797,6 +797,8 @@ $(document).ready(function() {
OCA.RichDocuments.documentsMain = documentsMain
Config.update('wopi_callback_url', loadState('richdocuments', 'wopi_callback_url', ''))
if (shouldAskForGuestName()) {
PostMessages.sendPostMessage('parent', 'NC_ShowNamePicker')
$('#documents-content').guestNamePicker()

Просмотреть файл

@ -32,22 +32,29 @@ const getSearchParam = (name) => {
return decodeURI(results[1]) || ''
}
const getCallbackBaseUrl = () => {
const callbackUrl = Config.get('wopi_callback_url')
return callbackUrl || window.location.protocol + '//' + window.location.host + getRootUrl() + '/'
}
const getWopiSrc = (fileId) => {
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
// index.php is forced here to avoid different wopi srcs for the same document
const wopiurl = getCallbackBaseUrl() + '/index.php/apps/richdocuments/wopi/files/' + fileId
console.debug('[getWopiUrl] ' + wopiurl)
return wopiurl
}
const getWopiUrl = ({ fileId, title, readOnly, closeButton, revisionHistory, target = undefined }) => {
// Only set the revision history parameter if the versions app is enabled
revisionHistory = revisionHistory && window?.oc_appswebroots?.files_versions
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
// index.php is forced here to avoid different wopi srcs for the same document
const wopiurl = window.location.protocol + '//' + window.location.host + getRootUrl() + '/index.php/apps/richdocuments/wopi/files/' + fileId
console.debug('[getWopiUrl] ' + wopiurl)
const wopisrc = encodeURIComponent(wopiurl)
// urlsrc - the URL from discovery xml that we access for the particular
// document; we add various parameters to that.
// The discovery is available at
// https://<loolwsd-server>:9980/hosting/discovery
return Config.get('urlsrc')
+ 'WOPISrc=' + wopisrc
+ 'WOPISrc=' + encodeURIComponent(getWopiSrc(fileId))
+ '&title=' + encodeURIComponent(title)
+ '&lang=' + languageToBCP47()
+ (closeButton ? '&closebutton=1' : '')
@ -97,6 +104,7 @@ const getNextcloudUrl = () => {
export {
getSearchParam,
getWopiUrl,
getCallbackBaseUrl,
getDocumentUrlFromTemplate,
getDocumentUrlForPublicFile,

Просмотреть файл

@ -243,6 +243,7 @@ export default {
fileId: fileid, shareToken: this.shareToken, version,
})
Config.update('urlsrc', data.urlSrc)
Config.update('wopi_callback_url', loadState('richdocuments', 'wopi_callback_url', ''))
// Generate form and submit to the iframe
const action = getWopiUrl({

Просмотреть файл

@ -7,7 +7,7 @@ script('files', 'jquery.fileupload');
<div id="admin-vue" data-initial="<?php p(json_encode($_['settings'], true)); ?>"></div>
<?php if ($_['settings']['templatesAvailable'] === true) { ?>
<form class="section" id="richdocuments-templates" method="post" action="/template/">
<form class="section hidden" id="richdocuments-templates" method="post" action="/template/">
<input name="files" class="hidden-visually" id="add-template" type="file" />
<h2>
<?php p($l->t('Global templates')) ?>