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("_", "-", $this->trans->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 = \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("_", "-", $this->trans->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 = \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 = \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"); } }