The app which enables the users to edit office documents from Nextcloud using ONLYOFFICE Document Server, allows multiple users to collaborate in real time and to save back those changes to Nextcloud
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
onlyoffice-nextcloud/lib/Controller/EditorController.php

1598 lines
57 KiB

<?php
1 month ago
/*
* Copyright (C) Ascensio System SIA, 2009-2026
*
1 month ago
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation, together with the
* additional terms provided in the LICENSE file.
*
1 month ago
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: https://www.gnu.org/licenses/agpl-3.0.html
8 years ago
*
1 month ago
* You can contact Ascensio System SIA by email at info@onlyoffice.com
* or by postal mail at 20A-6 Ernesta Birznieka-Upisha Street, Riga,
* LV-1050, Latvia, European Union.
8 years ago
*
1 month ago
* The interactive user interfaces in modified versions of the Program
* are required to display Appropriate Legal Notices in accordance with
* Section 5 of the GNU AGPL version 3.
*
1 month ago
* No trademark rights are granted under this License.
*
1 month ago
* All non-code elements of the Product, including illustrations,
* icon sets, and technical writing content, are licensed under the
* Creative Commons Attribution-ShareAlike 4.0 International License:
* https://creativecommons.org/licenses/by-sa/4.0/legalcode
*
1 month ago
* This license applies only to such non-code elements and does not
* modify or replace the licensing terms applicable to the Program's
* source code, which remains licensed under the GNU Affero General
* Public License v3.
*
1 month ago
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Onlyoffice\Controller;
3 years ago
use OCA\Files\Helper;
use OCA\Files_Sharing\SharedStorage;
3 years ago
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Mount\GroupFolderStorage;
3 years ago
use OCA\Onlyoffice\AppConfig;
use OCA\Onlyoffice\Crypt;
use OCA\Onlyoffice\DocumentService;
use OCA\Onlyoffice\EmailManager;
3 years ago
use OCA\Onlyoffice\FileUtility;
use OCA\Onlyoffice\FileVersions;
use OCA\Onlyoffice\KeyManager;
3 years ago
use OCA\Onlyoffice\TemplateManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
5 years ago
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
3 years ago
use OCP\AppFramework\Http\TemplateResponse;
8 years ago
use OCP\Constants;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IAvatarManager;
3 years ago
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
6 years ago
use OCP\IUser;
7 years ago
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
8 years ago
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
/**
* Controller with the main functions
*/
class EditorController extends Controller {
3 years ago
public function __construct(
string $appName,
3 years ago
IRequest $request,
private readonly IRootFolder $root,
private readonly IUserSession $userSession,
private readonly IUserManager $userManager,
private readonly IURLGenerator $urlGenerator,
private readonly IL10N $trans,
private readonly LoggerInterface $logger,
private readonly AppConfig $appConfig,
private readonly Crypt $crypt,
private readonly IManager $shareManager,
private readonly IGroupManager $groupManager,
private readonly FileUtility $fileUtility,
private readonly IAvatarManager $avatarManager,
private readonly EmailManager $emailManager,
private readonly DocumentService $documentService,
private readonly KeyManager $keyManager,
private readonly ?IVersionManager $versionManager,
private readonly ?FolderManager $folderManager
3 years ago
) {
parent::__construct($appName, $request);
}
/**
* Create new file in folder
*
* @param string $name - file name
* @param string $dir - folder path
* @param ?int $templateId - file identifier
* @param int $targetId - identifier of the file for using as template for create
* @param string $shareToken - access token
*
* @return DataResponse
*/
#[NoAdminRequired]
#[PublicPage]
public function create(
string $name,
string $dir,
?int $templateId = null,
int $targetId = 0,
?string $shareToken = null
): DataResponse {
$this->logger->debug("Create: $name");
if (empty($shareToken) && !$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
if (empty($name)) {
$this->logger->error("File name for creation was not found: $name");
return new DataResponse(["error" => $this->trans->t("Template not found")]);
}
$user = null;
if (empty($shareToken)) {
$user = $this->userSession->getUser();
$userId = $user->getUID();
$userFolder = $this->root->getUserFolder($userId);
} else {
[$userFolder, $error, $share] = $this->fileUtility->getNodeByToken($shareToken);
if (isset($error)) {
$this->logger->error("Create: $error");
return new DataResponse(["error" => $error]);
}
if ($userFolder instanceof File) {
return new DataResponse(["error" => $this->trans->t("You don't have enough permission to create")]);
}
if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) {
$this->logger->error("Create in public folder without access");
return new DataResponse(["error" => $this->trans->t("You do not have enough permissions to view the file")]);
}
}
$folder = $userFolder->get($dir);
6 years ago
if ($folder === null) {
$this->logger->error("Folder for file creation was not found: $dir");
return new DataResponse(["error" => $this->trans->t("The required folder was not found")]);
}
if (!($folder->isCreatable() && $folder->isUpdateable())) {
$this->logger->error("Folder for file creation without permission: $dir");
return new DataResponse(["error" => $this->trans->t("You don't have enough permission to create")]);
}
if (!empty($templateId)) {
$templateFile = TemplateManager::getTemplate($templateId);
if ($templateFile !== null) {
5 years ago
$template = $templateFile->getContent();
}
} elseif (!empty($targetId)) {
$targetFile = $userFolder->getById($targetId)[0];
$targetName = $targetFile->getName();
$targetExt = strtolower(pathinfo((string) $targetName, PATHINFO_EXTENSION));
$targetKey = $this->fileUtility->getKey($targetFile);
4 years ago
$fileUrl = $this->getUrl($targetFile, $user, $shareToken);
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$region = str_replace("_", "-", Server::get(IFactory::class)->get("")->getLocaleCode());
try {
$newFileUri = $this->documentService->getConvertedUri($fileUrl, $targetExt, $ext, $targetKey, $region, $ext === "pdf");
} catch (\Exception $e) {
$this->logger->error("getConvertedUri: " . $targetFile->getId(), ["exception" => $e]);
return new DataResponse(["error" => $e->getMessage()]);
}
$template = $this->documentService->request($newFileUri);
} else {
$template = TemplateManager::getEmptyTemplate($name);
}
if (!$template) {
$this->logger->error("Template for file creation not found: $name ($templateId)");
return new DataResponse(["error" => $this->trans->t("Template not found")]);
}
$name = $folder->getNonExistingName($name);
try {
if (\version_compare(\implode(".", Server::get(\OCP\ServerVersion::class)->getVersion()), "19", "<")) {
$file = $folder->newFile($name);
$file->putContent($template);
} else {
$file = $folder->newFile($name, $template);
}
} catch (NotPermittedException $e) {
$this->logger->error("Can't create file: $name", ["exception" => $e]);
return new DataResponse(["error" => $this->trans->t("Can't create file")]);
}
return new DataResponse(Helper::formatFileInfo($file->getFileInfo()));
}
/**
* Create new file in folder from editor
*
* @param string $name - file name
* @param string $dir - folder path
* @param ?int $templateId - file identifier
*
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function createNew(string $name, string $dir, ?int $templateId = null): TemplateResponse|RedirectResponse {
$this->logger->debug("Create from editor: $name in $dir");
$response = $this->create($name, $dir, $templateId);
$data = $response->getData();
if (isset($data['error'])) {
return $this->renderError(error: $data["error"]);
}
$openEditor = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".editor.index", ["fileId" => $data["id"]]);
return new RedirectResponse($openEditor);
}
/**
* Get users
*
* @param $fileId - file identifier
* @param $operationType - type of operation
*
* @return DataResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function users(
int $fileId,
string $operationType = "",
int $offset = 0,
int $limit = 100,
string $search = ""
): DataResponse {
$this->logger->debug("Search users");
$result = [];
$currentUserGroups = [];
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse();
}
if (!$this->shareManager->allowEnumeration()) {
return new DataResponse();
}
$autocompleteMemberGroup = false;
if ($this->shareManager->limitEnumerationToGroups()) {
$autocompleteMemberGroup = true;
}
$currentUser = $this->userSession->getUser();
$currentUserId = $currentUser->getUID();
$currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
$excludedGroups = $this->getShareExcludedGroups();
$isMemberExcludedGroups = true;
if ((count(array_intersect($currentUserGroups, $excludedGroups)) !== count($currentUserGroups)) || empty($currentUserGroups)) {
$isMemberExcludedGroups = false;
}
[$file, $error, $share] = $this->getFile($currentUserId, $fileId);
if (isset($error)) {
$this->logger->error("Users: $fileId $error");
return new DataResponse();
}
$canShare = (($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE)
&& !$isMemberExcludedGroups;
$shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly();
$all = false;
$users = [];
if ($canShare && $operationType !== "protect") {
// who can be given access
if ($shareMemberGroups || $autocompleteMemberGroup) {
foreach ($currentUserGroups as $currentUserGroup) {
$group = $this->groupManager->get($currentUserGroup);
foreach ($group->getUsers() as $user) {
if ($this->filterUser($user, $currentUserId, $operationType, $search)) {
$users[$user->getUID()] = $user;
}
}
}
} else {
// all users
$all = true;
$allUsers = $this->userManager->searchDisplayName($search);
foreach ($allUsers as $user) {
if ($this->filterUser($user, $currentUserId, $operationType, $search)) {
$users[$user->getUID()] = $user;
}
}
}
}
if (!$all) {
// who has access
$accessList = $this->shareManager->getAccessList($file);
foreach ($accessList["users"] as $accessUser) {
$user = $this->userManager->get($accessUser);
if ($this->filterUser($user, $currentUserId, $operationType, $search)) {
$users[$user->getUID()] = $user;
}
}
$fileInfo = $file->getFileInfo();
if ($fileInfo->getStorage()->instanceOfStorage(GroupFolderStorage::class)) {
if ($this->folderManager !== null) {
$folderId = $this->folderManager->getFolderByPath($fileInfo->getPath());
$folderUsers = $this->folderManager->searchUsers($folderId, "", -1);
foreach ($folderUsers as $folderUser) {
$user = $this->userManager->get($folderUser["uid"]);
if ($this->filterUser($user, $currentUserId, $operationType, $search)) {
$users[$user->getUID()] = $user;
}
}
} else {
$this->logger->error("Group folder manager is not available");
}
}
}
$users = array_slice($users, $offset, $limit);
foreach ($users as $user) {
$userElement = [
"name" => $user->getDisplayName(),
"id" => $operationType === "protect" ? $this->buildUserId($user->getUID()) : $user->getUID(),
"email" => $user->getEMailAddress()
];
$result[] = $userElement;
}
return new DataResponse($result);
}
/**
* List all emails of the user which is currently logged-in
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function emails(): DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$emails = $this->emailManager->getSenderAddressesFor($user->getUID());
return new DataResponse(['emails' => $emails]);
}
/**
* Checking if the user matches the filter
*
* @param IUser $user - user
* @param string $currentUserId - id of current user
* @param string $operationType - type of the get user operation
* @param string $searchString - string for searching
*/
private function filterUser(IUser $user, string $currentUserId, string $operationType, string $searchString): bool {
return $user->getUID() != $currentUserId
&& (!empty($user->getEMailAddress()) || $operationType === "protect")
&& $this->searchInUser($user, $searchString);
}
/**
* Check if the user contains the search string
*
* @param IUser $user - user
* @param string $searchString - string for searching
*/
private function searchInUser(IUser $user, string $searchString): bool {
return empty($searchString)
|| stripos((string) $user->getUID(), (string) $searchString) !== false
|| stripos((string) $user->getDisplayName(), (string) $searchString) !== false
|| !empty($user->getEMailAddress()) && stripos((string) $user->getEMailAddress(), (string) $searchString) !== false;
}
/**
* Get user for Info
*
* @param string $userIds - users identifiers
*
* @return DataResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function userInfo(string $userIds): DataResponse {
$result = [];
$userIds = json_decode($userIds, true);
if ($userIds !== null && is_array($userIds)) {
foreach ($userIds as $userId) {
$userData = [];
$user = $this->userManager->get($this->getUserId($userId));
if (!empty($user)) {
$userData = [
"name" => $user->getDisplayName(),
"id" => $userId
];
$avatar = $this->avatarManager->getAvatar($user->getUID());
if ($avatar->exists() && $avatar->isCustomAvatar()) {
$userAvatarUrl = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute("core.avatar.getAvatar", [
"userId" => $user->getUID(),
"size" => 64,
])
);
$userData["image"] = $userAvatarUrl;
}
$result[] = $userData;
}
}
}
return new DataResponse($result);
}
/**
* Send notify about mention
*
* @param int $fileId - file identifier
* @param string $anchor - the anchor on target content
* @param string $comment - comment
* @param array $emails - emails array to whom to send notify
*
* @return DataResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function mention(int $fileId, string $anchor, string $comment, array $emails): DataResponse {
$this->logger->debug("mention: from $fileId to " . json_encode($emails));
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
if (empty($emails)) {
return new DataResponse(["error" => $this->trans->t("Failed to send notification")]);
}
$recipientIds = [];
foreach ($emails as $email) {
$recipients = $this->userManager->getByEmail($email);
foreach ($recipients as $recipient) {
3 years ago
$recipientId = $recipient->getUID();
if (!in_array($recipientId, $recipientIds, true)) {
$recipientIds[] = $recipientId;
}
}
}
$user = $this->userSession->getUser();
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
$currentUserGroups = $this->groupManager->getUserGroupIds($user);
$excludedGroups = $this->getShareExcludedGroups();
$isMemberExcludedGroups = true;
if ((count(array_intersect($currentUserGroups, $excludedGroups)) !== count($currentUserGroups)) || empty($currentUserGroups)) {
$isMemberExcludedGroups = false;
}
[$file, $error, $share] = $this->getFile($userId, $fileId);
if (isset($error)) {
$this->logger->error("Mention: $fileId $error");
return new DataResponse(["error" => $this->trans->t("Failed to send notification")]);
}
foreach ($emails as $email) {
$substrToDelete = "+" . $email . " ";
$comment = str_replace($substrToDelete, "", $comment);
}
//Length from Nextcloud:
3 years ago
//https://github.com/nextcloud/server/blob/88b03d69cedab6f210178e9dcb04bc512beeb9be/lib/private/Notification/Notification.php#L204
$maxLen = 64;
if (strlen($comment) > $maxLen) {
$ending = "...";
$comment = substr($comment, 0, ($maxLen - strlen($ending))) . $ending;
}
$notificationManager = Server::get(\OCP\Notification\IManager::class);
$notification = $notificationManager->createNotification();
$notification->setApp($this->appName)
->setDateTime(new \DateTime())
->setObject("mention", $comment)
->setSubject("mention_info", [
"notifierId" => $userId,
"fileId" => $file->getId(),
"fileName" => $file->getName(),
"anchor" => $anchor
]);
$shareMemberGroups = $this->shareManager->shareWithGroupMembersOnly();
$canShare = (($file->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE)
&& !$isMemberExcludedGroups;
5 years ago
$accessList = $this->shareManager->getAccessList($file);
foreach ($recipientIds as $recipientId) {
$isAvailable = in_array($recipientId, $accessList["users"], true);
if (!$isAvailable
&& ($file->getFileInfo()->getStorage()->instanceOfStorage(GroupFolderStorage::class)
|| $file->getFileInfo()->getMountPoint() instanceof \OCA\Files_External\Config\ExternalMountPoint)) {
$recipientFolder = $this->root->getUserFolder($recipientId);
$recipientFile = $recipientFolder->getById($file->getId());
$isAvailable = !empty($recipientFile);
}
if (!$isAvailable) {
5 years ago
if (!$canShare) {
continue;
}
if ($shareMemberGroups) {
$recipient = $this->userManager->get($recipientId);
$recipientGroups = $this->groupManager->getUserGroupIds($recipient);
if (empty(array_intersect($currentUserGroups, $recipientGroups))) {
continue;
}
}
5 years ago
$share = $this->shareManager->newShare();
$share->setNode($file)
->setShareType(IShare::TYPE_USER)
->setSharedBy($userId)
->setSharedWith($recipientId)
->setShareOwner($userId)
->setPermissions(Constants::PERMISSION_READ);
$this->shareManager->createShare($share);
5 years ago
$this->logger->debug("mention: share $fileId to $recipientId");
}
$notification->setUser($recipientId);
$notificationManager->notify($notification);
if ($this->appConfig->getEmailNotifications()) {
$this->emailManager->notifyMentionEmail($userId, $recipientId, $file->getId(), $file->getName(), $anchor, $notification->getObjectId());
}
}
return new DataResponse(["message" => $this->trans->t("Notification sent successfully")]);
}
/**
* Reference data
*
* @param array $referenceData - reference data
* @param string $path - file path
* @param string $link - file link
*
* @return DataResponse
*/
#[NoAdminRequired]
#[PublicPage]
public function reference(array $referenceData, ?string $path = null, ?string $link = null): DataResponse {
$this->logger->debug("reference: " . json_encode($referenceData) . " $path");
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$user = $this->userSession->getUser();
if (empty($user)) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$userId = $user->getUID();
$file = null;
$fileId = (int)($referenceData["fileKey"] ?? 0);
if (!empty($fileId)
&& $referenceData["instanceId"] === $this->appConfig->getSystemValue("instanceid", true)) {
[$file, $error, $share] = $this->getFile($userId, $fileId);
}
$userFolder = $this->root->getUserFolder($userId);
if ($file === null
&& $path !== null
&& $userFolder->nodeExists($path)) {
$node = $userFolder->get($path);
if ($node instanceof File
&& $node->isReadable()) {
$file = $node;
}
}
if ($file === null
&& !empty($link)) {
[$fileId, $redirect] = $this->getFileIdByLink($link);
if (!empty($fileId)) {
[$file, $error, $share] = $this->getFile($userId, $fileId);
} elseif ($redirect) {
return new DataResponse(["url" => $link]);
}
}
if ($file === null) {
$this->logger->error("Reference not found: $fileId $path");
return new DataResponse(["error" => $this->trans->t("File not found")]);
}
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
$key = $this->fileUtility->getKey($file);
$key = DocumentService::generateRevisionId($key);
$response = [
"fileType" => $ext,
"path" => $userFolder->getRelativePath($file->getPath()),
"key" => $key,
"referenceData" => [
"fileKey" => (string)$file->getId(),
"instanceId" => $this->appConfig->getSystemValue("instanceid", true),
],
"url" => $this->getUrl($file, $user),
];
if (!empty($this->appConfig->getDocumentServerSecret())) {
$now = time();
$iat = $now;
$exp = $now + $this->appConfig->getJwtExpiration() * 60;
$response["iat"] = $iat;
$response["exp"] = $exp;
$token = \OCA\Onlyoffice\Vendor\Firebase\JWT\JWT::encode($response, $this->appConfig->getDocumentServerSecret(), "HS256");
$response["token"] = $token;
}
return new DataResponse($response);
}
/**
* Conversion file to Office Open XML format
*
* @param integer $fileId - file identifier
* @param string $shareToken - access token
*
* @return DataResponse
*/
#[NoAdminRequired]
#[PublicPage]
public function convert(int $fileId, ?string $shareToken = null): DataResponse {
$this->logger->debug("Convert: $fileId");
9 years ago
if (empty($shareToken) && !$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$user = $this->userSession->getUser();
6 years ago
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
[$file, $error, $share] = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->fileUtility->getFileByToken($fileId, $shareToken);
if (isset($error)) {
$this->logger->error("Convertion: $fileId $error");
return new DataResponse(["error" => $error]);
}
if (!empty($shareToken) && ($share->getPermissions() & Constants::PERMISSION_CREATE) === 0) {
$this->logger->error("Convertion in public folder without access: $fileId");
return new DataResponse(["error" => $this->trans->t("You do not have enough permissions to view the file")]);
}
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
$format = $this->appConfig->formatsSetting()[$ext];
if (!isset($format)) {
$this->logger->info("Format for convertion not supported: $fileName");
return new DataResponse(["error" => $this->trans->t("Format is not supported")]);
}
if (!isset($format["conv"]) || $format["conv"] !== true) {
$this->logger->info("Conversion is not required: $fileName");
return new DataResponse(["error" => $this->trans->t("Conversion is not required")]);
}
$internalExtension = match ($format["type"]) {
"cell" => "xlsx",
"slide" => "pptx",
default => "docx",
};
6 years ago
$newFileUri = null;
$key = $this->fileUtility->getKey($file);
7 years ago
$fileUrl = $this->getUrl($file, $user, $shareToken);
$region = str_replace("_", "-", Server::get(IFactory::class)->get("")->getLocaleCode());
try {
$newFileUri = $this->documentService->getConvertedUri($fileUrl, $ext, $internalExtension, $key, $region);
} catch (\Exception $e) {
$this->logger->error("getConvertedUri: " . $file->getId(), ["exception" => $e]);
return new DataResponse(["error" => $e->getMessage()]);
}
$folder = $file->getParent();
if (!($folder->isCreatable() && $folder->isUpdateable())) {
$folder = $this->root->getUserFolder($userId);
}
try {
$newData = $this->documentService->request($newFileUri);
} catch (\Exception $e) {
$this->logger->error("Failed to download converted file", ["exception" => $e]);
return new DataResponse(["error" => $this->trans->t("Failed to download converted file")]);
}
$fileNameWithoutExt = substr((string) $fileName, 0, strlen((string) $fileName) - strlen($ext) - 1);
$newFileName = $folder->getNonExistingName($fileNameWithoutExt . "." . $internalExtension);
try {
$file = $folder->newFile($newFileName);
$file->putContent($newData);
} catch (NotPermittedException $e) {
$this->logger->error("Can't create file: $newFileName", ["exception" => $e]);
return new DataResponse(["error" => $this->trans->t("Can't create file")]);
}
return new DataResponse(Helper::formatFileInfo($file->getFileInfo()));
}
/**
* Save file to folder
*
* @param string $name - file name
* @param string $dir - folder path
* @param string $url - file url
*
* @return DataResponse
*/
#[NoAdminRequired]
public function save(string $name, string $dir, string $url): DataResponse {
$this->logger->debug("Save: $name");
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$userId = $this->userSession->getUser()->getUID();
$userFolder = $this->root->getUserFolder($userId);
try {
/**
* @var \OC\Files\Node\Folder
*/
$folder = $userFolder->get($dir);
4 months ago
} catch (\OCP\Files\NotFoundException $e) {
$this->logger->error("Folder for saving file was not found: $dir", ['exception' => $e]);
return new DataResponse(["error" => $this->trans->t("The required folder was not found")]);
}
if (!($folder->isCreatable() && $folder->isUpdateable())) {
$this->logger->error("Folder for saving file without permission: $dir");
return new DataResponse(["error" => $this->trans->t("You don't have enough permission to create")]);
}
$documentServerUrl = $this->appConfig->getDocumentServerUrl();
if (empty($documentServerUrl)) {
$this->logger->error("documentServerUrl is empty");
return new DataResponse(["error" => $this->trans->t("ONLYOFFICE app is not configured. Please contact admin")]);
}
if (str_starts_with($documentServerUrl, "/")) {
$documentServerUrl = $this->urlGenerator->getAbsoluteURL($documentServerUrl);
}
if (parse_url($url, PHP_URL_HOST) !== parse_url((string) $documentServerUrl, PHP_URL_HOST)) {
$this->logger->error("Incorrect domain in file url");
return new DataResponse(["error" => $this->trans->t("The domain in the file url does not match the domain of the Document server")]);
}
$url = $this->appConfig->replaceDocumentServerUrlToInternal($url);
try {
$newData = $this->documentService->request($url);
} catch (\Exception $e) {
$this->logger->error("Failed to download file for saving: $url", ["exception" => $e]);
return new DataResponse(["error" => $this->trans->t("Download failed")]);
}
$name = $folder->getNonExistingName($name);
try {
$file = $folder->newFile($name);
$file->putContent($newData);
} catch (NotPermittedException $e) {
$this->logger->error("Can't save file: $name", ["exception" => $e]);
return new DataResponse(["error" => $this->trans->t("Can't create file")]);
}
return new DataResponse(Helper::formatFileInfo($file->getFileInfo()));
}
/**
* Get versions history for file
*
* @param integer $fileId - file identifier
*
* @return DataResponse
*/
#[NoAdminRequired]
public function history(int $fileId): DataResponse {
$this->logger->debug("Request history for: $fileId");
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$history = [];
$user = $this->userSession->getUser();
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
[$file, $error, $share] = $this->getFile($userId, $fileId);
if (isset($error)) {
$this->logger->error("History: $fileId $error");
return new DataResponse(["error" => $error]);
}
if ($fileId === 0) {
$fileId = $file->getId();
}
$ownerId = null;
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$ownerId = $owner->getUID();
}
$versions = [];
if ($this->versionManager !== null
&& $owner !== null) {
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
}
$prevVersion = "";
$versionNum = 0;
foreach ($versions as $version) {
$versionNum += 1;
$key = $this->fileUtility->getVersionKey($version);
$key = DocumentService::generateRevisionId($key);
$historyItem = [
"created" => $version->getTimestamp(),
"key" => $key,
"version" => $versionNum
];
$versionId = $version->getRevisionId();
$author = FileVersions::getAuthor($ownerId, $file->getFileInfo(), $versionId);
if ($author !== null) {
$historyItem["user"] = [
"id" => $this->buildUserId($author["id"]),
"name" => $author["name"],
];
} elseif (!empty($this->appConfig->getUnknownAuthor()) && $versionNum !== 1) {
$authorName = $this->appConfig->getUnknownAuthor();
$historyItem["user"] = [
"name" => $authorName,
];
} elseif ($owner !== null) {
$authorName = $owner->getDisplayName();
$authorId = $owner->getUID();
$historyItem["user"] = [
"id" => $this->buildUserId($authorId),
"name" => $authorName,
];
}
$historyData = FileVersions::getHistoryData($ownerId, $file->getFileInfo(), $versionId, $prevVersion);
if ($historyData !== null) {
$historyItem["changes"] = $historyData["changes"];
$historyItem["serverVersion"] = $historyData["serverVersion"];
}
$prevVersion = $versionId;
$history[] = $historyItem;
}
$key = $this->fileUtility->getKey($file, true);
$key = DocumentService::generateRevisionId($key);
$historyItem = [
"created" => $file->getMTime(),
"key" => $key,
"version" => $versionNum + 1
];
$versionId = $file->getFileInfo()->getMtime();
$author = FileVersions::getAuthor($ownerId, $file->getFileInfo(), $versionId);
if ($author !== null) {
$historyItem["user"] = [
"id" => $this->buildUserId($author["id"]),
"name" => $author["name"],
];
} elseif (!empty($this->appConfig->getUnknownAuthor()) && $versionNum !== 0) {
$authorName = $this->appConfig->getUnknownAuthor();
$historyItem["user"] = [
"name" => $authorName,
];
} elseif ($owner !== null) {
$authorName = $owner->getDisplayName();
$authorId = $owner->getUID();
$historyItem["user"] = [
"id" => $this->buildUserId($authorId),
"name" => $authorName,
];
}
$historyData = FileVersions::getHistoryData($ownerId, $file->getFileInfo(), $versionId, $prevVersion);
if ($historyData !== null) {
$historyItem["changes"] = $historyData["changes"];
$historyItem["serverVersion"] = $historyData["serverVersion"];
}
$history[] = $historyItem;
return new DataResponse($history);
}
/**
* Get file attributes of specific version
*
* @param integer $fileId - file identifier
* @param integer $version - file version
*
* @return DataResponse
*/
#[NoAdminRequired]
public function version(int $fileId, int $version): DataResponse {
$this->logger->debug("Request version for: $fileId ($version)");
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$user = $this->userSession->getUser();
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
[$file, $error, $share] = $this->getFile($userId, $fileId);
if (isset($error)) {
$this->logger->error("History: $fileId $error");
return new DataResponse(["error" => $error]);
}
if ($fileId === 0) {
$fileId = $file->getId();
}
$owner = null;
$ownerId = null;
$versions = [];
if ($this->versionManager !== null) {
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$ownerId = $owner->getUID();
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
}
}
$key = null;
$fileUrl = null;
$versionId = null;
if ($version > count($versions)) {
$key = $this->fileUtility->getKey($file, true);
$versionId = $file->getFileInfo()->getMtime();
$fileUrl = $this->getUrl($file, $user);
} else {
$fileVersion = array_values($versions)[$version - 1];
$key = $this->fileUtility->getVersionKey($fileVersion);
$versionId = $fileVersion->getRevisionId();
$fileUrl = $this->getUrl($file, $user, null, $version);
}
$key = DocumentService::generateRevisionId($key);
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
6 years ago
$result = [
"fileType" => $ext,
"url" => $fileUrl,
"version" => $version,
"key" => $key
6 years ago
];
if ($version > 1
&& count($versions) >= $version - 1
&& FileVersions::hasChanges($ownerId, $file->getFileInfo(), $versionId)) {
$changesUrl = $this->getUrl($file, $user, null, $version, true);
$result["changesUrl"] = $changesUrl;
$prevVersion = array_values($versions)[$version - 2];
$prevVersionKey = $this->fileUtility->getVersionKey($prevVersion);
$prevVersionKey = DocumentService::generateRevisionId($prevVersionKey);
$prevVersionUrl = $this->getUrl($file, $user, null, $version - 1);
$result["previous"] = [
"fileType" => $ext,
"key" => $prevVersionKey,
"url" => $prevVersionUrl
];
}
if (!empty($this->appConfig->getDocumentServerSecret())) {
$now = time();
$iat = $now;
$exp = $now + $this->appConfig->getJwtExpiration() * 60;
$result["iat"] = $iat;
$result["exp"] = $exp;
$token = \OCA\Onlyoffice\Vendor\Firebase\JWT\JWT::encode($result, $this->appConfig->getDocumentServerSecret(), "HS256");
6 years ago
$result["token"] = $token;
}
return new DataResponse($result);
}
5 years ago
/**
* Restore file version
*
* @param integer $fileId - file identifier
* @param integer $version - file version
*
* @return DataResponse
5 years ago
*/
#[NoAdminRequired]
public function restore(int $fileId, int $version): DataResponse {
$this->logger->debug("Request restore version for: $fileId ($version)");
5 years ago
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
5 years ago
}
$user = $this->userSession->getUser();
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
[$file, $error, $share] = $this->getFile($userId, $fileId);
5 years ago
if (isset($error)) {
$this->logger->error("Restore: $fileId $error");
return new DataResponse(["error" => $error]);
5 years ago
}
if ($fileId === 0) {
$fileId = $file->getId();
}
$owner = null;
$versions = [];
5 years ago
if ($this->versionManager !== null) {
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
5 years ago
}
if (count($versions) >= $version) {
$fileVersion = array_values($versions)[$version - 1];
$this->versionManager->rollback($fileVersion);
if ($fileVersion->getSourceFile()->getFileInfo()->getStorage()->instanceOfStorage(GroupFolderStorage::class)) {
$this->keyManager->delete($fileVersion->getSourceFile()->getId());
}
5 years ago
}
}
return $this->history($fileId);
5 years ago
}
7 years ago
/**
* Get presigned url to file
*
* @param string $filePath - file path
*
* @return DataResponse
7 years ago
*/
#[NoAdminRequired]
public function url(string $filePath): DataResponse {
$this->logger->debug("Request url for: $filePath");
7 years ago
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
7 years ago
}
7 years ago
$user = $this->userSession->getUser();
$userId = $user->getUID();
7 years ago
$userFolder = $this->root->getUserFolder($userId);
try {
$file = $userFolder->get($filePath);
} catch (\OCP\Files\NotFoundException) {
$this->logger->error("File for generate presigned url was not found: $filePath");
return new DataResponse(["error" => $this->trans->t("File not found")]);
7 years ago
}
$canDownload = true;
/**
* @var \OCP\Files\Storage\IStorage|\OCA\Files_Sharing\SharedStorage
*/
$fileStorage = $file->getStorage();
if ($fileStorage->instanceOfStorage(SharedStorage::class)) {
$share = $fileStorage->getShare();
$canDownload = FileUtility::canShareDownload($share);
}
if (!$file->isReadable() || !$canDownload) {
$this->logger->error("File without permission: $filePath");
return new DataResponse(["error" => $this->trans->t("You do not have enough permissions to view the file")]);
7 years ago
}
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
7 years ago
$fileUrl = $this->getUrl($file, $user);
7 years ago
$result = [
"fileType" => $ext,
"url" => $fileUrl
];
if (!empty($this->appConfig->getDocumentServerSecret())) {
$now = time();
$iat = $now;
$exp = $now + $this->appConfig->getJwtExpiration() * 60;
$result["iat"] = $iat;
$result["exp"] = $exp;
$token = \OCA\Onlyoffice\Vendor\Firebase\JWT\JWT::encode($result, $this->appConfig->getDocumentServerSecret(), "HS256");
7 years ago
$result["token"] = $token;
}
return new DataResponse($result);
7 years ago
}
5 years ago
/**
* Download method
*
* @param int $fileId - file identifier
* @param string $toExtension - file extension to download
* @param bool $template - file extension to download
5 years ago
*
* @return DataDownloadResponse|TemplateResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function download(int $fileId, ?string $toExtension = null, bool $template = false): DataDownloadResponse|TemplateResponse {
$this->logger->debug("Download: $fileId $toExtension");
5 years ago
if (!$this->appConfig->isUserAllowedToUse() || $this->appConfig->getDisableDownload()) {
return $this->renderError($this->trans->t("Not permitted"));
}
if ($template) {
$templateFile = TemplateManager::getTemplate($fileId);
5 years ago
if (empty($templateFile)) {
$this->logger->info("Download: template not found: $fileId");
return $this->renderError($this->trans->t("File not found"));
}
5 years ago
5 years ago
$file = $templateFile;
} else {
$user = $this->userSession->getUser();
$userId = null;
if (!empty($user)) {
$userId = $user->getUID();
}
5 years ago
[$file, $error, $share] = $this->getFile($userId, $fileId);
5 years ago
if (isset($error)) {
$this->logger->error("Download: $fileId $error");
return $this->renderError($error);
}
5 years ago
}
$fileStorage = $file->getStorage();
if ($fileStorage->instanceOfStorage(SharedStorage::class)) {
$share = empty($share) ? $fileStorage->getShare() : $share;
if (!FileUtility::canShareDownload($share)) {
return $this->renderError($this->trans->t("Not permitted"));
}
}
5 years ago
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
$toExtension = strtolower((string) $toExtension);
if ($toExtension === ""
|| $ext === $toExtension
|| $template) {
return new DataDownloadResponse($file->getContent(), $fileName, $file->getMimeType());
}
5 years ago
$newFileUri = null;
$newFileType = $toExtension;
5 years ago
$key = $this->fileUtility->getKey($file);
$fileUrl = $this->getUrl($file, $user);
$thumbnail = ['first' => false];
5 years ago
try {
$response = $this->documentService->sendRequestToConvertService(
$fileUrl,
$ext,
$toExtension,
$key,
false,
"",
false,
$thumbnail,
);
$error = $response["error"] ?? null;
$endConvert = $response["endConvert"] ?? false;
if ($error !== null) {
$this->documentService->processConvServResponceError((int)$error);
}
if ($endConvert) {
$newFileUri = $response["fileUrl"] ?? "";
$newFileType = $response["fileType"] ?? "";
}
5 years ago
} catch (\Exception $e) {
$this->logger->error("sendRequestToConvertService: " . $file->getId(), ["exception" => $e]);
5 years ago
return $this->renderError($e->getMessage());
}
try {
$newData = $this->documentService->request($newFileUri);
5 years ago
} catch (\Exception $e) {
$this->logger->error("Failed to download converted file", ["exception" => $e]);
5 years ago
return $this->renderError($this->trans->t("Failed to download converted file"));
}
$fileNameWithoutExt = substr((string) $fileName, 0, strlen((string) $fileName) - strlen($ext) - 1);
$newFileName = "$fileNameWithoutExt.$newFileType";
5 years ago
$mimeType = $this->appConfig->getMimeType($newFileType);
5 years ago
return new DataDownloadResponse($newData, $newFileName, $mimeType);
5 years ago
}
/**
* Print editor section
*
* @param integer $fileId - file identifier
* @param string $filePath - file path
7 years ago
* @param string $shareToken - access token
* @param bool $inframe - open in frame
* @param bool $inviewer - open in viewer
5 years ago
* @param bool $template - file is template
* @param string $anchor - anchor for file content
*
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function index(
?int $fileId,
?string $filePath = null,
?string $shareToken = null,
bool $inframe = false,
bool $inviewer = false,
bool $template = false,
?string $anchor = null
): TemplateResponse|RedirectResponse {
$this->logger->debug("Open: $fileId $filePath ");
9 years ago
$isLoggedIn = $this->userSession->isLoggedIn();
if (empty($shareToken) && !$isLoggedIn) {
$redirectUrl = $this->urlGenerator->linkToRoute("core.login.showLoginForm", [
"redirect_url" => $this->request->getRequestUri()
]);
return new RedirectResponse($redirectUrl);
}
$shareBy = null;
if (!empty($shareToken) && !$isLoggedIn) {
[$share, $error] = $this->fileUtility->getShare($shareToken);
if (!empty($share)) {
$shareBy = $share->getSharedBy();
}
}
if (!$this->appConfig->isUserAllowedToUse($shareBy)) {
return $this->renderError($this->trans->t("Not permitted"));
}
$documentServerUrl = $this->appConfig->getDocumentServerUrl();
if (empty($documentServerUrl)) {
$this->logger->error("documentServerUrl is empty");
return $this->renderError($this->trans->t("ONLYOFFICE app is not configured. Please contact admin"));
}
$params = [
8 years ago
"fileId" => $fileId,
"filePath" => $filePath,
7 years ago
"shareToken" => $shareToken,
"directToken" => null,
"isTemplate" => $template,
"inframe" => false,
"inviewer" => $inviewer === true,
"anchor" => $anchor
];
$response = null;
7 years ago
if ($inframe === true) {
$params["inframe"] = true;
$response = new TemplateResponse($this->appName, "editor", $params, "base");
} elseif ($isLoggedIn) {
$response = new TemplateResponse($this->appName, "editor", $params);
} else {
$response = new PublicTemplateResponse($this->appName, "editor", $params);
[$file, $error, $share] = $this->fileUtility->getFileByToken($fileId, $shareToken);
if (!isset($error)) {
$response->setHeaderTitle($file->getName());
}
}
\OCP\Util::addHeader("meta", ["name" => "apple-touch-fullscreen", "content" => "yes"]);
$csp = new ContentSecurityPolicy();
9 years ago
if (preg_match("/^https?:\/\//i", $documentServerUrl)) {
$csp->addAllowedScriptDomain($documentServerUrl);
$csp->addAllowedFrameDomain($documentServerUrl);
} else {
$csp->addAllowedFrameDomain("'self'");
}
$response->setContentSecurityPolicy($csp);
return $response;
}
8 years ago
/**
* Print public editor section
*
8 years ago
* @param integer $fileId - file identifier
* @param string $shareToken - access token
* @param bool $inframe - open in frame
8 years ago
*
* @return TemplateResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function publicPage(
?int $fileId,
string $shareToken,
bool $inframe = false
): TemplateResponse {
return $this->index($fileId, null, $shareToken, $inframe);
8 years ago
}
/**
* Getting file by identifier
*
8 years ago
* @param string $userId - user identifier
* @param integer $fileId - file identifier
* @param string $filePath - file path
5 years ago
* @param bool $template - file is template
*/
private function getFile(?string $userId, $fileId, $filePath = null, $template = false): array {
if (empty($userId)) {
return [null, $this->trans->t("UserId is empty"), null];
}
if (empty($fileId)) {
6 years ago
return [null, $this->trans->t("FileId is empty"), null];
}
try {
$folder = $template ? TemplateManager::getGlobalTemplateDir() : $this->root->getUserFolder($userId);
5 years ago
$files = $folder->getById($fileId);
} catch (\Exception $e) {
$this->logger->error("getFile: $fileId", ["exception" => $e]);
6 years ago
return [null, $this->trans->t("Invalid request"), null];
}
if (empty($files)) {
$this->logger->info("Files not found: $fileId");
6 years ago
return [null, $this->trans->t("File not found"), null];
}
$file = $files[0];
if (count($files) > 1 && !empty($filePath)) {
$filePath = "/" . $userId . "/files" . $filePath;
foreach ($files as $curFile) {
if ($curFile->getPath() === $filePath) {
$file = $curFile;
break;
}
}
}
if (!$file->isReadable()) {
6 years ago
return [null, $this->trans->t("You do not have enough permissions to view the file"), null];
}
6 years ago
return [$file, null, null];
}
/**
* Generate secure link to download document
*
* @param File $file - file
7 years ago
* @param IUser $user - user with access
* @param string $shareToken - access token
* @param integer $version - file version
* @param bool $changes - is required url to file changes
5 years ago
* @param bool $template - file is template
*
* @return string
*/
private function getUrl(
File $file,
?IUser $user = null,
?string $shareToken = null,
int $version = 0,
bool $changes = false,
bool $template = false
): string {
$data = [
"action" => "download",
"fileId" => $file->getId()
];
6 years ago
$userId = null;
8 years ago
if (!empty($user)) {
$userId = $user->getUID();
$data["userId"] = $userId;
}
if (!empty($shareToken)) {
$data["shareToken"] = $shareToken;
}
if ($version > 0) {
$data["version"] = $version;
}
if ($changes) {
$data["changes"] = true;
8 years ago
}
5 years ago
if ($template) {
$data["template"] = true;
}
$hashUrl = $this->crypt->getHash($data);
$fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]);
if (!$this->appConfig->useDemo() && !empty($this->appConfig->getStorageUrl())) {
$fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->appConfig->getStorageUrl(), $fileUrl);
}
return $fileUrl;
}
/**
* Return excluded groups list for share
*/
private function getShareExcludedGroups(): array {
$excludedGroups = [];
if (Server::get(\OCP\IAppConfig::class)->getValueString("core", "shareapi_exclude_groups", "no") === "yes") {
$excludedGroups = json_decode((string) Server::get(\OCP\IAppConfig::class)->getValueString("core", "shareapi_exclude_groups_list", ""), true);
}
return $excludedGroups;
}
/**
* Generate unique user identifier
*
* @param string $userId - current user identifier
*/
private function buildUserId(string $userId): string {
$instanceId = $this->appConfig->getSystemValue("instanceid", true);
return $instanceId . "_" . $userId;
}
/**
* Get Nextcloud userId from unique user identifier
*
* @param string $userId - current user identifier
*/
private function getUserId(string $userId): string {
$instanceId = $this->appConfig->getSystemValue("instanceid", true);
$prefix = $instanceId . "_";
if (str_starts_with($userId, $prefix)) {
return substr($userId, strlen($prefix));
}
return $userId;
}
/**
* Get File id from by link
*/
private function getFileIdByLink(string $link): array {
$path = parse_url($link, PHP_URL_PATH);
$encodedPath = array_map(urlencode(...), explode("/", $path));
$parsedLink = str_replace($path, implode("/", $encodedPath), $link);
if (filter_var($parsedLink, FILTER_VALIDATE_URL) === false) {
return [null, true];
}
$storageUrl = $this->urlGenerator->getAbsoluteURL("/");
if (parse_url($parsedLink, PHP_URL_HOST) !== parse_url((string) $storageUrl, PHP_URL_HOST)) {
return [null, true];
}
if (preg_match('/\/(files|f|onlyoffice)\/(\d+)/', $parsedLink, $matches)) {
return [$matches[2], false];
}
return [null, false];
}
/**
* Print error page
*
* @param string $error - error message
* @param string $hint - error hint
*
* @return TemplateResponse
*/
private function renderError(string $error, string $hint = ""): TemplateResponse {
return new TemplateResponse("", "error", [
"errors" => [
[
"error" => $error,
"hint" => $hint
]
]
], "error");
}
7 years ago
}