Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/richdocuments.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--appinfo/routes.php1
-rw-r--r--lib/AppConfig.php13
-rw-r--r--lib/AppInfo/Application.php83
-rw-r--r--lib/Controller/DirectViewController.php12
-rw-r--r--lib/Controller/DocumentController.php99
-rw-r--r--lib/Controller/DocumentTrait.php42
-rw-r--r--lib/Listener/CSPListener.php126
-rw-r--r--lib/Service/FederationService.php25
-rw-r--r--src/files.js56
-rw-r--r--src/services/preload.js43
-rw-r--r--src/view/FilesAppIntegration.js40
-rw-r--r--tests/lib/Listener/CSPListenerTest.php211
12 files changed, 423 insertions, 328 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 5e358e0d..760e0a83 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -30,7 +30,6 @@ return [
//documents
['name' => 'document#index', 'url' => 'index', 'verb' => 'GET'],
['name' => 'document#remote', 'url' => 'remote', 'verb' => 'GET'],
- ['name' => 'document#openRemoteFile', 'url' => 'open', 'verb' => 'GET'],
['name' => 'document#createFromTemplate', 'url' => 'indexTemplate', 'verb' => 'GET'],
['name' => 'document#publicPage', 'url' => '/public', 'verb' => 'GET'],
diff --git a/lib/AppConfig.php b/lib/AppConfig.php
index 6a84dd0c..f8f54f59 100644
--- a/lib/AppConfig.php
+++ b/lib/AppConfig.php
@@ -15,6 +15,9 @@ use OCA\Richdocuments\AppInfo\Application;
use \OCP\IConfig;
class AppConfig {
+ public const WOPI_URL = 'wopi_url';
+ public const WOPI_URL_PUBLIC = 'wopi_url_public';
+
public const FEDERATION_USE_TRUSTED_DOMAINS = 'federation_use_trusted_domains';
public const SYSTEM_GS_TRUSTED_HOSTS = 'gs.trustedHosts';
@@ -113,7 +116,7 @@ class AppConfig {
/**
* Returns a list of trusted domains from the gs.trustedHosts config
*/
- public function getTrustedDomains(): array {
+ public function getGlobalScaleTrustedHosts(): array {
return $this->config->getSystemValue(self::SYSTEM_GS_TRUSTED_HOSTS, []);
}
@@ -123,4 +126,12 @@ class AppConfig {
public function isTrustedDomainAllowedForFederation(): bool {
return $this->config->getAppValue(Application::APPNAME, self::FEDERATION_USE_TRUSTED_DOMAINS, 'no') === 'yes';
}
+
+ public function getCollaboraUrlPublic(): string {
+ return $this->config->getAppValue(Application::APPNAME, self::WOPI_URL_PUBLIC, $this->getCollaboraUrlInternal());
+ }
+
+ public function getCollaboraUrlInternal(): string {
+ return $this->config->getAppValue(Application::APPNAME, self::WOPI_URL, '');
+ }
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index e75905be..238bac7b 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -25,9 +25,9 @@
namespace OCA\Richdocuments\AppInfo;
use OC\EventDispatcher\SymfonyAdapter;
-use OC\Security\CSP\ContentSecurityPolicy;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
+use OCA\Richdocuments\Listener\CSPListener;
use OCA\Richdocuments\Middleware\WOPIMiddleware;
use OCA\Richdocuments\Listener\FileCreatedFromTemplateListener;
use OCA\Richdocuments\PermissionManager;
@@ -37,7 +37,6 @@ use OCA\Richdocuments\Preview\OOXML;
use OCA\Richdocuments\Preview\OpenDocument;
use OCA\Richdocuments\Preview\Pdf;
use OCA\Richdocuments\Service\CapabilitiesService;
-use OCA\Richdocuments\Service\FederationService;
use OCA\Richdocuments\Service\InitialStateService;
use OCA\Richdocuments\Template\CollaboraTemplateProvider;
use OCA\Richdocuments\WOPI\DiscoveryManager;
@@ -53,13 +52,13 @@ use OCP\Files\Template\TemplateFileCreator;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
+use OCP\Security\CSP\AddContentSecurityPolicyEvent;
class Application extends App implements IBootstrap {
public const APPNAME = 'richdocuments';
public function __construct(array $urlParams = array()) {
parent::__construct(self::APPNAME, $urlParams);
- $this->getContainer()->registerCapability(Capabilities::class);
}
@@ -68,6 +67,7 @@ class Application extends App implements IBootstrap {
$context->registerCapability(Capabilities::class);
$context->registerMiddleWare(WOPIMiddleware::class);
$context->registerEventListener(FileCreatedFromTemplateEvent::class, FileCreatedFromTemplateListener::class);
+ $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);
}
public function boot(IBootContext $context): void {
@@ -160,7 +160,6 @@ class Application extends App implements IBootstrap {
}
$this->registerProvider();
- $this->updateCSP();
$this->checkAndEnableCODEServer();
});
}
@@ -192,68 +191,6 @@ class Application extends App implements IBootstrap {
});
}
- public function updateCSP() {
- $container = $this->getContainer();
-
- // Do not apply CSP rules on WebDAV/OCS
- // Ideally this could be a middleware running after the controller execution before rendering the result to only do it on page response
- $scriptNameParts = explode('/', $container->getServer()->getRequest()->getScriptName());
- $scriptFile = end($scriptNameParts);
- if ($scriptFile !== 'index.php') {
- return;
- }
-
- $publicWopiUrl = $container->getServer()->getConfig()->getAppValue('richdocuments', 'public_wopi_url', '');
- $publicWopiUrl = $publicWopiUrl === '' ? \OC::$server->getConfig()->getAppValue('richdocuments', 'wopi_url') : $publicWopiUrl;
- $cspManager = $container->getServer()->getContentSecurityPolicyManager();
- $policy = new ContentSecurityPolicy();
- if ($publicWopiUrl !== '') {
- $policy->addAllowedFrameDomain('\'self\'');
- $policy->addAllowedFrameDomain($this->domainOnly($publicWopiUrl));
- $policy->addAllowedFormActionDomain($this->domainOnly($publicWopiUrl));
- }
-
- /**
- * Dynamically add CSP for federated editing
- */
- if ($container->getServer()->getAppManager()->isEnabledForUser('federation')) {
- /** @var FederationService $federationService */
- $federationService = \OC::$server->query(FederationService::class);
-
- // Always add trusted servers on global scale
- /** @var \OCP\GlobalScale\IConfig $globalScale */
- $globalScale = $container->query(\OCP\GlobalScale\IConfig::class);
- if ($globalScale->isGlobalScaleEnabled()) {
- $trustedList = \OC::$server->getConfig()->getSystemValue('gs.trustedHosts', []);
- foreach ($trustedList as $server) {
- $policy->addAllowedFrameDomain($server);
- $this->addTrustedRemote($policy, $server);
- $policy->addAllowedFormActionDomain($server);
- }
- }
- $remoteAccess = $container->getServer()->getRequest()->getParam('richdocuments_remote_access');
-
- if ($remoteAccess && $federationService->isTrustedRemote($remoteAccess)) {
- $this->addTrustedRemote($policy, $remoteAccess);
- }
- }
-
- $cspManager->addDefaultPolicy($policy);
- }
-
- private function addTrustedRemote($policy, $url) {
- $federationService = \OC::$server->get(FederationService::class);
- try {
- $remoteCollabora = $federationService->getRemoteCollaboraURL($url);
- $policy->addAllowedFrameDomain($url);
- $policy->addAllowedFrameDomain($remoteCollabora);
- } catch (\Exception $e) {
- // We can ignore this exception for adding predefined domains to the CSP as it it would then just
- // reload the page to set a proper allowed frame domain if we don't have a fixed list of trusted
- // remotes in a global scale scenario
- }
- }
-
public function checkAndEnableCODEServer() {
// Supported only on Linux OS, and x86_64 & ARM64 platforms
$supportedArchs = array('x86_64', 'aarch64');
@@ -295,18 +232,4 @@ class Application extends App implements IBootstrap {
$capabilitiesService->refetch();
}
}
-
- /**
- * Strips the path and query parameters from the URL.
- *
- * @param string $url
- * @return string
- */
- private function domainOnly($url) {
- $parsed_url = parse_url($url);
- $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
- $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
- $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
- return "$scheme$host$port";
- }
}
diff --git a/lib/Controller/DirectViewController.php b/lib/Controller/DirectViewController.php
index a36ac287..343cf231 100644
--- a/lib/Controller/DirectViewController.php
+++ b/lib/Controller/DirectViewController.php
@@ -32,7 +32,6 @@ use OCA\Richdocuments\TokenManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
-use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
@@ -45,6 +44,7 @@ use OCP\ILogger;
use OCP\IRequest;
class DirectViewController extends Controller {
+ use DocumentTrait;
/** @var IRootFolder */
private $rootFolder;
@@ -180,10 +180,7 @@ class DirectViewController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $policy = new ContentSecurityPolicy();
- $policy->allowInlineScript(true);
- $policy->addAllowedFrameDomain($this->appConfig->getAppValue('public_wopi_url'));
- $response->setContentSecurityPolicy($policy);
+ $this->applyPolicies($response);
return $response;
} catch (\Exception $e) {
$this->logger->logException($e);
@@ -236,10 +233,7 @@ class DirectViewController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $policy = new ContentSecurityPolicy();
- $policy->allowInlineScript(true);
- $policy->addAllowedFrameDomain($this->appConfig->getAppValue('public_wopi_url'));
- $response->setContentSecurityPolicy($policy);
+ $this->applyPolicies($response);
return $response;
}
} catch (\Exception $e) {
diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php
index c12df105..399f702e 100644
--- a/lib/Controller/DocumentController.php
+++ b/lib/Controller/DocumentController.php
@@ -11,7 +11,6 @@
namespace OCA\Richdocuments\Controller;
-use OCA\Richdocuments\Events\BeforeFederationRedirectEvent;
use OCA\Richdocuments\Service\FederationService;
use OCA\Richdocuments\Service\InitialStateService;
use OCA\Richdocuments\TemplateManager;
@@ -28,8 +27,6 @@ use OCP\Files\NotPermittedException;
use \OCP\IRequest;
use \OCP\IConfig;
use \OCP\ILogger;
-use \OCP\AppFramework\Http\ContentSecurityPolicy;
-use \OCP\AppFramework\Http\FeaturePolicy;
use \OCP\AppFramework\Http\TemplateResponse;
use \OCA\Richdocuments\AppConfig;
use OCP\ISession;
@@ -37,6 +34,7 @@ use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
class DocumentController extends Controller {
+ use DocumentTrait;
/** @var string */
private $uid;
@@ -136,37 +134,6 @@ class DocumentController extends Controller {
}
/**
- * Strips the path and query parameters from the URL.
- *
- * @param string $url
- * @return string
- */
- private function domainOnly($url) {
- $parsed_url = parse_url($url);
- $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
- $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
- $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
- return "$scheme$host$port";
- }
-
- /**
- * Setup policy headers for the response
- */
- private function setupPolicy($response) {
- $wopiDomain = $this->domainOnly($this->appConfig->getAppValue('public_wopi_url'));
-
- $policy = new ContentSecurityPolicy();
- $policy->addAllowedFrameDomain($wopiDomain);
- $policy->allowInlineScript(true);
- $policy->addAllowedFormActionDomain($wopiDomain);
- $response->setContentSecurityPolicy($policy);
-
- $featurePolicy = new FeaturePolicy();
- $featurePolicy->addAllowedFullScreenDomain($wopiDomain);
- $response->setFeaturePolicy($featurePolicy);
- }
-
- /**
* @NoAdminRequired
*
* @param string $fileId
@@ -231,7 +198,7 @@ class DocumentController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $this->setupPolicy($response);
+ $this->applyPolicies($response);
return $response;
} catch (\Exception $e) {
$this->logger->logException($e, ['app' => 'richdocuments']);
@@ -290,7 +257,7 @@ class DocumentController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $this->setupPolicy($response);
+ $this->applyPolicies($response);
return $response;
}
@@ -349,7 +316,7 @@ class DocumentController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $this->setupPolicy($response);
+ $this->applyPolicies($response);
return $response;
}
} catch (\Exception $e) {
@@ -361,53 +328,6 @@ class DocumentController extends Controller {
}
/**
- * Redirect to the files app with proper CSP headers set for federated editing
- * This is a workaround since we cannot set a nonce for allowing dynamic URLs in the richdocument iframe
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- */
- public function openRemoteFile($fileId) {
- try {
- $folder = $this->rootFolder->getUserFolder($this->uid);
- $item = $folder->getById($fileId)[0];
- if (!($item instanceof File)) {
- throw new \Exception('Node is not a file');
- }
-
- if ($item->getStorage()->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class)) {
- $remote = $item->getStorage()->getRemote();
- $remoteCollabora = $this->federationService->getRemoteCollaboraURL($remote);
- if ($remoteCollabora !== '') {
- $absolute = $item->getParent()->getPath();
- $relativeFolderPath = $folder->getRelativePath($absolute);
- $relativeFilePath = $folder->getRelativePath($item->getPath());
- $url = '/index.php/apps/files/?dir=' . $relativeFolderPath .
- '&richdocuments_open=' . $relativeFilePath .
- '&richdocuments_fileId=' . $fileId .
- '&richdocuments_remote_access=' . $remote;
-
- $event = new BeforeFederationRedirectEvent(
- $item, $relativeFolderPath, $remote
- );
- $eventDispatcher = \OC::$server->getEventDispatcher();
- $eventDispatcher->dispatch(BeforeFederationRedirectEvent::class, $event);
- if ($event->getRedirectUrl()) {
- $url = $event->getRedirectUrl();
- }
- return new RedirectResponse($url);
- }
- $this->logger->warning('Failed to connect to remote collabora instance for ' . $fileId);
- }
- } catch (\Exception $e) {
- $this->logger->logException($e, ['app' => 'richdocuments']);
- return $this->renderErrorPage('Failed to open the requested file.');
- }
-
- return new TemplateResponse('core', '403', [], 'guest');
- }
-
- /**
* Open file on Source instance with token from Initiator instance
*
* @PublicPage
@@ -469,16 +389,7 @@ class DocumentController extends Controller {
$this->initialState->provideDocument($wopi);
$response = new TemplateResponse('richdocuments', 'documents', $params, 'base');
- $remoteWopi = $this->domainOnly($this->appConfig->getAppValue('wopi_url'));
- $policy = new ContentSecurityPolicy();
- $policy->addAllowedFrameDomain($remoteWopi);
- $policy->allowInlineScript(true);
- $policy->addAllowedFrameAncestorDomain('https://*');
- $response->setContentSecurityPolicy($policy);
- $featurePolicy = new FeaturePolicy();
- $featurePolicy->addAllowedFullScreenDomain($remoteWopi);
- $response->setFeaturePolicy($featurePolicy);
- $response->addHeader('X-Frame-Options', 'ALLOW');
+ $this->applyPolicies($response);
return $response;
}
} catch (ShareNotFound $e) {
diff --git a/lib/Controller/DocumentTrait.php b/lib/Controller/DocumentTrait.php
new file mode 100644
index 00000000..992bbad9
--- /dev/null
+++ b/lib/Controller/DocumentTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace OCA\Richdocuments\Controller;
+
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\FeaturePolicy;
+
+trait DocumentTrait {
+ private $appConfig;
+
+ /**
+ * Setup policy headers for the response
+ */
+ private function applyPolicies($response) {
+ $collaboraHost = $this->domainOnly($this->appConfig->getCollaboraUrlPublic());
+
+ // FIXME We can skip inline source once templates/documents.php is migrated to IInitialState
+ $policy = new ContentSecurityPolicy();
+ $policy->allowInlineScript(true);
+ $response->setContentSecurityPolicy($policy);
+
+ $featurePolicy = new FeaturePolicy();
+ $featurePolicy->addAllowedFullScreenDomain($collaboraHost);
+ $response->setFeaturePolicy($featurePolicy);
+
+ $response->addHeader('X-Frame-Options', 'ALLOW');
+ }
+
+ /**
+ * Strips the path and query parameters from the URL.
+ *
+ * @param string $url
+ * @return string
+ */
+ private function domainOnly(string $url): string {
+ $parsed_url = parse_url($url);
+ $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
+ $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
+ $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
+ return "$scheme$host$port";
+ }
+}
diff --git a/lib/Listener/CSPListener.php b/lib/Listener/CSPListener.php
new file mode 100644
index 00000000..33098ef8
--- /dev/null
+++ b/lib/Listener/CSPListener.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 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\Listener;
+
+use OCA\Richdocuments\AppConfig;
+use OCA\Richdocuments\Service\FederationService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\GlobalScale\IConfig as GlobalScaleConfig;
+use OCP\IRequest;
+use OCP\Security\CSP\AddContentSecurityPolicyEvent;
+
+class CSPListener implements IEventListener {
+ private IRequest $request;
+ private AppConfig $config;
+ private IAppManager $appManager;
+ private FederationService $federationService;
+ private GlobalScaleConfig $globalScaleConfig;
+
+ public function __construct(IRequest $request, AppConfig $config, IAppManager $appManager, FederationService $federationService, GlobalScaleConfig $globalScaleConfig) {
+ $this->request = $request;
+ $this->config = $config;
+ $this->appManager = $appManager;
+ $this->federationService = $federationService;
+ $this->globalScaleConfig = $globalScaleConfig;
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof AddContentSecurityPolicyEvent) {
+ return;
+ }
+
+ if (!$this->isPageLoad()) {
+ return;
+ }
+
+ $urls = array_merge(
+ [ $this->domainOnly($this->config->getCollaboraUrlPublic()) ],
+ $this->getFederationDomains(),
+ $this->getGSDomains()
+ );
+
+ $urls = array_filter($urls);
+
+ $policy = new EmptyContentSecurityPolicy();
+ foreach ($urls as $url) {
+ $policy->addAllowedFrameDomain($url);
+ $policy->addAllowedFormActionDomain($url);
+ $policy->addAllowedFrameAncestorDomain($url);
+ $policy->addAllowedImageDomain($url);
+ }
+
+ $event->addPolicy($policy);
+ }
+
+ private function isPageLoad(): bool {
+ $scriptNameParts = explode('/', $this->request->getScriptName());
+ return end($scriptNameParts) === 'index.php';
+ }
+
+ private function getFederationDomains(): array {
+ if (!$this->appManager->isEnabledForUser('federation')) {
+ return [];
+ }
+
+ $trustedNextcloudDomains = array_filter(array_map(function ($server) {
+ return $this->federationService->isTrustedRemote($server) ? $server : null;
+ }, $this->federationService->getTrustedServers()));
+
+ $trustedCollaboraDomains = array_filter(array_map(function ($server) {
+ try {
+ return $this->federationService->getRemoteCollaboraURL($server);
+ } catch (\Exception $e) {
+ // If there is no remote collabora server we can just skip that
+ return null;
+ }
+ }, $trustedNextcloudDomains));
+
+ return array_map(function ($url) {
+ return $this->domainOnly($url);
+ }, array_merge($trustedNextcloudDomains, $trustedCollaboraDomains));
+ }
+
+ private function getGSDomains(): array {
+ if (!$this->globalScaleConfig->isGlobalScaleEnabled()) {
+ return [];
+ }
+
+ return $this->config->getGlobalScaleTrustedHosts();
+ }
+
+ /**
+ * Strips the path and query parameters from the URL.
+ */
+ private function domainOnly(string $url): string {
+ $parsedUrl = parse_url($url);
+ $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
+ $host = $parsedUrl['host'] ?? '';
+ $port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
+ return "$scheme$host$port";
+ }
+}
diff --git a/lib/Service/FederationService.php b/lib/Service/FederationService.php
index 403df2c1..5d00c5e6 100644
--- a/lib/Service/FederationService.php
+++ b/lib/Service/FederationService.php
@@ -1,4 +1,7 @@
<?php
+
+declare(strict_types=1);
+
/**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
@@ -29,7 +32,7 @@ use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Db\Direct;
use OCA\Richdocuments\Db\Wopi;
use OCA\Richdocuments\TokenManager;
-use OCP\AppFramework\QueryException;
+use OCP\AutoloadNotAllowedException;
use OCP\Files\File;
use OCP\Files\InvalidPathException;
use OCP\Files\NotFoundException;
@@ -40,6 +43,8 @@ use OCP\ILogger;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\Share\IShare;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
class FederationService {
@@ -69,9 +74,21 @@ class FederationService {
$this->request = $request;
$this->urlGenerator = $urlGenerator;
try {
- $this->trustedServers = \OC::$server->query(\OCA\Federation\TrustedServers::class);
- } catch (QueryException $e) {
+ $this->trustedServers = \OC::$server->get(\OCA\Federation\TrustedServers::class);
+ } catch (NotFoundExceptionInterface $e) {
+ } catch (ContainerExceptionInterface $e) {
+ } catch (AutoloadNotAllowedException $e) {
+ }
+ }
+
+ public function getTrustedServers(): array {
+ if (!$this->trustedServers) {
+ return [];
}
+
+ return array_map(function (array $server) {
+ return $server['url'];
+ }, $this->trustedServers->getServers());
}
/**
@@ -118,7 +135,7 @@ class FederationService {
$domain = $this->getDomainWithoutPort($domainWithPort);
- $trustedList = array_merge($this->appConfig->getTrustedDomains(), [$this->request->getServerHost()]);
+ $trustedList = array_merge($this->appConfig->getGlobalScaleTrustedHosts(), [$this->request->getServerHost()]);
if (!is_array($trustedList)) {
return false;
}
diff --git a/src/files.js b/src/files.js
index 932381f7..e9d7e359 100644
--- a/src/files.js
+++ b/src/files.js
@@ -7,7 +7,6 @@ import { showError } from '@nextcloud/dialogs'
import { getDocumentUrlFromTemplate, getDocumentUrlForPublicFile, getDocumentUrlForFile } from './helpers/url'
import PostMessageService from './services/postMessage.tsx'
import Config from './services/config.tsx'
-import Preload from './services/preload'
import Types from './helpers/types'
import FilesAppIntegration from './view/FilesAppIntegration'
import { splitPath } from './helpers'
@@ -151,52 +150,6 @@ const odfViewer = {
documentUrl = getDocumentUrlFromTemplate(templateId, fileName, fileDir)
}
- /**
- * We need to reload the page to set a proper CSP if the file is federated
- * and the reload didn't happen for the exact same file
- *
- * @param {string} url the url
- * @param {Function} callback to be run after reload is complete
- */
- const canAccessCSP = (url, callback) => {
- let canEmbed = false
- const frame = document.createElement('iframe')
- frame.style.display = 'none'
- frame.onload = () => {
- canEmbed = true
- }
- document.body.appendChild(frame)
- frame.setAttribute('src', url)
- setTimeout(() => {
- if (!canEmbed) {
- callback()
- }
- document.body.removeChild(frame)
- }, 1000)
-
- }
-
- // FIXME: Once Nextcloud 16 is minimum requirement we can just pass the allowed domains to initial state
- // to check then if they are set properly
- const reloadForFederationCSP = (fileName, shareOwnerId) => {
- const preloadId = Preload.open ? parseInt(Preload.open.id) : -1
- if (typeof shareOwnerId !== 'undefined') {
- const lastIndex = shareOwnerId.lastIndexOf('@')
- // only redirect if remote file, not opened though reload and csp blocks the request
- if (shareOwnerId.slice(lastIndex).indexOf('/') !== -1 && fileId !== preloadId) {
- canAccessCSP('https://' + shareOwnerId.slice(lastIndex) + '/ocs/v2.php/apps/richdocuments/api/v1/federation', () => {
- console.debug('Cannot load federated instance though CSP, navigating to ', generateUrl('/apps/richdocuments/open?fileId=' + fileId))
- window.location = generateUrl('/apps/richdocuments/open?fileId=' + fileId)
- })
- }
- }
- return false
- }
-
- if (context) {
- reloadForFederationCSP(fileName, context?.shareOwnerId)
- }
-
$('head').append($('<link rel="stylesheet" type="text/css" href="' + generateFilePath('richdocuments', 'css', 'mobile.css') + '"/>'))
const $iframe = $('<iframe id="richdocumentsframe" nonce="' + btoa(OC.requestToken) + '" scrolling="no" allowfullscreen src="' + documentUrl + '" />')
@@ -361,15 +314,6 @@ $(document).ready(function() {
OC.MimeType._mimeTypeIcons['application/vnd.oasis.opendocument.graphics'] = imagePath('richdocuments', 'x-office-draw')
- // Open the template picker if there was a create parameter detected on load
- if (Preload.create && Preload.create.type && Preload.create.filename) {
- FilesAppIntegration.preloadCreate()
- }
-
- if (Preload.open) {
- FilesAppIntegration.preloadOpen()
- }
-
// Open documents if a public page is opened for a supported mimetype
const isSupportedMime = isPublic && odfViewer.supportedMimes.indexOf($('#mimetype').val()) !== -1 && odfViewer.excludeMimeFromDefaultOpen.indexOf($('#mimetype').val()) === -1
const showSecureView = isPublic && isDownloadHidden && odfViewer.hideDownloadMimes.indexOf($('#mimetype').val()) !== -1
diff --git a/src/services/preload.js b/src/services/preload.js
deleted file mode 100644
index c0cf1e1c..00000000
--- a/src/services/preload.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 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/>.
- *
- */
-
-import { getSearchParam } from '../helpers/url'
-
-const preloadCreate = getSearchParam('richdocuments_create')
-const preloadOpen = getSearchParam('richdocuments_open')
-const Preload = {}
-
-if (preloadCreate) {
- Preload.create = {
- type: getSearchParam('richdocuments_create'),
- filename: getSearchParam('richdocuments_filename'),
- }
-}
-
-if (preloadOpen) {
- Preload.open = {
- filename: preloadOpen,
- id: getSearchParam('richdocuments_fileId'),
- }
-}
-
-export default Preload
diff --git a/src/view/FilesAppIntegration.js b/src/view/FilesAppIntegration.js
index 66b7c384..a634ac83 100644
--- a/src/view/FilesAppIntegration.js
+++ b/src/view/FilesAppIntegration.js
@@ -23,11 +23,6 @@
import { generateUrl, generateRemoteUrl, getRootUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import moment from '@nextcloud/moment'
-import Preload from '../services/preload'
-import { splitPath } from '../helpers'
-import Types from '../helpers/types'
-import Config from '../services/config.tsx'
-import NewFileMenu from './NewFileMenu'
const isPublic = document.getElementById('isPublic') && document.getElementById('isPublic').value === '1'
@@ -570,41 +565,6 @@ export default {
})
},
- /**
- * Automaically open a document on page load
- */
- preloadOpen() {
- if (this.handlers.preloadOpen && this.handlers.preloadOpen(this)) {
- return
- }
-
- const fileId = Preload.open.id
- const path = Preload.open.filename
- setTimeout(function() {
- window.FileList.$fileList.one('updated', function() {
- const [, file] = splitPath(path)
- const fileModel = FileList.getModelForFile(file)
- OCA.RichDocuments.open({ path, fileId, fileModel, fileList: window.FileList })
- })
- }, 250)
- },
-
- /**
- * Automaically open a template picker on page load
- */
- preloadCreate() {
- if (this.handlers.preloadCreate && this.handlers.preloadCreate(this)) {
- return
- }
-
- setTimeout(function() {
- window.FileList.$fileList.one('updated', function() {
- const fileType = Types.getFileType(Preload.create.type, Config.get('ooxml'))
- NewFileMenu._openTemplatePicker(Preload.create.type, fileType.mime, Preload.create.filename + '.' + fileType.extension)
- })
- }, 250)
- },
-
loggingContext() {
return {
currentUser: getCurrentUser()?.uid,
diff --git a/tests/lib/Listener/CSPListenerTest.php b/tests/lib/Listener/CSPListenerTest.php
new file mode 100644
index 00000000..32d205e2
--- /dev/null
+++ b/tests/lib/Listener/CSPListenerTest.php
@@ -0,0 +1,211 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 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/>.
+ *
+ */
+
+use OC\Security\CSP\ContentSecurityPolicyManager;
+use OCA\Richdocuments\AppConfig;
+use OCA\Richdocuments\Listener\CSPListener;
+use OCA\Richdocuments\Service\FederationService;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\GlobalScale\IConfig as GlobalScaleConfig;
+use OCP\IRequest;
+use OCP\Security\CSP\AddContentSecurityPolicyEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class CSPListenerTest extends TestCase {
+
+ /** @var IRequest|MockObject */
+ private $request;
+ /** @var AppConfig|MockObject */
+ private $config;
+ /** @var IAppManager|MockObject */
+ private $appManager;
+ /** @var GlobalScaleConfig|MockObject */
+ private $gsConfig;
+ /** @var FederationService|MockObject */
+ private $federationService;
+ private CSPListener $listener;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->config = $this->createMock(AppConfig::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->gsConfig = $this->createMock(GlobalScaleConfig::class);
+ $this->federationService = $this->createMock(FederationService::class);
+
+ $this->listener = new CSPListener(
+ $this->request,
+ $this->config,
+ $this->appManager,
+ $this->federationService,
+ $this->gsConfig
+ );
+ }
+
+ private function getMergedPolicy(): ContentSecurityPolicy {
+ $eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->willReturnCallback(function ($event) {
+ $this->listener->handle($event);
+ });
+ $manager = new ContentSecurityPolicyManager($eventDispatcher);
+
+ return $manager->getDefaultPolicy();
+ }
+
+ private function expectPageLoad(): void {
+ $this->request->expects(self::once())
+ ->method('getScriptName')
+ ->willReturn('index.php');
+ }
+
+ public function testHandle() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('http://public');
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals(["http://public"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "http://public"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testHandleRemote() {
+ $manager = $this->createMock(ContentSecurityPolicyManager::class);
+ $event = new AddContentSecurityPolicyEvent(
+ $manager
+ );
+
+ $this->request->expects(self::once())
+ ->method('getScriptName')
+ ->willReturn('remote.php');
+
+ $manager->expects(self::never())
+ ->method('addDefaultPolicy');
+ $this->listener->handle($event);
+ }
+
+ public function testNotSetup() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('');
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals([], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testWopiUrlPublic() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('http://public');
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals(["http://public"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "http://public"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testWopiUrl() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('https://public/collabora/');
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals(["https://public"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "https://public"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testGS() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('https://public/collabora/');
+ $this->gsConfig->expects(self::any())
+ ->method('isGlobalScaleEnabled')
+ ->willReturn(true);
+ $this->config->expects(self::any())
+ ->method('getGlobalScaleTrustedHosts')
+ ->willReturn(['*.example.com']);
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals(["https://public", "*.example.com"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "https://public", "*.example.com"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testNoGS() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('http://internal');
+ $this->gsConfig->expects(self::any())
+ ->method('isGlobalScaleEnabled')
+ ->willReturn(false);
+
+ $policy = $this->getMergedPolicy();
+
+ self::assertEquals(["http://internal"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "http://internal"], $policy->getAllowedFormActionDomains());
+ }
+
+ public function testHandleMerged() {
+ $this->expectPageLoad();
+ $this->config->expects(self::any())
+ ->method('getCollaboraUrlPublic')
+ ->willReturn('http://public');
+
+ $eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $eventDispatcher->expects(self::once())
+ ->method('dispatchTyped')
+ ->willReturnCallback(function (AddContentSecurityPolicyEvent $event) {
+ $otherPolicy = new EmptyContentSecurityPolicy();
+ $otherPolicy->addAllowedFrameDomain('external.example.com');
+ $otherPolicy->addAllowedFormActionDomain('external.example.com');
+ $event->addPolicy($otherPolicy);
+
+ $this->listener->handle($event);
+ });
+ $manager = new ContentSecurityPolicyManager($eventDispatcher);
+
+ $policy = $manager->getDefaultPolicy();
+
+ self::assertEquals(["external.example.com", "http://public"], $policy->getAllowedFrameDomains());
+ self::assertEquals(["'self'", "external.example.com", "http://public"], $policy->getAllowedFormActionDomains());
+ }
+}