pdfdraw/lib/Controller/ViewerController.php
2020-11-25 15:38:38 +01:00

453 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/**
* @copyright Copyright (C) 2018, struktur AG
*
* @author Joachim Bauch <mail@joachim-bauch.de>
*
* @license AGPL-3.0
*
* 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 <https://www.gnu.org/licenses/>.
*/
namespace OCA\Pdfdraw\Controller;
use \Firebase\JWT\JWT;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Constants;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
class ViewerController extends Controller {
/** @var string */
private $userId;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IRootFolder */
private $root;
/** @var L10N */
private $l10n;
/** @var ILogger */
private $logger;
/** @var IUserManager */
private $userManager;
/** @var IUserSession */
private $userSession;
/** @var ISession */
private $session;
/** @var IConfig */
private $config;
/** @var ISecureRandom */
private $secureRandom;
/** @var IManager */
private $shareManager;
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* @param string $AppName
* @param IRequest $request
* @param $userId
* @param IRootFolder $root
* @param IL10N $l10n
* @param ILogger $logger
* @param IURLGenerator $urlGenerator
* @param IUserManager $userManager
* @param IUserSession $userSession
* @param ISession $session
* @param IConfig $config
* @param ISecureRandom $secureRandom
* @param IManager $shareManager
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
string $AppName,
IRequest $request,
$UserId,
IRootFolder $root,
IL10N $l10n,
ILogger $logger,
IURLGenerator $urlGenerator,
IUserManager $userManager,
IUserSession $userSession,
ISession $session,
IConfig $config,
ISecureRandom $secureRandom,
IManager $shareManager,
EventDispatcherInterface $dispatcher) {
parent::__construct($AppName, $request);
$this->userId = $UserId;
$this->root = $root;
$this->l10n = $l10n;
$this->logger = $logger;
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->userSession = $userSession;
$this->session = $session;
$this->config = $config;
$this->secureRandom = $secureRandom;
$this->shareManager =$shareManager;
$this->dispatcher = $dispatcher;
}
/**
* Getting file by identifier
*
* @param integer $userId - user identifier
* @param integer $fileId - file identifier
*
* @return array
*/
private function getFile($userId, $fileId) {
if (empty($fileId)) {
return [NULL, $this->l10n->t("FileId is empty")];
}
if ($userId !== NULL) {
$files = $this->root->getUserFolder($userId)->getById($fileId);
} else {
$this->logger->debug("getFile by unknown user: " . $fileId, array("app" => $this->appName));
$files = $this->root->getById($fileId);
}
if (empty($files)) {
return [NULL, $this->l10n->t("File not found")];
}
$file = $files[0];
if (!$file->isReadable()) {
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
}
return [$file, NULL];
}
private function getShare($token) {
if (empty($token)) {
return [NULL, $this->l10n->t("Token is empty")];
}
$share;
try {
$share = $this->shareManager->getShareByToken($token);
} catch (ShareNotFound $e) {
$this->logger->error("getShare error: " . $e->getMessage(), array("app" => $this->appName));
$share = NULL;
}
if ($share === NULL || $share === false) {
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
}
if ($share->getPassword()
&& (!$this->session->exists("public_link_authenticated")
|| $this->session->get("public_link_authenticated") !== (string) $share->getId())) {
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
}
return [$share, NULL];
}
private function getFileByToken($fileId, $token) {
list ($share, $error) = $this->getShare($token);
if (isset($error)) {
return [NULL, $error];
}
if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
return [NULL, $this->l10n->t("You do not have enough permissions to view the file")];
}
try {
$node = $share->getNode();
} catch (NotFoundException $e) {
$this->logger->error("getFileByToken error: " . $e->getMessage(), array("app" => $this->appName));
return [NULL, $this->l10n->t("File not found")];
}
if ($node instanceof Folder) {
$files = $node->getById($fileId);
if (empty($files)) {
return [NULL, $this->l10n->t("File not found")];
}
$file = $files[0];
} else {
$file = $node;
}
return [$file, NULL];
}
private function getDownloadSecret(): string {
$secret = $this->config->getAppValue('pdfdraw', 'download-token');
if (empty($secret)) {
$secret = $this->secureRandom->generate(32);
$this->config->setAppValue('pdfdraw', 'download-token', $secret);
}
return $secret;
}
/**
* Generate secure link to download document
*
* @param integer $fileId - file identifier
* @param string $token - access token
*
* @return string
*/
private function getUrl($fileId, $token = NULL) {
$user = $this->userSession->getUser();
$userId = NULL;
if (!empty($user)) {
$userId = $user->getUID();
}
$secret = $this->getDownloadSecret();
$data = [
'iss' => $this->urlGenerator->getBaseUrl(),
'sub' => $userId,
'exp' => time() + (5 * 60), // 5 minutes
'fileId' => $fileId,
'token' => $token,
];
$jwt = JWT::encode($data, $secret);
$fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".viewer.downloadFile", ["token" => $jwt]);
return $fileUrl;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @return TemplateResponse|RedirectResponse
*/
public function index($fileId, $token = null) {
if (empty($token) && !$this->userSession->isLoggedIn()) {
$redirectUrl = $this->urlGenerator->linkToRoute("core.login.showLoginForm", [
"redirect_url" => $this->request->getRequestUri()
]);
return new RedirectResponse($redirectUrl);
}
list($file, $error) = empty($token) ? $this->getFile($this->userId, $fileId) : $this->getFileByToken($fileId, $token);
if (isset($error)) {
$this->logger->error("index: " . $fileId . " " . $error, array("app" => $this->appName));
return ['error' => $error];
}
$backend = $this->config->getAppValue('pdfdraw', 'backend');
if (!empty($backend)) {
$backend = json_decode($backend);
}
if (empty($backend) || empty($backend->server)) {
return ['error' => $this->l10n->t('No backend configured.')];
}
$fileUrl = $this->getUrl($fileId, $token);
$url = $this->urlGenerator->getBaseUrl() . '/apps/pdfdraw/viewer/' . $file->getId() . '?file=' . $fileUrl;
if (!empty($token)) {
$url = $url . '&token=' . $token;
}
$params = [
'url' => $url,
];
$response = new TemplateResponse($this->appName, 'index', $params);
$policy = new ContentSecurityPolicy();
$policy->addAllowedFrameDomain('\'self\'');
$response->setContentSecurityPolicy($policy);
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @return TemplateResponse|RedirectResponse
*/
public function publicIndex($fileId, $token) {
return $this->index($fileId, $token);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return TemplateResponse|array
*/
public function viewer(string $fileId, $token = null) {
list($file, $error) = empty($token) ? $this->getFile($this->userId, $fileId) : $this->getFileByToken($fileId, $token);
if (isset($error)) {
$this->logger->error("viewer: " . $fileId . " " . $error, array("app" => $this->appName));
return ['error' => $error];
}
if (!empty($token)) {
list ($share, $error) = $this->getShare($token);
if (isset($error)) {
return [NULL, $error];
}
$permissions = $share->getPermissions();
} else {
$permissions = $file->getPermissions();
}
$event = new GenericEvent($file, [
'permissions' => $permissions,
'token' => $token,
]);
$this->dispatcher->dispatch('OCA\\PdfDraw::getPermissions', $event);
$permissions = $event->getArgument('permissions');
$currentUser = $this->userManager->get($this->userId);
$displayName= "";
if ($currentUser instanceof IUser) {
$displayName = $currentUser->getDisplayName();
}
$event = new GenericEvent($file, [
'displayName' => $displayName,
'token' => $token,
]);
$this->dispatcher->dispatch('OCA\\PdfDraw::getDisplayName', $event);
$displayName = $event->getArgument('displayName');
$backend = $this->config->getAppValue('pdfdraw', 'backend');
$server = null;
$url = null;
$jwt = null;
if (!empty($backend)) {
$backend = json_decode($backend);
$secret = $backend->secret;
$server = $backend->server;
$url = parse_url($server);
$token = [
'iss' => $this->urlGenerator->getBaseUrl(),
'sub' => $this->userId,
'exp' => time() + 86400, // 24 hours
'file' => $fileId,
'filename' => $file->getName(),
'displayname' => $displayName,
'permissions' => $permissions,
];
$jwt = JWT::encode($token, $secret);
}
$params = [
'urlGenerator' => $this->urlGenerator,
'fileId' => $fileId,
'userId' => $this->userId,
'displayName' => $displayName,
'server' => $server,
'token' => $jwt,
'permissions' => $permissions,
];
$response = new TemplateResponse($this->appName, 'viewer', $params, 'blank');
$policy = new ContentSecurityPolicy();
if (!empty($url)) {
// Allow access to backend server.
$host = $url['host'];
if (!empty($url['port'])) {
$host = $host . ':' . $url['port'];
}
$policy->addAllowedConnectDomain($url['scheme'] . '://' . $host);
if ($url['scheme'] === 'http') {
$policy->addAllowedConnectDomain('ws://' . $host);
} else {
$policy->addAllowedConnectDomain('wss://' . $host);
}
}
$policy->addAllowedChildSrcDomain('\'self\'');
$policy->addAllowedFontDomain('data:');
$policy->addAllowedImageDomain('*');
$policy->allowEvalScript(true); // Required for paper.js
$response->setContentSecurityPolicy($policy);
return $response;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return DataDisplayResponse
*/
public function downloadFile(string $token) {
$secret = $this->getDownloadSecret();
try {
$data = JWT::decode($token, $secret, ['HS256']);
} catch (\Exception $e) {
$this->logger->logException($e, [
'message' => 'download: ' . $token,
'app' => $this->appName,
]);
return new JSONResponse(["message" => $this->l10n->t("Invalid token")], Http::STATUS_FORBIDDEN);
}
if ($this->userSession->isLoggedIn()) {
$userId = $this->userSession->getUser()->getUID();
} else {
$userId = $data->sub;
}
$token = isset($data->token) ? $data->token : null;
list ($file, $error) = empty($token) ? $this->getFile($userId, $data->fileId) : $this->getFileByToken($data->fileId, $token);
if (isset($error)) {
$this->logger->error('Could not get file: ' . $error, ['app' => $this->appName]);
return new JSONResponse(["message" => $error]);
}
if ($this->userSession->isLoggedIn() && !$file->isReadable()) {
return new JSONResponse(["message" => $this->l10n->t("Access denied")], Http::STATUS_FORBIDDEN);
}
return new DataDisplayResponse($file->getContent(), Http::STATUS_OK, [
'Content-Type' => $file->getMimeType(),
]);
}
}