diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index dcff792ea42..dd4f029eb45 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -31,6 +31,7 @@ 'OCA\\Text\\Event\\LoadEditor' => $baseDir . '/../lib/Event/LoadEditor.php', 'OCA\\Text\\Exception\\DocumentHasUnsavedChangesException' => $baseDir . '/../lib/Exception/DocumentHasUnsavedChangesException.php', 'OCA\\Text\\Exception\\DocumentSaveConflictException' => $baseDir . '/../lib/Exception/DocumentSaveConflictException.php', + 'OCA\\Text\\Exception\\InvalidDocumentBaseVersionEtagException' => $baseDir . '/../lib/Exception/InvalidDocumentBaseVersionEtagException.php', 'OCA\\Text\\Exception\\InvalidSessionException' => $baseDir . '/../lib/Exception/InvalidSessionException.php', 'OCA\\Text\\Exception\\UploadException' => $baseDir . '/../lib/Exception/UploadException.php', 'OCA\\Text\\Exception\\VersionMismatchException' => $baseDir . '/../lib/Exception/VersionMismatchException.php', @@ -45,6 +46,7 @@ 'OCA\\Text\\Listeners\\LoadViewerListener' => $baseDir . '/../lib/Listeners/LoadViewerListener.php', 'OCA\\Text\\Listeners\\NodeCopiedListener' => $baseDir . '/../lib/Listeners/NodeCopiedListener.php', 'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => $baseDir . '/../lib/Listeners/RegisterDirectEditorEventListener.php', + 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentBaseVersionEtag' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentBaseVersionEtag.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSession.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php', 'OCA\\Text\\Middleware\\SessionMiddleware' => $baseDir . '/../lib/Middleware/SessionMiddleware.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index a172981b0ef..aac06275a14 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -46,6 +46,7 @@ class ComposerStaticInitText 'OCA\\Text\\Event\\LoadEditor' => __DIR__ . '/..' . '/../lib/Event/LoadEditor.php', 'OCA\\Text\\Exception\\DocumentHasUnsavedChangesException' => __DIR__ . '/..' . '/../lib/Exception/DocumentHasUnsavedChangesException.php', 'OCA\\Text\\Exception\\DocumentSaveConflictException' => __DIR__ . '/..' . '/../lib/Exception/DocumentSaveConflictException.php', + 'OCA\\Text\\Exception\\InvalidDocumentBaseVersionEtagException' => __DIR__ . '/..' . '/../lib/Exception/InvalidDocumentBaseVersionEtagException.php', 'OCA\\Text\\Exception\\InvalidSessionException' => __DIR__ . '/..' . '/../lib/Exception/InvalidSessionException.php', 'OCA\\Text\\Exception\\UploadException' => __DIR__ . '/..' . '/../lib/Exception/UploadException.php', 'OCA\\Text\\Exception\\VersionMismatchException' => __DIR__ . '/..' . '/../lib/Exception/VersionMismatchException.php', @@ -60,6 +61,7 @@ class ComposerStaticInitText 'OCA\\Text\\Listeners\\LoadViewerListener' => __DIR__ . '/..' . '/../lib/Listeners/LoadViewerListener.php', 'OCA\\Text\\Listeners\\NodeCopiedListener' => __DIR__ . '/..' . '/../lib/Listeners/NodeCopiedListener.php', 'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => __DIR__ . '/..' . '/../lib/Listeners/RegisterDirectEditorEventListener.php', + 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentBaseVersionEtag' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentBaseVersionEtag.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSession.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php', 'OCA\\Text\\Middleware\\SessionMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SessionMiddleware.php', diff --git a/lib/Controller/PublicSessionController.php b/lib/Controller/PublicSessionController.php index be38220f2ff..9d6177584ed 100644 --- a/lib/Controller/PublicSessionController.php +++ b/lib/Controller/PublicSessionController.php @@ -25,6 +25,7 @@ namespace OCA\Text\Controller; +use OCA\Text\Middleware\Attribute\RequireDocumentBaseVersionEtag; use OCA\Text\Middleware\Attribute\RequireDocumentSession; use OCA\Text\Service\ApiService; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -92,6 +93,7 @@ public function close(int $documentId, int $sessionId, string $sessionToken): Da #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function push(int $documentId, int $sessionId, string $sessionToken, int $version, array $steps, string $awareness, string $token): DataResponse { return $this->apiService->push($this->getSession(), $this->getDocument(), $version, $steps, $awareness, $token); @@ -99,6 +101,7 @@ public function push(int $documentId, int $sessionId, string $sessionToken, int #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function sync(string $token, int $version = 0): DataResponse { return $this->apiService->sync($this->getSession(), $this->getDocument(), $version, $token); @@ -106,6 +109,7 @@ public function sync(string $token, int $version = 0): DataResponse { #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function save(string $token, int $version = 0, ?string $autosaveContent = null, ?string $documentState = null, bool $force = false, bool $manualSave = false): DataResponse { return $this->apiService->save($this->getSession(), $this->getDocument(), $version, $autosaveContent, $documentState, $force, $manualSave, $token); diff --git a/lib/Controller/SessionController.php b/lib/Controller/SessionController.php index 09b056da07d..ec5daf5edbf 100644 --- a/lib/Controller/SessionController.php +++ b/lib/Controller/SessionController.php @@ -26,6 +26,7 @@ namespace OCA\Text\Controller; use OCA\Text\Middleware\Attribute\RequireDocumentSession; +use OCA\Text\Middleware\Attribute\RequireDocumentBaseVersionEtag; use OCA\Text\Service\ApiService; use OCA\Text\Service\NotificationService; use OCA\Text\Service\SessionService; @@ -69,6 +70,7 @@ public function close(int $documentId, int $sessionId, string $sessionToken): Da #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function push(int $version, array $steps, string $awareness): DataResponse { try { @@ -81,6 +83,7 @@ public function push(int $version, array $steps, string $awareness): DataRespons #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function sync(int $version = 0): DataResponse { try { @@ -93,6 +96,7 @@ public function sync(int $version = 0): DataResponse { #[NoAdminRequired] #[PublicPage] + #[RequireDocumentBaseVersionEtag] #[RequireDocumentSession] public function save(int $version = 0, ?string $autosaveContent = null, ?string $documentState = null, bool $force = false, bool $manualSave = false): DataResponse { try { diff --git a/lib/Exception/InvalidDocumentBaseVersionEtagException.php b/lib/Exception/InvalidDocumentBaseVersionEtagException.php new file mode 100644 index 00000000000..1bb13ca65a2 --- /dev/null +++ b/lib/Exception/InvalidDocumentBaseVersionEtagException.php @@ -0,0 +1,9 @@ +getAttributes(RequireDocumentBaseVersionEtag::class))) { + $this->assertDocumentBaseVersionEtag(); + } + if (!empty($reflectionMethod->getAttributes(RequireDocumentSession::class))) { $this->assertDocumentSession($controller); } } + /** + * @throws InvalidDocumentBaseVersionEtagException + */ + private function assertDocumentBaseVersionEtag(): void { + $documentId = (int)$this->request->getParam('documentId'); + $baseVersionEtag = $this->request->getParam('baseVersionEtag'); + + $document = $this->documentService->getDocument($documentId); + if ($baseVersionEtag && $document?->getBaseVersionEtag() !== $baseVersionEtag) { + throw new InvalidDocumentBaseVersionEtagException(); + } + } + /** * @throws InvalidSessionException */ @@ -118,9 +141,13 @@ private function assertUserOrShareToken(ISessionAwareController $controller): vo $controller->setDocument($document); } - public function afterException($controller, $methodName, \Exception $exception): DataResponse|Response { + public function afterException($controller, $methodName, \Exception $exception): JSONResponse|Response { + if ($exception instanceof InvalidDocumentBaseVersionEtagException) { + return new JSONResponse($this->l10n->t('Editing session has expired. Please reload the page.'), Http::STATUS_PRECONDITION_FAILED); + } + if ($exception instanceof InvalidSessionException) { - return new DataResponse([], 403); + return new JSONResponse([], Http::STATUS_CONFLICT); } return parent::afterException($controller, $methodName, $exception); diff --git a/lib/Service/ApiService.php b/lib/Service/ApiService.php index 95dc3310215..1be3bf51d97 100644 --- a/lib/Service/ApiService.php +++ b/lib/Service/ApiService.php @@ -116,7 +116,7 @@ public function create(?int $fileId = null, ?string $filePath = null, ?string $b $document = $this->documentService->getDocument($file->getId()); $freshSession = $document === null; if ($baseVersionEtag && $baseVersionEtag !== $document?->getBaseVersionEtag()) { - return new DataResponse($this->l10n->t('Editing session has expired. Please reload the page.'), Http::STATUS_CONFLICT); + return new DataResponse($this->l10n->t('Editing session has expired. Please reload the page.'), Http::STATUS_PRECONDITION_FAILED); } if ($freshSession) { @@ -193,7 +193,7 @@ public function push(Session $session, Document $document, int $version, array $ $session = $this->sessionService->updateSessionAwareness($session, $awareness); } catch (DoesNotExistException $e) { // Session was removed in the meantime. #3875 - return new DataResponse([], 403); + return new DataResponse($this->l10n->t('Editing session has expired. Please reload the page.'), Http::STATUS_PRECONDITION_FAILED); } if (empty($steps)) { return new DataResponse([]); @@ -204,7 +204,7 @@ public function push(Session $session, Document $document, int $version, array $ return new DataResponse($e->getMessage(), 422); } catch (DoesNotExistException|NotPermittedException) { // Either no write access or session was removed in the meantime (#3875). - return new DataResponse([], 403); + return new DataResponse($this->l10n->t('Editing session has expired. Please reload the page.'), Http::STATUS_PRECONDITION_FAILED); } return new DataResponse($result); } diff --git a/src/services/PollingBackend.js b/src/services/PollingBackend.js index 39595db05cd..3cbb3537f5e 100644 --- a/src/services/PollingBackend.js +++ b/src/services/PollingBackend.js @@ -170,6 +170,9 @@ class PollingBackend { outsideChange: e.response.data.outsideChange, }, }) + } else if (e.response.status === 412) { + this.#syncService.emit('error', { type: ERROR_TYPE.LOAD_ERROR, data: e.response }) + this.disconnect() } else if (e.response.status === 403) { this.#syncService.emit('error', { type: ERROR_TYPE.SOURCE_NOT_FOUND, data: {} }) this.disconnect() diff --git a/src/services/SessionApi.js b/src/services/SessionApi.js index 96bf299ad0a..c8f49e0a77b 100644 --- a/src/services/SessionApi.js +++ b/src/services/SessionApi.js @@ -114,6 +114,7 @@ export class Connection { return this.#post(this.#url(`session/${this.#document.id}/sync`), { ...this.#defaultParams, filePath: this.#options.filePath, + baseVersionEtag: this.#document.baseVersionEtag, version, }) } @@ -122,6 +123,7 @@ export class Connection { return this.#post(this.#url(`session/${this.#document.id}/save`), { ...this.#defaultParams, filePath: this.#options.filePath, + baseVersionEtag: this.#document.baseVersionEtag, version, autosaveContent, documentState, @@ -134,6 +136,7 @@ export class Connection { return this.#post(this.#url(`session/${this.#document.id}/push`), { ...this.#defaultParams, filePath: this.#options.filePath, + baseVersionEtag: this.#document.baseVersionEtag, steps, version, awareness, diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 3c753b3624b..0bdd707ca25 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -179,7 +179,9 @@ class SyncService { if (!response || code === 'ECONNABORTED') { this.emit('error', { type: ERROR_TYPE.CONNECTION_FAILED, data: {} }) } - if (response?.status === 403) { + if (response?.status === 412) { + this.emit('error', { type: ERROR_TYPE.LOAD_ERROR, data: response }) + } else if (response?.status === 403) { if (!data.document) { // either the session is invalid or the document is read only. logger.error('failed to write to document - not allowed')