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/CallbackController.php

903 lines
36 KiB

<?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 OC\Files\SetupManager;
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\Onlyoffice\AppConfig;
use OCA\Onlyoffice\Crypt;
use OCA\Onlyoffice\DocumentService;
use OCA\Onlyoffice\Events\DocumentUnsavedEvent;
use OCA\Onlyoffice\Events\MailMergeEndedEvent;
use OCA\Onlyoffice\FileVersions;
use OCA\Onlyoffice\FileUtility;
use OCA\Onlyoffice\KeyManager;
use OCA\Onlyoffice\MailMergeMessage;
use OCA\Onlyoffice\MailMergeAttachment;
use OCA\Onlyoffice\MailMergeService;
use OCA\Onlyoffice\RemoteInstance;
use OCA\Onlyoffice\TemplateManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Lock\ILock;
use OCP\Files\Lock\ILockManager;
use OCP\Files\Lock\LockContext;
use OCP\Files\Lock\NoLockProviderException;
use OCP\Files\Lock\OwnerLockedException;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;
/**
* Callback handler for the document server.
* Download the file without authentication.
* Save the file without authentication.
*/
class CallbackController extends Controller {
/**
* Status of the document
*/
private const TRACKERSTATUS_EDITING = 1;
private const TRACKERSTATUS_MUSTSAVE = 2;
private const TRACKERSTATUS_CORRUPTED = 3;
private const TRACKERSTATUS_CLOSED = 4;
private const TRACKERSTATUS_MAILMERGE = 5;
private const TRACKERSTATUS_FORCESAVE = 6;
private const TRACKERSTATUS_CORRUPTEDFORCESAVE = 7;
private const TYPE_HTML = 0;
private const TYPE_ATTACH_DOCX = 1;
public function __construct(
string $appName,
IRequest $request,
private readonly IRootFolder $root,
private readonly IUserSession $userSession,
private readonly IUserManager $userManager,
private readonly IL10N $trans,
private readonly LoggerInterface $logger,
private readonly AppConfig $appConfig,
private readonly Crypt $crypt,
private readonly IManager $shareManager,
private readonly ILockManager $lockManager,
private readonly IEventDispatcher $eventDispatcher,
private readonly ?IVersionManager $versionManager,
private readonly DocumentService $documentService,
private readonly KeyManager $keyManager,
private readonly SetupManager $setupManager,
private readonly MailMergeService $mailMergeService
) {
parent::__construct($appName, $request);
}
/**
* Downloading file by the document service
*
* @param string $doc - verification token with the file identifier
*
* @return StreamResponse|JSONResponse
*/
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function download(string $doc): StreamResponse|JSONResponse {
[$hashData, $error] = $this->crypt->readHash($doc);
if ($hashData === null) {
$this->logger->error("Download with empty or not correct hash: $error");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
if ($hashData->action !== "download") {
$this->logger->error("Download with other action");
return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST);
}
$fileId = $hashData->fileId;
$version = $hashData->version ?? 0;
$changes = $hashData->changes ?? false;
$template = $hashData->template ?? false;
$filePath = $hashData->filePath ?? "";
$this->logger->debug("Download: $fileId ($version)" . ($changes ? " changes" : ""));
if (!empty($this->appConfig->getDocumentServerSecret())) {
$header = $this->request->getHeader($this->appConfig->jwtHeader());
if (empty($header)) {
$this->logger->error("Download without jwt");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
$header = substr((string) $header, strlen("Bearer "));
try {
$decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->appConfig->getDocumentServerSecret(), "HS256"));
} catch (\UnexpectedValueException $e) {
$this->logger->error("Download with invalid jwt", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
}
$userId = null;
$user = null;
if ($this->userSession->isLoggedIn()) {
$this->logger->debug("Download: by $userId instead of " . $hashData->userId);
}
$this->setupManager->tearDown();
if (isset($hashData->userId)) {
$userId = $hashData->userId;
$user = $this->userManager->get($userId);
if (!empty($user)) {
\OC_User::setUserId($userId);
$this->setupManager->setupForUser($user);
}
}
$shareToken = $hashData->shareToken ?? null;
[$file, $error, $share] = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath, $changes ? 0 : $version, $template) : $this->getFileByToken($fileId, $shareToken, $changes ? 0 : $version);
if (isset($error)) {
return $error;
}
$canDownload = true;
$fileStorage = $file->getStorage();
if ($fileStorage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class) || !empty($shareToken)) {
$share = empty($share) ? $fileStorage->getShare() : $share;
$canDownload = FileUtility::canShareDownload($share);
if (!$canDownload && !empty($this->appConfig->getDocumentServerSecret())) {
$canDownload = true;
}
}
if ((!empty($user) && !$file->isReadable()) || !$canDownload) {
if ($this->userSession->getUser()?->getUID() != $userId) {
$this->logger->error("Download error: expected $userId instead of " . $this->userSession->getUser()?->getUID());
}
$this->logger->error("Download without access right");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
if (empty($user)) {
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$this->setupManager->setupForUser($owner);
}
}
if ($changes) {
if (!$this->versionManager instanceof IVersionManager) {
$this->logger->error("Download changes: versionManager is null");
return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST);
}
$owner = $file->getFileInfo()->getOwner();
if ($owner === null) {
$this->logger->error("Download: changes owner of $fileId was not found");
return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND);
}
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
$versionId = null;
if ($version > count($versions)) {
$versionId = $file->getFileInfo()->getMtime();
} else {
$fileVersion = array_values($versions)[$version - 1];
$versionId = $fileVersion->getRevisionId();
}
$changesFile = FileVersions::getChangesFile($owner->getUID(), $file->getFileInfo(), $versionId);
if (!$changesFile instanceof \OC\Files\Node\File) {
$this->logger->error("Download: changes $fileId ($version) was not found");
return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND);
}
$file = $changesFile;
}
try {
$handle = $file->fopen('rb');
if ($handle !== false && $handle !== null) {
$response = new StreamResponse($handle);
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode((string) $file->getName()) . '"');
$response->addHeader('Content-Type', $file->getMimeType());
return $response;
}
} catch (NotPermittedException $e) {
$this->logger->error("Download Not permitted: $fileId ($version)", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN);
}
return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR);
}
/**
* Downloading empty file by the document service
*
* @param string $doc - verification token with the file identifier
*
* @return DataDownloadResponse|JSONResponse
*/
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function emptyfile(string $doc): DataDownloadResponse|JSONResponse {
$this->logger->debug("Download empty");
[$hashData, $error] = $this->crypt->readHash($doc);
if ($hashData === null) {
$this->logger->error("Download empty with empty or not correct hash: $error");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
if ($hashData->action !== "empty") {
$this->logger->error("Download empty with other action");
return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST);
}
if (!empty($this->appConfig->getDocumentServerSecret())) {
$header = $this->request->getHeader($this->appConfig->jwtHeader());
if (empty($header)) {
$this->logger->error("Download empty without jwt");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
$header = substr((string) $header, strlen("Bearer "));
try {
$decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->appConfig->getDocumentServerSecret(), "HS256"));
} catch (\UnexpectedValueException $e) {
$this->logger->error("Download empty with invalid jwt", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
}
$templatePath = TemplateManager::getEmptyTemplatePath("default", ".docx");
$template = file_get_contents($templatePath);
if (!$template) {
$this->logger->info("Template for download empty not found: $templatePath");
return new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND);
}
try {
return new DataDownloadResponse($template, "new.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
} catch (NotPermittedException $e) {
$this->logger->error("Download Not permitted", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Not permitted")], Http::STATUS_FORBIDDEN);
}
return new JSONResponse(["message" => $this->trans->t("Download failed")], Http::STATUS_INTERNAL_SERVER_ERROR);
}
/**
* Handle request from the document server with the document status information
*
* @param string $doc verification token with the file identifier
* @param string $key the edited document identifier
* @param int $status the edited status
* @param array $actions the array of action
* @param array $users the list of the identifiers of the users
* @param string $changesurl link to file changes
* @param string $filetype extension of the document that is downloaded from the link specified with the url parameter
* @param mixed $forcesavetype the type of force save action
* @param array $history file history
* @param string $url the link to the edited document to be saved
* @param string $token request signature
*
* @return JSONResponse
*/
#[CORS]
#[NoAdminRequired]
#[NoCSRFRequired]
#[PublicPage]
public function track(
string $doc,
string $key,
int $status,
?array $actions = null,
?array $users = null,
?string $changesurl = null,
?string $filetype = null,
?int $forcesavetype = null,
?array $history = null,
?string $url = null,
?string $token = null
): JSONResponse {
[$hashData, $error] = $this->crypt->readHash($doc);
if ($hashData === null) {
$this->logger->error("Track with empty or not correct hash: $error");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
if ($hashData->action !== "track") {
$this->logger->error("Track with other action");
return new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST);
}
$fileId = $hashData->fileId;
$this->logger->debug("Track: $fileId status $status");
if (!empty($this->appConfig->getDocumentServerSecret())) {
if (!empty($token)) {
try {
$payload = \Firebase\JWT\JWT::decode($token, new \Firebase\JWT\Key($this->appConfig->getDocumentServerSecret(), "HS256"));
} catch (\UnexpectedValueException $e) {
$this->logger->error("Track with invalid jwt in body", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
} else {
$header = $this->request->getHeader($this->appConfig->jwtHeader());
if (empty($header)) {
$this->logger->error("Track without jwt");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
$header = substr((string) $header, strlen("Bearer "));
try {
$decodedHeader = \Firebase\JWT\JWT::decode($header, new \Firebase\JWT\Key($this->appConfig->getDocumentServerSecret(), "HS256"));
$payload = $decodedHeader->payload;
} catch (\UnexpectedValueException $e) {
$this->logger->error("Track with invalid jwt", ["exception" => $e]);
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
}
$users = $payload->users ?? null;
$key = $payload->key;
$status = $payload->status;
$url = $payload->url ?? null;
$mailMergeData = $payload->mailMerge ?? null;
}
$shareToken = $hashData->shareToken ?? null;
$filePath = $hashData->filePath;
$this->setupManager->tearDown();
$isForcesave = $status === self::TRACKERSTATUS_FORCESAVE || $status === self::TRACKERSTATUS_CORRUPTEDFORCESAVE;
$callbackUserId = $hashData->userId;
$userId = null;
if (!empty($users)
&& $status !== self::TRACKERSTATUS_EDITING) {
// author of the latest changes
$userId = $this->parseUserId($users[0]);
} else {
$userId = $callbackUserId;
}
if ($isForcesave
&& $forcesavetype === 1
&& !empty($actions)) {
// the user who clicked Save
$userId = $this->parseUserId($actions[0]["userid"]);
}
$user = $this->userManager->get($userId);
if (!empty($user)) {
\OC_User::setUserId($userId);
} else {
if (empty($shareToken)) {
$this->logger->error("Track without token: $fileId status $status");
return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN);
}
$this->logger->debug("Track $fileId by token for $userId");
}
// owner of file from the callback link
$ownerId = $hashData->userId;
$owner = $this->userManager->get($ownerId);
if (!empty($owner)) {
$userId = $ownerId;
} else {
$callbackUser = $this->userManager->get($callbackUserId);
if (!empty($callbackUser)) {
// author of the callback link
$userId = $callbackUserId;
// path for author of the callback link
$filePath = $hashData->filePath;
}
}
if (!empty($userId) && empty($shareToken) && ($setupUser = $this->userManager->get($userId))) {
$this->setupManager->setupForUser($setupUser);
}
[$file, $error, $share] = empty($shareToken) ? $this->getFile($userId, $fileId, $filePath) : $this->getFileByToken($fileId, $shareToken);
if (isset($error)) {
$this->logger->error("track error $fileId " . json_encode($error->getData()));
return $error;
}
$result = 1;
switch ($status) {
case self::TRACKERSTATUS_MUSTSAVE:
case self::TRACKERSTATUS_CORRUPTED:
case self::TRACKERSTATUS_FORCESAVE:
case self::TRACKERSTATUS_CORRUPTEDFORCESAVE:
if (empty($url)) {
$this->logger->error("Track without url: $fileId status $status");
return new JSONResponse(["message" => "Url not found"], Http::STATUS_BAD_REQUEST);
}
try {
$url = $this->appConfig->replaceDocumentServerUrlToInternal($url);
$prevVersion = $file->getFileInfo()->getMtime();
$fileName = $file->getName();
$curExt = strtolower(pathinfo((string) $fileName, PATHINFO_EXTENSION));
$downloadExt = $filetype;
if ($downloadExt !== $curExt) {
$key = DocumentService::generateRevisionId($fileId . $url);
try {
$this->logger->debug("Converted from $downloadExt to $curExt");
$url = $this->documentService->getConvertedUri($url, $downloadExt, $curExt, $key);
} catch (\Exception $e) {
$this->logger->error("Converted on save error", ["exception" => $e]);
return new JSONResponse(["message" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
$newData = $this->documentService->request($url);
$prevIsForcesave = $this->keyManager->wasForcesave($fileId);
if (RemoteInstance::isRemoteFile($file)) {
$isLock = RemoteInstance::lockRemoteKey($file, $isForcesave, false);
if ($isForcesave && !$isLock) {
break;
}
} else {
$this->keyManager->lock($fileId, $isForcesave);
}
$this->logger->debug("Track put content " . $file->getPath());
$retryOperation = function () use ($file, $newData): void {
$this->retryOperation(fn() => $file->putContent($newData));
};
try {
$lockContext = new LockContext($file, ILock::TYPE_APP, $this->appName);
$this->lockManager->runInScope($lockContext, $retryOperation);
} catch (NoLockProviderException $e) {
$retryOperation();
}
if (!$isForcesave) {
$this->unlock($file);
}
if (RemoteInstance::isRemoteFile($file)) {
if ($isForcesave) {
RemoteInstance::lockRemoteKey($file, false, $isForcesave);
}
} else {
$this->keyManager->lock($fileId, false);
$this->keyManager->setForcesave($fileId, $isForcesave);
}
if (!$isForcesave
&& !$prevIsForcesave
&& $this->versionManager instanceof IVersionManager
&& $this->appConfig->getVersionHistory()) {
$changes = null;
if (!empty($changesurl)) {
$changesurl = $this->appConfig->replaceDocumentServerUrlToInternal($changesurl);
try {
$changes = $this->documentService->request($changesurl);
} catch (\Exception $e) {
$this->logger->error("Failed to download changes", ["exception" => $e]);
}
}
FileVersions::saveHistory($file->getFileInfo(), $history, $changes, $prevVersion);
}
if (!empty($user) && $this->appConfig->getVersionHistory()) {
FileVersions::saveAuthor($file->getFileInfo(), $user);
}
$result = 0;
} catch (\Exception | \Error $e) {
$this->logger->error("Track: $fileId status $status error", ["exception" => $e]);
// if ($status === self::TRACKERSTATUS_MUSTSAVE) {
// $this->eventDispatcher->dispatchTyped(new DocumentUnsavedEvent($userId, $fileId, $file->getName()));
// }
}
break;
case self::TRACKERSTATUS_MAILMERGE:
$mailMergeStatusMessage = null;
if (empty($url)) {
$this->logger->error("MailMerge: file url cannot be empty");
return new JSONResponse(["message" => "Url not found"], Http::STATUS_BAD_REQUEST);
}
if (empty($user)) {
$this->logger->error("MailMerge: user not found");
return new JSONResponse(["message" => "User not found"], Http::STATUS_BAD_REQUEST);
}
if ($mailMergeData === null) {
$this->logger->error("MailMerge: data cannot be empty");
return new JSONResponse(["message" => "MailMerge data not found"], Http::STATUS_BAD_REQUEST);
}
try {
$url = $this->appConfig->replaceDocumentServerUrlToInternal($url);
$data = $this->documentService->request($url);
$isHtml = $mailMergeData->type === self::TYPE_HTML;
$bodyPlain = $mailMergeData->message ?? '';
$bodyHtml = $isHtml ? $data : '';
$mailMergeMessage = (new MailMergeMessage())
->setFrom($mailMergeData->from)
->setTo($mailMergeData->to)
->setSubject($mailMergeData->subject)
->setBodyPlain($bodyPlain)
->setBodyHtml($bodyHtml);
if (!$isHtml) {
$attachmentExtension = $mailMergeData->type === self::TYPE_ATTACH_DOCX ? 'docx' : 'pdf';
$attachmentName = pathinfo($mailMergeData->title ?? 'Attach', PATHINFO_FILENAME) . ".$attachmentExtension";
$mailMergeAttachment = (new MailMergeAttachment())
->setName($attachmentName)
->setExtension($attachmentExtension)
->setContent($data);
$mailMergeMessage->setAttachment($mailMergeAttachment);
}
$this->mailMergeService->send($userId, $mailMergeMessage);
$recordIndex = $mailMergeData->recordIndex + 1;
$this->logger->info(
"DocService MailMerge {$recordIndex}/{$mailMergeData->recordCount}"
);
} catch (\Exception $e) {
$recordCounterString = empty($mailMergeData) ? "" : " {$mailMergeData->recordIndex}/{$mailMergeData->recordCount} ";
$userIdString = $userId ?? "";
$urlString = $url ?? "";
$this->logger->error(
"DocService MailMerge$recordCounterString error: userId - $userIdString, url - $urlString",
['exception' => $e]
);
$mailMergeStatusMessage = $e->getMessage();
}
if ($mailMergeData !== null && $mailMergeData->recordIndex === ($mailMergeData->recordCount - 1)) {
$errorCount = !$mailMergeStatusMessage ? $mailMergeData->recordErrorCount : $mailMergeData->recordErrorCount + 1;
$mailMergeEndedEvent = new MailMergeEndedEvent(
$userId,
$mailMergeData->from,
$mailMergeData->recordCount,
$errorCount
);
$this->eventDispatcher->dispatchTyped($mailMergeEndedEvent);
}
if ($mailMergeStatusMessage) {
return new JSONResponse(
["error" => 1, "message" => $mailMergeStatusMessage],
Http::STATUS_BAD_REQUEST
);
}
$result = 0;
break;
case self::TRACKERSTATUS_EDITING:
$this->lock($file);
$result = 0;
break;
case self::TRACKERSTATUS_CLOSED:
$this->unlock($file);
$result = 0;
break;
}
$this->logger->debug("Track: $fileId status $status result $result");
return new JSONResponse(["error" => $result], Http::STATUS_OK);
}
/**
* Getting file by identifier
*
* @param string $userId - user identifier
* @param integer $fileId - file identifier
* @param string $filePath - file path
* @param integer $version - file version
* @param bool $template - file is template
*/
private function getFile(
?string $userId,
?int $fileId,
?string $filePath = null,
int $version = 0,
bool $template = false
): array {
if (empty($fileId)) {
return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST), 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, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST), null];
}
if (empty($files)) {
$this->logger->error("Files not found: $fileId");
return [null, new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_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 instanceof File)) {
$this->logger->error("File not found: $fileId");
return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND), null];
}
if ($version > 0 && $this->versionManager instanceof IVersionManager) {
$owner = $file->getOwner();
if ($owner !== null) {
if ($owner->getUID() !== $userId) {
[$file, $error, $share] = $this->getFile($owner->getUID(), $file->getId());
if (isset($error)) {
return [null, $error, null];
}
}
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
if ($version <= count($versions)) {
$fileVersion = array_values($versions)[$version - 1];
$file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId());
}
}
}
return [$file, null, null];
}
/**
* Getting file by token
*
* @param integer $fileId - file identifier
* @param string $shareToken - access token
* @param integer $version - file version
*/
private function getFileByToken(int $fileId, string $shareToken, int $version = 0): array {
[$share, $error] = $this->getShare($shareToken);
if (isset($error)) {
return [null, $error, null];
}
try {
$node = $share->getNode();
} catch (NotFoundException $e) {
$this->logger->error("getFileByToken error", ["exception" => $e]);
return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND), null];
}
if ($node instanceof Folder) {
try {
$files = $node->getById($fileId);
} catch (\Exception $e) {
$this->logger->error("getFileByToken: $fileId", ["exception" => $e]);
return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_NOT_FOUND), null];
}
if (empty($files)) {
$this->logger->error("getFileByToken Files not found: $fileId");
return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND), null];
}
$file = $files[0];
} else {
$file = $node;
}
if ($version > 0 && $this->versionManager instanceof IVersionManager) {
$owner = $file->getFileInfo()->getOwner();
if ($owner !== null) {
$versions = FileVersions::processVersionsArray($this->versionManager->getVersionsForFile($owner, $file));
if ($version <= count($versions)) {
$fileVersion = array_values($versions)[$version - 1];
$file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId());
}
}
}
return [$file, null, $share];
}
/**
* Getting share by token
*
* @param string $shareToken - access token
*/
private function getShare(?string $shareToken): array {
if (empty($shareToken)) {
return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)];
}
$share = null;
try {
$share = $this->shareManager->getShareByToken($shareToken);
} catch (ShareNotFound $e) {
$this->logger->error("getShare error", ["exception" => $e]);
$share = null;
}
if ($share === null || $share === false) {
return [null, new JSONResponse(["message" => $this->trans->t("You do not have enough permissions to view the file")], Http::STATUS_FORBIDDEN)];
}
return [$share, null];
}
/**
* Parse user identifier for current instance
*
* @param string $userId - unique user identifier
*/
private function parseUserId(string $userId): string {
$instanceId = $this->appConfig->getSystemValue("instanceid", true);
$instanceId .= "_";
if (str_starts_with($userId, $instanceId)) {
return substr($userId, strlen($instanceId));
}
return $userId;
}
/**
* Lock file by lock provider if exists
*
* @param File $file - file
*/
private function lock(File $file): void {
if (!$this->lockManager->isLockProviderAvailable()) {
return;
}
$fileId = $file->getId();
try {
if (empty($this->lockManager->getLocks($fileId))) {
$this->lockManager->lock(new LockContext($file, ILock::TYPE_APP, $this->appName));
$this->logger->debug("$this->appName has locked file $fileId");
}
} catch (PreConditionNotMetException | OwnerLockedException | NoLockProviderException) {
}
}
/**
* Unlock file by lock provider if exists
*
* @param File $file - file
*/
private function unlock(File $file): void {
if (!$this->lockManager->isLockProviderAvailable()) {
return;
}
$fileId = $file->getId();
try {
$this->lockManager->unlock(new LockContext($file, ILock::TYPE_APP, $this->appName));
$this->logger->debug("$this->appName has unlocked file $fileId");
} catch (PreConditionNotMetException | NoLockProviderException) {
}
}
/**
* Retry operation if a LockedException occurred
* Other exceptions will still be thrown
*
* @throws LockedException
*/
private function retryOperation(callable $operation) {
$i = 0;
while (true) {
try {
return $operation();
} catch (LockedException $e) {
if (++$i === 4) {
throw $e;
}
}
usleep(500000);
}
}
}