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

904 lines
36 KiB

<?php
4 weeks ago
/*
* Copyright (C) Ascensio System SIA, 2009-2026
*
4 weeks 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.
*
4 weeks 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
*
4 weeks 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
*
4 weeks 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.
*
4 weeks ago
* No trademark rights are granted under this License.
*
4 weeks 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
*
4 weeks 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.
*
4 weeks ago
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Onlyoffice\Controller;
use OC\Files\SetupManager;
3 years ago
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;
3 years ago
use OCA\Onlyoffice\FileVersions;
use OCA\Onlyoffice\FileUtility;
3 years ago
use OCA\Onlyoffice\KeyManager;
use OCA\Onlyoffice\MailMergeMessage;
use OCA\Onlyoffice\MailMergeAttachment;
use OCA\Onlyoffice\MailMergeService;
3 years ago
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;
8 months ago
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;
3 years ago
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lock\LockedException;
3 years ago
use OCP\PreConditionNotMetException;
use OCP\Share\Exceptions\ShareNotFound;
8 years ago
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;
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 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
3 years ago
) {
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);
6 years ago
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);
}
9 years ago
$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" : ""));
9 years ago
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);
8 years ago
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;
}
8 years ago
} 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
*
6 years ago
* @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);
6 years ago
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");
8 years ago
} 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);
6 years ago
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);
}
9 years ago
$fileId = $hashData->fileId;
$this->logger->debug("Track: $fileId status $status");
9 years ago
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();
4 years ago
}
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:
4 years ago
$this->lock($file);
$result = 0;
break;
case self::TRACKERSTATUS_CLOSED:
4 years ago
$this->unlock($file);
$result = 0;
break;
}
$this->logger->debug("Track: $fileId status $status result $result");
return new JSONResponse(["error" => $result], Http::STATUS_OK);
}
8 years ago
/**
* Getting file by identifier
*
8 years ago
* @param string $userId - user identifier
8 years ago
* @param integer $fileId - file identifier
* @param string $filePath - file path
* @param integer $version - file version
5 years ago
* @param bool $template - file is template
8 years ago
*/
private function getFile(
?string $userId,
?int $fileId,
?string $filePath = null,
int $version = 0,
bool $template = false
): array {
8 years ago
if (empty($fileId)) {
return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST), null];
8 years ago
}
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]);
return [null, new JSONResponse(["message" => $this->trans->t("Invalid request")], Http::STATUS_BAD_REQUEST), null];
}
8 years ago
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];
8 years ago
}
8 years ago
$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];
8 years ago
}
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];
8 years ago
}
8 years ago
/**
* Getting file by token
*
* @param integer $fileId - file identifier
* @param string $shareToken - access token
* @param integer $version - file version
8 years ago
*/
private function getFileByToken(int $fileId, string $shareToken, int $version = 0): array {
[$share, $error] = $this->getShare($shareToken);
8 years ago
if (isset($error)) {
return [null, $error, null];
8 years ago
}
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];
}
8 years ago
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)) {
7 months ago
$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];
8 years ago
}
/**
* Getting share by token
*
* @param string $shareToken - access token
8 years ago
*/
private function getShare(?string $shareToken): array {
if (empty($shareToken)) {
6 years ago
return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)];
8 years ago
}
6 years ago
$share = null;
try {
$share = $this->shareManager->getShareByToken($shareToken);
} catch (ShareNotFound $e) {
$this->logger->error("getShare error", ["exception" => $e]);
6 years ago
$share = null;
}
6 years ago
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)];
8 years ago
}
6 years ago
return [$share, null];
8 years ago
}
/**
* 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;
}
4 years ago
/**
* Lock file by lock provider if exists
*
* @param File $file - file
*/
private function lock(File $file): void {
4 years ago
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");
4 years ago
}
} catch (PreConditionNotMetException | OwnerLockedException | NoLockProviderException) {
3 years ago
}
4 years ago
}
/**
* Unlock file by lock provider if exists
*
* @param File $file - file
*/
private function unlock(File $file): void {
4 years ago
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) {
3 years ago
}
4 years ago
}
/**
* 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);
}
}
7 years ago
}