<?php
/*
* Copyright (C) Ascensio System SIA, 2009-2026
*
* 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.
*
* 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
*
* 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.
*
* 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.
*
* No trademark rights are granted under this License.
*
* 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
*
* 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.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Onlyoffice\Controller;
use OCA\Files\Helper;
use OCA\Files_Sharing\SharedStorage;
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Mount\GroupFolderStorage;
use OCA\Onlyoffice\AppConfig;
use OCA\Onlyoffice\Crypt;
use OCA\Onlyoffice\DocumentService;
use OCA\Onlyoffice\EmailManager;
use OCA\Onlyoffice\FileUtility;
use OCA\Onlyoffice\FileVersions;
use OCA\Onlyoffice\KeyManager;
use OCA\Onlyoffice\TemplateManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
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;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Constants;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotPermittedException;
use OCP\IAvatarManager;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
/**
* Controller with the main functions
*/
class EditorController extends Controller {
public function __construct(
string $appName,
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
) {
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);
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) {
$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);
$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) {
$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:
//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;
$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) {
if (!$canShare) {
continue;
}
if ($shareMemberGroups) {
$recipient = $this->userManager->get($recipientId);
$recipientGroups = $this->groupManager->getUserGroupIds($recipient);
if (empty(array_intersect($currentUserGroups, $recipientGroups))) {
continue;
}
}
$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);
$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");
if (empty($shareToken) & & !$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] = 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",
};
$newFileUri = null;
$key = $this->fileUtility->getKey($file);
$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);
} 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));
$result = [
"fileType" => $ext,
"url" => $fileUrl,
"version" => $version,
"key" => $key
];
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");
$result["token"] = $token;
}
return new DataResponse($result);
}
/**
* Restore file version
*
* @param integer $fileId - file identifier
* @param integer $version - file version
*
* @return DataResponse
*/
#[NoAdminRequired]
public function restore(int $fileId, int $version): DataResponse {
$this->logger->debug("Request restore 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("Restore: $fileId $error");
return new DataResponse(["error" => $error]);
}
if ($fileId === 0) {
$fileId = $file->getId();
}
$owner = null;
$versions = [];
if ($this->versionManager !== null) {
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
}
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());
}
}
}
return $this->history($fileId);
}
/**
* Get presigned url to file
*
* @param string $filePath - file path
*
* @return DataResponse
*/
#[NoAdminRequired]
public function url(string $filePath): DataResponse {
$this->logger->debug("Request url for: $filePath");
if (!$this->appConfig->isUserAllowedToUse()) {
return new DataResponse(["error" => $this->trans->t("Not permitted")]);
}
$user = $this->userSession->getUser();
$userId = $user->getUID();
$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")]);
}
$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")]);
}
$fileName = $file->getName();
$ext = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
$fileUrl = $this->getUrl($file, $user);
$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");
$result["token"] = $token;
}
return new DataResponse($result);
}
/**
* Download method
*
* @param int $fileId - file identifier
* @param string $toExtension - file extension to download
* @param bool $template - file extension to download
*
* @return DataDownloadResponse|TemplateResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function download(int $fileId, ?string $toExtension = null, bool $template = false): DataDownloadResponse|TemplateResponse {
$this->logger->debug("Download: $fileId $toExtension");
if (!$this->appConfig->isUserAllowedToUse() || $this->appConfig->getDisableDownload()) {
return $this->renderError($this->trans->t("Not permitted"));
}
if ($template) {
$templateFile = TemplateManager::getTemplate($fileId);
if (empty($templateFile)) {
$this->logger->info("Download: template not found: $fileId");
return $this->renderError($this->trans->t("File not found"));
}
$file = $templateFile;
} else {
$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("Download: $fileId $error");
return $this->renderError($error);
}
}
$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"));
}
}
$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());
}
$newFileUri = null;
$newFileType = $toExtension;
$key = $this->fileUtility->getKey($file);
$fileUrl = $this->getUrl($file, $user);
$thumbnail = ['first' => false];
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"] ?? "";
}
} catch (\Exception $e) {
$this->logger->error("sendRequestToConvertService: " . $file->getId(), ["exception" => $e]);
return $this->renderError($e->getMessage());
}
try {
$newData = $this->documentService->request($newFileUri);
} catch (\Exception $e) {
$this->logger->error("Failed to download converted file", ["exception" => $e]);
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";
$mimeType = $this->appConfig->getMimeType($newFileType);
return new DataDownloadResponse($newData, $newFileName, $mimeType);
}
/**
* Print editor section
*
* @param integer $fileId - file identifier
* @param string $filePath - file path
* @param string $shareToken - access token
* @param bool $inframe - open in frame
* @param bool $inviewer - open in viewer
* @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 ");
$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 = [
"fileId" => $fileId,
"filePath" => $filePath,
"shareToken" => $shareToken,
"directToken" => null,
"isTemplate" => $template,
"inframe" => false,
"inviewer" => $inviewer === true,
"anchor" => $anchor
];
$response = null;
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();
if (preg_match("/^https?:\/\//i", $documentServerUrl)) {
$csp->addAllowedScriptDomain($documentServerUrl);
$csp->addAllowedFrameDomain($documentServerUrl);
} else {
$csp->addAllowedFrameDomain("'self'");
}
$response->setContentSecurityPolicy($csp);
return $response;
}
/**
* Print public editor section
*
* @param integer $fileId - file identifier
* @param string $shareToken - access token
* @param bool $inframe - open in frame
*
* @return TemplateResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function publicPage(
?int $fileId,
string $shareToken,
bool $inframe = false
): TemplateResponse {
return $this->index($fileId, null, $shareToken, $inframe);
}
/**
* Getting file by identifier
*
* @param string $userId - user identifier
* @param integer $fileId - file identifier
* @param string $filePath - file path
* @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)) {
return [null, $this->trans->t("FileId is empty"), null];
}
try {
$folder = $template ? TemplateManager::getGlobalTemplateDir() : $this->root->getUserFolder($userId);
$files = $folder->getById($fileId);
} catch (\Exception $e) {
$this->logger->error("getFile: $fileId", ["exception" => $e]);
return [null, $this->trans->t("Invalid request"), null];
}
if (empty($files)) {
$this->logger->info("Files not found: $fileId");
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()) {
return [null, $this->trans->t("You do not have enough permissions to view the file"), null];
}
return [$file, null, null];
}
/**
* Generate secure link to download document
*
* @param File $file - file
* @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
* @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()
];
$userId = null;
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;
}
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");
}
}