diff options
-rw-r--r-- | appinfo/application.php | 4 | ||||
-rw-r--r-- | appinfo/routes.php | 2 | ||||
-rw-r--r-- | controller/callbackcontroller.php | 112 | ||||
-rw-r--r-- | controller/editorcontroller.php | 282 | ||||
-rw-r--r-- | js/editor.js | 98 | ||||
-rw-r--r-- | js/listener.js | 10 | ||||
-rw-r--r-- | js/main.js | 51 | ||||
-rw-r--r-- | lib/directeditor.php | 1 | ||||
-rw-r--r-- | lib/fileutility.php | 17 | ||||
-rw-r--r-- | lib/fileversions.php | 338 | ||||
-rw-r--r-- | lib/hooks.php | 152 | ||||
-rw-r--r-- | templates/editor.php | 1 |
12 files changed, 1034 insertions, 34 deletions
diff --git a/appinfo/application.php b/appinfo/application.php index 0e06449..e83a0e9 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -42,6 +42,7 @@ use OCA\Onlyoffice\Controller\EditorController; use OCA\Onlyoffice\Controller\SettingsController; use OCA\Onlyoffice\Crypt; use OCA\Onlyoffice\DirectEditor; +use OCA\Onlyoffice\Hooks; class Application extends App { @@ -216,5 +217,8 @@ class Application extends App { $c->query("IManager") ); }); + + + Hooks::connectHooks(); } } diff --git a/appinfo/routes.php b/appinfo/routes.php index 3497183..ab5d6df 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -40,6 +40,8 @@ return [ ["name" => "editor#convert", "url" => "/ajax/convert", "verb" => "POST"], ["name" => "editor#save", "url" => "/ajax/save", "verb" => "POST"], ["name" => "editor#url", "url" => "/ajax/url", "verb" => "GET"], + ["name" => "editor#history", "url" => "/ajax/history", "verb" => "GET"], + ["name" => "editor#version", "url" => "/ajax/version", "verb" => "GET"], ["name" => "settings#save_address", "url" => "/ajax/settings/address", "verb" => "PUT"], ["name" => "settings#save_common", "url" => "/ajax/settings/common", "verb" => "PUT"], ["name" => "settings#save_watermark", "url" => "/ajax/settings/watermark", "verb" => "PUT"], diff --git a/controller/callbackcontroller.php b/controller/callbackcontroller.php index 7037c9f..18c6ec1 100644 --- a/controller/callbackcontroller.php +++ b/controller/callbackcontroller.php @@ -47,9 +47,12 @@ use OCP\Lock\LockedException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; +use OCA\Files_Versions\Versions\IVersionManager; + use OCA\Onlyoffice\AppConfig; use OCA\Onlyoffice\Crypt; use OCA\Onlyoffice\DocumentService; +use OCA\Onlyoffice\FileVersions; /** * Callback handler for the document server. @@ -115,6 +118,13 @@ class CallbackController extends Controller { private $shareManager; /** + * File version manager + * + * @var IVersionManager + */ + private $versionManager; + + /** * Status of the document * * @var Array @@ -160,6 +170,14 @@ class CallbackController extends Controller { $this->config = $config; $this->crypt = $crypt; $this->shareManager = $shareManager; + + if (\OC::$server->getAppManager()->isInstalled("files_versions")) { + try { + $this->versionManager = \OC::$server->query(IVersionManager::class); + } catch (QueryException $e) { + $this->logger->logException($e, ["message" => "VersionManager init error", "app" => $this->appName]); + } + } } @@ -188,9 +206,12 @@ class CallbackController extends Controller { } $fileId = $hashData->fileId; - $this->logger->debug("Download: $fileId", ["app" => $this->appName]); + $version = isset($hashData->version) ? $hashData->version : null; + $changes = isset($hashData->changes) ? $hashData->changes : false; + $this->logger->debug("Download: $fileId ($version)" . ($changes ? " changes" : ""), ["app" => $this->appName]); - if (!$this->userSession->isLoggedIn()) { + if (!$this->userSession->isLoggedIn() + && !$changes) { if (!empty($this->config->GetDocumentServerSecret())) { $header = \OC::$server->getRequest()->getHeader($this->config->JwtHeader()); if (empty($header)) { @@ -224,7 +245,7 @@ class CallbackController extends Controller { } $shareToken = isset($hashData->shareToken) ? $hashData->shareToken : null; - list ($file, $error) = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->getFileByToken($fileId, $shareToken); + list ($file, $error) = empty($shareToken) ? $this->getFile($userId, $fileId, null, $changes ? null : $version) : $this->getFileByToken($fileId, $shareToken, $changes ? null : $version); if (isset($error)) { return $error; @@ -235,10 +256,42 @@ class CallbackController extends Controller { return new JSONResponse(["message" => $this->trans->t("Access denied")], Http::STATUS_FORBIDDEN); } + if ($changes) { + if ($this->versionManager === null) { + $this->logger->error("Download changes: versionManager is null", ["app" => $this->appName]); + 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", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); + } + + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + $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(), $fileId, $versionId); + if ($changesFile === null) { + $this->logger->error("Download: changes $fileId ($version) was not found", ["app" => $this->appName]); + return new JSONResponse(["message" => $this->trans->t("Files not found")], Http::STATUS_NOT_FOUND); + } + + $file = $changesFile; + } + try { return new DataDownloadResponse($file->getContent(), $file->getName(), $file->getMimeType()); } catch (NotPermittedException $e) { - $this->logger->logException($e, ["message" => "Download Not permitted: $fileId", "app" => $this->appName]); + $this->logger->logException($e, ["message" => "Download Not permitted: $fileId ($version)", "app" => $this->appName]); 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); @@ -312,6 +365,8 @@ class CallbackController extends Controller { * @param integer $status - the edited status * @param string $url - the link to the edited document to be saved * @param string $token - request signature + * @param array $history - file history + * @param string $changesurl - link to file changes * * @return array * @@ -320,7 +375,7 @@ class CallbackController extends Controller { * @PublicPage * @CORS */ - public function track($doc, $users, $key, $status, $url, $token) { + public function track($doc, $users, $key, $status, $url, $token, $history, $changesurl) { list ($hashData, $error) = $this->crypt->ReadHash($doc); if ($hashData === null) { @@ -424,6 +479,7 @@ class CallbackController extends Controller { $url = $this->config->ReplaceDocumentServerUrlToInternal($url); + $prevVersion = $file->getFileInfo()->getMtime(); $fileName = $file->getName(); $curExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); $downloadExt = strtolower(pathinfo($url, PATHINFO_EXTENSION)); @@ -448,9 +504,18 @@ class CallbackController extends Controller { return $file->putContent($newData); }); + if ($this->versionManager !== null) { + $changes = null; + if (!empty($changesurl)) { + $changesurl = $this->config->ReplaceDocumentServerUrlToInternal($changesurl); + $changes = $documentService->Request($changesurl); + } + FileVersions::saveHistory($file->getFileInfo(), $history, $changes, $prevVersion); + } + $result = 0; } catch (\Exception $e) { - $this->logger->logException($e, ["message" => "Track $trackerStatus error", "app" => $this->appName]); + $this->logger->logException($e, ["message" => "Track: $fileId status $trackerStatus error", "app" => $this->appName]); } break; @@ -472,10 +537,11 @@ class CallbackController extends Controller { * @param string $userId - user identifier * @param integer $fileId - file identifier * @param string $filePath - file path + * @param integer $version - file version * * @return array */ - private function getFile($userId, $fileId, $filePath = null) { + private function getFile($userId, $fileId, $filePath = null, $version = 0) { if (empty($fileId)) { return [null, new JSONResponse(["message" => $this->trans->t("FileId is empty")], Http::STATUS_BAD_REQUEST)]; } @@ -509,6 +575,25 @@ class CallbackController extends Controller { return [null, new JSONResponse(["message" => $this->trans->t("File not found")], Http::STATUS_NOT_FOUND)]; } + if ($version > 0 && $this->versionManager !== null) { + $owner = $file->getFileInfo()->getOwner(); + + if ($owner->getUID() !== $userId) { + list ($file, $error) = $this->getFile($owner->getUID(), $file->getId()); + + if (isset($error)) { + return [null, $error]; + } + } + + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); + } + } + return [$file, null]; } @@ -517,10 +602,11 @@ class CallbackController extends Controller { * * @param integer $fileId - file identifier * @param string $shareToken - access token + * @param integer $version - file version * * @return array */ - private function getFileByToken($fileId, $shareToken) { + private function getFileByToken($fileId, $shareToken, $version = 0) { list ($share, $error) = $this->getShare($shareToken); if (isset($error)) { @@ -550,6 +636,16 @@ class CallbackController extends Controller { $file = $node; } + if ($version > 0 && $this->versionManager !== null) { + $owner = $file->getFileInfo()->getOwner(); + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + $file = $this->versionManager->getVersionFile($owner, $file->getFileInfo(), $fileVersion->getRevisionId()); + } + } + return [$file, null]; } diff --git a/controller/editorcontroller.php b/controller/editorcontroller.php index 01e6db4..13c342a 100644 --- a/controller/editorcontroller.php +++ b/controller/editorcontroller.php @@ -49,12 +49,14 @@ use OCP\IUserSession; use OCP\Share\IManager; use OCA\Files\Helper; +use OCA\Files_Versions\Versions\IVersionManager; use OCA\Onlyoffice\AppConfig; use OCA\Onlyoffice\Crypt; use OCA\Onlyoffice\DocumentService; use OCA\Onlyoffice\FileUtility; use OCA\Onlyoffice\TemplateManager; +use OCA\Onlyoffice\FileVersions; /** * Controller with the main functions @@ -125,6 +127,13 @@ class EditorController extends Controller { private $fileUtility; /** + * File version manager + * + * @var IVersionManager + */ + private $versionManager; + + /** * Mobile regex from https://github.com/ONLYOFFICE/CommunityServer/blob/v9.1.1/web/studio/ASC.Web.Studio/web.appsettings.config#L35 */ const USER_AGENT_MOBILE = "/android|avantgo|playbook|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i"; @@ -167,6 +176,14 @@ class EditorController extends Controller { $this->config = $config; $this->crypt = $crypt; + if (\OC::$server->getAppManager()->isInstalled("files_versions")) { + try { + $this->versionManager = \OC::$server->query(IVersionManager::class); + } catch (QueryException $e) { + $this->logger->logException($e, ["message" => "VersionManager init error", "app" => $this->appName]); + } + } + $this->fileUtility = new FileUtility($AppName, $trans, $logger, $config, $shareManager, $session); } @@ -408,6 +425,204 @@ class EditorController extends Controller { } /** + * Get versions history for file + * + * @param integer $fileId - file identifier + * @param string $shareToken - access token + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function history($fileId, $shareToken = null) { + $this->logger->debug("Request history for: $fileId", ["app" => $this->appName]); + + $history = []; + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list ($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->fileUtility->getFileByToken($fileId, $shareToken); + + if (isset($error)) { + $this->logger->error("History: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($fileId === 0) { + $fileId = $file->getId(); + } + + $owner = null; + $ownerId = null; + $versions = array(); + if ($this->versionManager !== null) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $ownerId = $owner->getUID(); + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + } + } + + $prevVersion = ""; + $versionNum = 0; + foreach ($versions as $version) { + $versionNum = $versionNum + 1; + + $key = $this->fileUtility->getVersionKey($version); + $key = DocumentService::GenerateRevisionId($key); + + $historyItem = [ + "created" => $this->trans->l("datetime", $version->getTimestamp(), ["width" => "short"]), + "key" => $key, + "user" => [ + "id" => $this->buildUserId($ownerId), + "name" => $owner->getDisplayName() + ], + "version" => $versionNum + ]; + + $versionId = $version->getRevisionId(); + $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); + if ($historyData !== null) { + $historyItem["changes"] = $historyData["changes"]; + $historyItem["serverVersion"] = $historyData["serverVersion"]; + } + + $prevVersion = $versionId; + + array_push($history, $historyItem); + } + + $key = $this->fileUtility->getKey($file, true); + $key = DocumentService::GenerateRevisionId($key); + + $historyItem = [ + "created" => $this->trans->l("datetime", $file->getMTime(), ["width" => "short"]), + "key" => $key, + "version" => $versionNum + 1 + ]; + + if ($owner !== null) { + $historyItem["user"] = [ + "id" => $this->buildUserId($owner->getUID()), + "name" => $owner->getDisplayName() + ]; + } + + $versionId = $file->getFileInfo()->getMtime(); + $historyData = FileVersions::getHistoryData($ownerId, $fileId, $versionId, $prevVersion); + if ($historyData !== null) { + $historyItem["changes"] = $historyData["changes"]; + $historyItem["serverVersion"] = $historyData["serverVersion"]; + } + + array_push($history, $historyItem); + + return $history; + } + + /** + * Get file attributes of specific version + * + * @param integer $fileId - file identifier + * @param integer $version - file version + * @param string $shareToken - access token + * + * @return array + * + * @NoAdminRequired + * @PublicPage + */ + public function version($fileId, $version, $shareToken = null) { + $this->logger->debug("Request version for: $fileId ($version)", ["app" => $this->appName]); + + $version = empty($version) ? null : $version; + + $user = $this->userSession->getUser(); + $userId = null; + if (!empty($user)) { + $userId = $user->getUID(); + } + + list ($file, $error, $share) = empty($shareToken) ? $this->getFile($userId, $fileId) : $this->fileUtility->getFileByToken($fileId, $shareToken); + + if (isset($error)) { + $this->logger->error("History: $fileId $error", ["app" => $this->appName]); + return ["error" => $error]; + } + + if ($fileId === 0) { + $fileId = $file->getId(); + } + + $owner = null; + $ownerId = null; + $versions = array(); + if ($this->versionManager !== null) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $ownerId = $owner->getUID(); + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + } + } + + $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, $shareToken); + } else { + $fileVersion = array_values($versions)[$version - 1]; + + $key = $this->fileUtility->getVersionKey($fileVersion); + $versionId = $fileVersion->getRevisionId(); + + $fileUrl = $this->getUrl($file, $user, $shareToken, $version); + } + $key = DocumentService::GenerateRevisionId($key); + + $result = [ + "url" => $fileUrl, + "version" => $version, + "key" => $key + ]; + + if ($version > 1 + && count($versions) >= $version - 1 + && FileVersions::hasChanges($ownerId, $fileId, $versionId)) { + + $changesUrl = $this->getUrl($file, $user, $shareToken, $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, $shareToken, $version - 1); + + $result["previous"] = [ + "key" => $prevVersionKey, + "url" => $prevVersionUrl + ]; + } + + if (!empty($this->config->GetDocumentServerSecret())) { + $token = \Firebase\JWT\JWT::encode($result, $this->config->GetDocumentServerSecret()); + $result["token"] = $token; + } + + return $result; + } + + /** * Get presigned url to file * * @param string $filePath - file path @@ -461,6 +676,7 @@ class EditorController extends Controller { * @param integer $fileId - file identifier * @param string $filePath - file path * @param string $shareToken - access token + * @param integer $version - file version * @param bool $inframe - open in frame * * @return TemplateResponse|RedirectResponse @@ -468,8 +684,8 @@ class EditorController extends Controller { * @NoAdminRequired * @NoCSRFRequired */ - public function index($fileId, $filePath = null, $shareToken = null, $inframe = false) { - $this->logger->debug("Open: $fileId $filePath", ["app" => $this->appName]); + public function index($fileId, $filePath = null, $shareToken = null, $version = 0, $inframe = false) { + $this->logger->debug("Open: $fileId ($version) $filePath ", ["app" => $this->appName]); $isLoggedIn = $this->userSession->isLoggedIn(); if (empty($shareToken) && !$isLoggedIn) { @@ -496,6 +712,7 @@ class EditorController extends Controller { "filePath" => $filePath, "shareToken" => $shareToken, "directToken" => null, + "version" => $version, "inframe" => false ]; @@ -535,6 +752,7 @@ class EditorController extends Controller { * * @param integer $fileId - file identifier * @param string $shareToken - access token + * @param integer $version - file version * @param bool $inframe - open in frame * * @return TemplateResponse @@ -543,8 +761,8 @@ class EditorController extends Controller { * @NoCSRFRequired * @PublicPage */ - public function PublicPage($fileId, $shareToken, $inframe = false) { - return $this->index($fileId, null, $shareToken, $inframe); + public function PublicPage($fileId, $shareToken, $version = 0, $inframe = false) { + return $this->index($fileId, null, $shareToken, $version, $inframe); } /** @@ -567,6 +785,7 @@ class EditorController extends Controller { * @param string $filePath - file path * @param string $shareToken - access token * @param string $directToken - direct token + * @param integer $version - file version * @param integer $inframe - open in frame. 0 - no, 1 - yes, 2 - without goback for old editor (5.4) * @param bool $desktop - desktop label * @@ -575,7 +794,7 @@ class EditorController extends Controller { * @NoAdminRequired * @PublicPage */ - public function config($fileId, $filePath = null, $shareToken = null, $directToken = null, $inframe = 0, $desktop = false) { + public function config($fileId, $filePath = null, $shareToken = null, $directToken = null, $version = 0, $inframe = 0, $desktop = false) { if (!empty($directToken)) { list ($directData, $error) = $this->crypt->ReadHash($directToken); @@ -628,8 +847,25 @@ class EditorController extends Controller { return ["error" => $this->trans->t("Format is not supported")]; } - $fileUrl = $this->getUrl($file, $user, $shareToken); - $key = $this->fileUtility->getKey($file, true); + $fileUrl = $this->getUrl($file, $user, $shareToken, $version); + + $key = null; + if ($version > 0 + && $this->versionManager !== null) { + $owner = $file->getFileInfo()->getOwner(); + if ($owner !== null) { + $versions = array_reverse($this->versionManager->getVersionsForFile($owner, $file->getFileInfo())); + + if ($version <= count($versions)) { + $fileVersion = array_values($versions)[$version - 1]; + + $key = $this->fileUtility->getVersionKey($fileVersion); + } + } + } + if ($key === null) { + $key = $this->fileUtility->getKey($file, true); + } $key = DocumentService::GenerateRevisionId($key); $params = [ @@ -653,7 +889,8 @@ class EditorController extends Controller { } $canEdit = isset($format["edit"]) && $format["edit"]; - $editable = $file->isUpdateable() + $editable = $version < 1 + && $file->isUpdateable() && (empty($shareToken) || ($share->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE); $params["document"]["permissions"]["edit"] = $editable; if ($editable && $canEdit) { @@ -746,7 +983,7 @@ class EditorController extends Controller { $params["token"] = $token; } - $this->logger->debug("Config is generated for: $fileId with key $key", ["app" => $this->appName]); + $this->logger->debug("Config is generated for: $fileId ($version) with key $key", ["app" => $this->appName]); return $params; } @@ -799,23 +1036,42 @@ class EditorController extends Controller { /** * Generate secure link to download document * - * @param integer $file - file + * @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 * * @return string */ - private function getUrl($file, $user = null, $shareToken = null) { + private function getUrl($file, $user = null, $shareToken = null, $version = 0, $changes = false) { + + $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; } - $hashUrl = $this->crypt->GetHash(["fileId" => $file->getId(), "userId" => $userId, "shareToken" => $shareToken, "action" => "download"]); + $hashUrl = $this->crypt->GetHash($data); $fileUrl = $this->urlGenerator->linkToRouteAbsolute($this->appName . ".callback.download", ["doc" => $hashUrl]); - if (!empty($this->config->GetStorageUrl())) { + if (!empty($this->config->GetStorageUrl()) + && !$changes) { $fileUrl = str_replace($this->urlGenerator->getAbsoluteURL("/"), $this->config->GetStorageUrl(), $fileUrl); } diff --git a/js/editor.js b/js/editor.js index 284fe67..d8ac41b 100644 --- a/js/editor.js +++ b/js/editor.js @@ -30,7 +30,9 @@ OCA.Onlyoffice = _.extend({ AppName: "onlyoffice", - inframe: false + inframe: false, + fileId: null, + shareToken: null }, OCA.Onlyoffice); OCA.Onlyoffice.InitEditor = function () { @@ -40,11 +42,12 @@ }); }; - var fileId = $("#iframeEditor").data("id"); - var shareToken = $("#iframeEditor").data("sharetoken"); + OCA.Onlyoffice.fileId = $("#iframeEditor").data("id"); + OCA.Onlyoffice.shareToken = $("#iframeEditor").data("sharetoken"); + OCA.Onlyoffice.version = $("#iframeEditor").data("version"); var directToken = $("#iframeEditor").data("directtoken"); OCA.Onlyoffice.inframe = !!$("#iframeEditor").data("inframe"); - if (!fileId && !shareToken && !directToken) { + if (!OCA.Onlyoffice.fileId && !OCA.Onlyoffice.shareToken && !directToken) { displayError(t(OCA.Onlyoffice.AppName, "FileId is empty")); return; } @@ -56,7 +59,7 @@ var configUrl = OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/config/{fileId}", { - fileId: fileId || 0 + fileId: OCA.Onlyoffice.fileId || 0 }); var params = []; @@ -64,13 +67,16 @@ if (filePath) { params.push("filePath=" + encodeURIComponent(filePath)); } - if (shareToken) { - params.push("shareToken=" + encodeURIComponent(shareToken)); + if (OCA.Onlyoffice.shareToken) { + params.push("shareToken=" + encodeURIComponent(OCA.Onlyoffice.shareToken)); } if (directToken) { $("html").addClass("onlyoffice-full-page"); params.push("directToken=" + encodeURIComponent(directToken)); } + if (OCA.Onlyoffice.version > 0) { + params.push("version=" + OCA.Onlyoffice.version); + } if (OCA.Onlyoffice.inframe || directToken) { var dsVersion = DocsAPI.DocEditor.version(); @@ -133,15 +139,22 @@ config.events = { "onDocumentStateChange": setPageTitle, + "onRequestHistory": OCA.Onlyoffice.onRequestHistory, + "onRequestHistoryData": OCA.Onlyoffice.onRequestHistoryData, + "onDocumentReady": OCA.Onlyoffice.onDocumentReady, }; + if (!OCA.Onlyoffice.version) { + config.events.onRequestHistoryClose = OCA.Onlyoffice.onRequestHistoryClose; + } + if (config.editorConfig.tenant) { config.events.onAppReady = function () { OCA.Onlyoffice.docEditor.showMessage(t(OCA.Onlyoffice.AppName, "You are using public demo ONLYOFFICE Document Server. Please do not store private sensitive data.")); }; } - if (OCA.Onlyoffice.inframe && !shareToken + if (OCA.Onlyoffice.inframe && !OCA.Onlyoffice.shareToken || OC.currentUser) { config.events.onRequestSaveAs = OCA.Onlyoffice.onRequestSaveAs; config.events.onRequestInsertImage = OCA.Onlyoffice.onRequestInsertImage; @@ -154,7 +167,7 @@ } if (OCA.Onlyoffice.inframe - && config._files_sharing && !shareToken + && config._files_sharing && !OCA.Onlyoffice.shareToken && window.parent.OCA.Onlyoffice.context) { config.events.onRequestSharingSettings = OCA.Onlyoffice.onRequestSharingSettings; } @@ -174,6 +187,73 @@ }); }; + OCA.Onlyoffice.onRequestHistory = function (version) { + $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/history?fileId={fileId}&shareToken={shareToken}", + { + fileId: OCA.Onlyoffice.fileId || 0, + shareToken: OCA.Onlyoffice.shareToken || "", + }), + function onSuccess(response) { + if (response.error) { + var data = {error: response.error}; + } else { + var currentVersion = 0; + $.each(response, function (i, fileVersion) { + if (fileVersion.version >= currentVersion) { + currentVersion = fileVersion.version; + } + }); + + if (version) { + currentVersion = Math.min(currentVersion, version); + } + + data = { + currentVersion: currentVersion, + history: response, + }; + } + OCA.Onlyoffice.docEditor.refreshHistory(data); + }); + }; + + OCA.Onlyoffice.onRequestHistoryData = function (event) { + var version = event.data; + + $.get(OC.generateUrl("apps/" + OCA.Onlyoffice.AppName + "/ajax/version?fileId={fileId}&version={version}&shareToken={shareToken}", + { + fileId: OCA.Onlyoffice.fileId || 0, + version: version, + shareToken: OCA.Onlyoffice.shareToken || "", + }), + function onSuccess(response) { + if (response.error) { + response = { + error: response.error, + version: version, + }; + } + OCA.Onlyoffice.docEditor.setHistoryData(response); + }); + }; + + OCA.Onlyoffice.onRequestHistoryClose = function () { + location.reload(true); + }; + + OCA.Onlyoffice.onDocumentReady = function() { + if (OCA.Onlyoffice.inframe) { + window.parent.postMessage({ + method: "onDocumentReady" + }, + "*"); + } + + if (OCA.Onlyoffice.version > 0) { + OCA.Onlyoffice.onRequestHistory(OCA.Onlyoffice.version); + } + }; + OCA.Onlyoffice.onRequestSaveAs = function (event) { var saveData = { name: event.data.title, diff --git a/js/listener.js b/js/listener.js index 3a25edb..f571010 100644 --- a/js/listener.js +++ b/js/listener.js @@ -36,6 +36,7 @@ OCA.Onlyoffice.onRequestClose = function () { $(OCA.Onlyoffice.frameSelector).remove(); + OCA.Onlyoffice.frameSelector = null; if (OCA.Viewer && OCA.Viewer.close) { OCA.Viewer.close(); @@ -77,6 +78,12 @@ revisedMimes); }; + OCA.Onlyoffice.onDocumentReady = function () { + if (OCA.Onlyoffice.bindVersionClick) { + OCA.Onlyoffice.bindVersionClick(); + } + }; + window.addEventListener("message", function (event) { if ($(OCA.Onlyoffice.frameSelector)[0].contentWindow !== event.source || !event.data["method"]) { @@ -108,6 +115,9 @@ case "editorRequestCompareFile": OCA.Onlyoffice.onRequestCompareFile(event.data.param); break; + case "onDocumentReady": + OCA.Onlyoffice.onDocumentReady(); + break; } }, false); @@ -66,7 +66,7 @@ } fileList.add(response, { animate: true }); - OCA.Onlyoffice.OpenEditor(response.id, dir, response.name, winEditor); + OCA.Onlyoffice.OpenEditor(response.id, dir, response.name, 0, winEditor); OCA.Onlyoffice.context = { fileList: fileList }; OCA.Onlyoffice.context.fileName = response.name; @@ -76,8 +76,11 @@ ); }; - OCA.Onlyoffice.OpenEditor = function (fileId, fileDir, fileName, winEditor) { - var filePath = fileDir.replace(new RegExp("\/$"), "") + "/" + fileName; + OCA.Onlyoffice.OpenEditor = function (fileId, fileDir, fileName, version, winEditor) { + var filePath = ""; + if (fileName) { + filePath = fileDir.replace(new RegExp("\/$"), "") + "/" + fileName; + } var url = OC.generateUrl("/apps/" + OCA.Onlyoffice.AppName + "/{fileId}?filePath={filePath}", { fileId: fileId, @@ -92,6 +95,10 @@ }); } + if (version > 0) { + url += "&version=" + version; + } + if (winEditor && winEditor.location) { winEditor.location.href = url; } else if (!OCA.Onlyoffice.setting.sameTab || OCA.Onlyoffice.Desktop) { @@ -305,6 +312,40 @@ return extension; } + OCA.Onlyoffice.openVersion = function (fileId, version) { + if (OCA.Onlyoffice.frameSelector) { + $(OCA.Onlyoffice.frameSelector)[0].contentWindow.OCA.Onlyoffice.onRequestHistory(version); + return; + } + + OCA.Onlyoffice.OpenEditor(fileId, "", "", version) + }; + + OCA.Onlyoffice.bindVersionClick = function () { + unbindVersionClick(); + $(document).on("click.onlyoffice-version", "#versionsTabView .downloadVersion", function() { + var versionNodes = $("#versionsTabView ul.versions>li"); + var versionNode = $(this).closest("#versionsTabView ul.versions>li")[0]; + + var href = $(this).attr("href"); + var search = new RegExp("\/versions\/\\w+\/versions\/(\\d+)\/\\d+"); + var result = search.exec(href); + if (result && result.length > 1) { + var fileId = result[1]; + } + + var versionNum = versionNodes.length - $.inArray(versionNode, versionNodes); + + OCA.Onlyoffice.openVersion(fileId || "", versionNum); + + return false; + }); + }; + + var unbindVersionClick = function() { + $(document).off("click.onlyoffice-version", "#versionsTabView .downloadVersion"); + } + var initPage = function () { if ($("#isPublic").val() === "1" && !$("#filestable").length) { var fileName = $("#filename").val(); @@ -334,6 +375,10 @@ } else { OC.Plugins.register("OCA.Files.FileList", OCA.Onlyoffice.FileList); OC.Plugins.register("OCA.Files.NewFileMenu", OCA.Onlyoffice.NewFileMenu); + + if (OCA.Versions) { + OCA.Onlyoffice.bindVersionClick(); + } } }; diff --git a/lib/directeditor.php b/lib/directeditor.php index aa8cabe..aebe56f 100644 --- a/lib/directeditor.php +++ b/lib/directeditor.php @@ -244,6 +244,7 @@ class DirectEditor implements IEditor { "filePath" => $filePath, "shareToken" => null, "directToken" => $directToken, + "version" => 0, "inframe" => false ]; diff --git a/lib/fileutility.php b/lib/fileutility.php index dd0edbc..bfba9ec 100644 --- a/lib/fileutility.php +++ b/lib/fileutility.php @@ -129,7 +129,7 @@ class FileUtility { } if ($node instanceof Folder) { - if ($fileId !== null) { + if ($fileId !== null && $fileId !== 0) { try { $files = $node->getById($fileId); } catch (\Exception $e) { @@ -282,4 +282,19 @@ class FileUtility { return $key; } + + /** + * Generate unique file version key + * + * @param OCA\Files_Versions\Versions\IVersion $version - file version + * + * @return string + */ + public function getVersionKey($version) { + $instanceId = $this->config->GetSystemValue("instanceid", true); + + $key = $instanceId . "_" . $version->getSourceFile()->getEtag() . "_" . $version->getRevisionId(); + + return $key; + } } diff --git a/lib/fileversions.php b/lib/fileversions.php new file mode 100644 index 0000000..32f65ce --- /dev/null +++ b/lib/fileversions.php @@ -0,0 +1,338 @@ +<?php +/** + * + * (c) Copyright Ascensio System SIA 2020 + * + * 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. + * In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * 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: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions of the Program + * must display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product logo when distributing the program. + * Pursuant to Section 7(e) we decline to grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as well as technical + * writing content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International. + * See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +namespace OCA\Onlyoffice; + +use OC\Files\Node\File; +use OC\Files\View; + +/** + * File versions + * + * @package OCA\Onlyoffice + */ +class FileVersions { + + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + /** + * Changes file extension + * + * @var string + */ + private static $changesExt = ".zip"; + + /** + * History file extension + * + * @var string + */ + private static $historyExt = ".json"; + + /** + * Split file path and version id + * + * @param string $pathVersion - version path + * + * @return array + */ + public static function splitPathVersion($pathVersion) { + $pos = strrpos($pathVersion, ".v"); + if ($pos === false) { + return false; + } + $filePath = substr($pathVersion, 0, $pos); + $versionId = substr($pathVersion, 2 + $pos - strlen($pathVersion)); + return [$filePath, $versionId]; + } + + /** + * Check if folder is not exist + * + * @param string $userId - user id + * @param string $path - folder path + * @param bool $createIfNotExist - create folder if not exist + * + * @return bool + */ + private static function checkFolderExist($view, $path, $createIfNotExist = false) { + if ($view->is_dir($path)) { + return true; + } + if (!$createIfNotExist) { + return false; + } + $view->mkdir($path); + return true; + } + + /** + * Get view and path for changes + * + * @param string $user - user id + * @param string $fileId - file id + * @param bool $createIfNotExist - create folder if not exist + * + * @return array + */ + private static function getView($userId, $fileId, $createIfNotExist = false) { + $view = new View("/" . $userId); + + $path = self::$appName; + if (!self::checkFolderExist($view, $path, $createIfNotExist)) { + return [null, null]; + } + + if ($fileId === null) { + return [$view, $path]; + } + + $path = $path . "/" . $fileId; + if (!self::checkFolderExist($view, $path, $createIfNotExist)) { + return [null, null]; + } + + return [$view, $path]; + } + + /** + * Get changes from stored to history object + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * @param string $prevVersion - previous version for check + * + * @return array + */ + public static function getHistoryData($ownerId, $fileId, $versionId, $prevVersion) { + $logger = \OC::$server->getLogger(); + + if ($ownerId === null || $fileId === null) { + return null; + } + + list ($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $historyPath = $path . "/" . $versionId . self::$historyExt; + if (!$view->file_exists($historyPath)) { + return null; + } + + $historyDataString = $view->file_get_contents($historyPath); + + try { + $historyData = json_decode($historyDataString, true); + + if ($historyData["prev"] !== $prevVersion) { + $logger->debug("getHistoryData: previous $prevVersion != " . $historyData["prev"], ["app" => self::$appName]); + + $view->unlink($historyPath); + $logger->debug("getHistoryData: delete $historyPath", ["app" => self::$appName]); + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if ($view->file_exists($changesPath)) { + $view->unlink($changesPath); + $logger->debug("getHistoryData: delete $changesPath", ["app" => self::$appName]); + } + return null; + } + + return $historyData; + } catch (\Exception $e) { + $logger->logException($e, ["message" => "getHistoryData: $fileId $versionId", "app" => self::$appName]); + return null; + } + } + + /** + * Check if changes is stored + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return bool + */ + public static function hasChanges($ownerId, $fileId, $versionId) { + if ($ownerId === null || $fileId === null) { + return false; + } + + list ($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return false; + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + return $view->file_exists($changesPath); + } + + /** + * Get changes file + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + * + * @return File + */ + public static function getChangesFile($ownerId, $fileId, $versionId) { + if ($ownerId === null || $fileId === null) { + return null; + } + + list ($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if (!$view->file_exists($changesPath)) { + return null; + } + + $changesInfo = $view->getFileInfo($changesPath); + $changes = new File($view->getRoot(), $view, $changesPath, $changesInfo); + + \OC::$server->getLogger()->debug("getChangesFile: $fileId for $ownerId get changes $changesPath", ["app" => self::$appName]); + + return $changes; + } + + /** + * Save history to storage + * + * @param OCP\Files\FileInfo $fileInfo - file info + * @param array $history - file history + * @param string $changes - file changes + * @param string $prevVersion - previous version for check + */ + public static function saveHistory($fileInfo, $history, $changes, $prevVersion) { + $logger = \OC::$server->getLogger(); + + $owner = $fileInfo->getOwner(); + + if ($owner === null) { + return; + } + if (empty($history) || empty($changes)) { + return; + } + + $ownerId = $owner->getUID(); + $fileId = $fileInfo->getId(); + $versionId = $fileInfo->getMtime(); + + list ($view, $path) = self::getView($ownerId, $fileId, true); + + try { + $changesPath = $path . "/" . $versionId . self::$changesExt; + $view->touch($changesPath); + $view->file_put_contents($changesPath, $changes); + + $history["prev"] = $prevVersion; + $historyPath = $path . "/" . $versionId . self::$historyExt; + $view->touch($historyPath); + $view->file_put_contents($historyPath, json_encode($history)); + + $logger->debug("saveHistory: $fileId for $ownerId stored changes $changesPath history $historyPath", ["app" => self::$appName]); + } catch (\Exception $e) { + $logger->logException($e, ["message" => "saveHistory: save $fileId history error", "app" => self::$appName]); + } + } + + /** + * Delete all versions of file + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + */ + public static function deleteAllVersions($ownerId, $fileId = null) { + $logger = \OC::$server->getLogger(); + + $logger->debug("deleteAllVersions $ownerId $fileId", ["app" => self::$appName]); + + if ($ownerId === null) { + return; + } + + list ($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return; + } + + $view->unlink($path); + } + + /** + * Delete changes and history + * + * @param string $ownerId - file owner id + * @param string $fileId - file id + * @param string $versionId - file version + */ + public static function deleteVersion($ownerId, $fileId, $versionId) { + $logger = \OC::$server->getLogger(); + + $logger->debug("deleteVersion $fileId ($versionId)", ["app" => self::$appName]); + + if ($ownerId === null) { + return; + } + if ($fileId === null || empty($versionId)) { + return; + } + + list ($view, $path) = self::getView($ownerId, $fileId); + if ($view === null) { + return null; + } + + $historyPath = $path . "/" . $versionId . self::$historyExt; + if ($view->file_exists($historyPath)) { + $view->unlink($historyPath); + $logger->debug("deleteVersion $historyPath", ["app" => self::$appName]); + } + + $changesPath = $path . "/" . $versionId . self::$changesExt; + if ($view->file_exists($changesPath)) { + $view->unlink($changesPath); + $logger->debug("deleteVersion $changesPath", ["app" => self::$appName]); + } + } +} diff --git a/lib/hooks.php b/lib/hooks.php new file mode 100644 index 0000000..d8d89d1 --- /dev/null +++ b/lib/hooks.php @@ -0,0 +1,152 @@ +<?php +/** + * + * (c) Copyright Ascensio System SIA 2020 + * + * 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. + * In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * 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: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions of the Program + * must display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product logo when distributing the program. + * Pursuant to Section 7(e) we decline to grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as well as technical + * writing content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International. + * See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +namespace OCA\Onlyoffice; + +use OC\Files\Filesystem; + +use OCP\Util; + +use OCA\Onlyoffice\FileVersions; + +/** + * The class to handle the filesystem hooks + * + * @package OCA\Onlyoffice + */ +class Hooks { + + /** + * Application name + * + * @var string + */ + private static $appName = "onlyoffice"; + + public static function connectHooks() { + // Listen user deletion + Util::connectHook("OC_User", "pre_deleteUser", Hooks::class, "userDelete"); + + // Listen file deletion + Util::connectHook("OC_Filesystem", "delete", Hooks::class, "fileDelete"); + + // Listen file version deletion + Util::connectHook("\OCP\Versions", "preDelete", Hooks::class, "fileVersionDelete"); + + // Listen file version restore + Util::connectHook("\OCP\Versions", "rollback", Hooks::class, "fileVersionRestore"); + } + + /** + * Erase user file versions + * + * @param array $params - hook params + */ + public static function userDelete($params) { + $userId = $params["uid"]; + + FileVersions::deleteAllVersions($userId); + } + + /** + * Erase versions of deleted file + * + * @param array $params - hook params + */ + public static function fileDelete($params) { + $filePath = $params[Filesystem::signal_param_path]; + if (empty($filePath)) { + return; + } + + try { + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + $fileId = $fileInfo->getId(); + + FileVersions::deleteAllVersions($ownerId, $fileId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileDelete " . json_encode($params), "app" => self::$appName]); + } + } + + /** + * Erase versions of deleted version of file + * + * @param array $params - hook param + */ + public static function fileVersionDelete($params) { + $pathVersion = $params["path"]; + if (empty($pathVersion)) { + return; + } + + try { + list ($filePath, $versionId) = FileVersions::splitPathVersion($pathVersion); + if (empty($filePath)) { + return; + } + + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + $fileId = $fileInfo->getId(); + + FileVersions::deleteVersion($ownerId, $fileId, $versionId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionDelete " . json_encode($params), "app" => self::$appName]); + } + } + + /** + * Erase versions of restored version of file + * + * @param array $params - hook param + */ + public static function fileVersionRestore($params) { + $filePath = $params["path"]; + if (empty($filePath)) { + return; + } + + $versionId = $params["revision"]; + + try { + $ownerId = Filesystem::getOwner($filePath); + + $fileInfo = Filesystem::getFileInfo($filePath); + $fileId = $fileInfo->getId(); + + FileVersions::deleteVersion($ownerId, $fileId, $versionId); + } catch (\Exception $e) { + \OC::$server->getLogger()->logException($e, ["message" => "Hook: fileVersionRestore " . json_encode($params), "app" => self::$appName]); + } + } +} diff --git a/templates/editor.php b/templates/editor.php index 381c316..c6d17af 100644 --- a/templates/editor.php +++ b/templates/editor.php @@ -42,6 +42,7 @@ data-path="<?php p($_["filePath"]) ?>" data-sharetoken="<?php p($_["shareToken"]) ?>" data-directtoken="<?php p($_["directToken"]) ?>" + data-version="<?php p($_["version"]) ?>" data-inframe="<?php p($_["inframe"]) ?>"></div> <?php if (!empty($_["documentServerUrl"])) { ?> |