Skip to content

Commit dd10927

Browse files
authored
Merge pull request #5523 from nextcloud/fix/baseversionetag_change
fix(sync): If `baseVersionEtag` changed, reset frontend
2 parents 4381329 + ec4dc16 commit dd10927

File tree

18 files changed

+210
-76
lines changed

18 files changed

+210
-76
lines changed

composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
'OCA\\Text\\Event\\LoadEditor' => $baseDir . '/../lib/Event/LoadEditor.php',
3232
'OCA\\Text\\Exception\\DocumentHasUnsavedChangesException' => $baseDir . '/../lib/Exception/DocumentHasUnsavedChangesException.php',
3333
'OCA\\Text\\Exception\\DocumentSaveConflictException' => $baseDir . '/../lib/Exception/DocumentSaveConflictException.php',
34+
'OCA\\Text\\Exception\\InvalidDocumentBaseVersionEtagException' => $baseDir . '/../lib/Exception/InvalidDocumentBaseVersionEtagException.php',
3435
'OCA\\Text\\Exception\\InvalidSessionException' => $baseDir . '/../lib/Exception/InvalidSessionException.php',
3536
'OCA\\Text\\Exception\\UploadException' => $baseDir . '/../lib/Exception/UploadException.php',
3637
'OCA\\Text\\Exception\\VersionMismatchException' => $baseDir . '/../lib/Exception/VersionMismatchException.php',
@@ -45,6 +46,7 @@
4546
'OCA\\Text\\Listeners\\LoadViewerListener' => $baseDir . '/../lib/Listeners/LoadViewerListener.php',
4647
'OCA\\Text\\Listeners\\NodeCopiedListener' => $baseDir . '/../lib/Listeners/NodeCopiedListener.php',
4748
'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => $baseDir . '/../lib/Listeners/RegisterDirectEditorEventListener.php',
49+
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentBaseVersionEtag' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentBaseVersionEtag.php',
4850
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSession.php',
4951
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php',
5052
'OCA\\Text\\Middleware\\SessionMiddleware' => $baseDir . '/../lib/Middleware/SessionMiddleware.php',

composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ComposerStaticInitText
4646
'OCA\\Text\\Event\\LoadEditor' => __DIR__ . '/..' . '/../lib/Event/LoadEditor.php',
4747
'OCA\\Text\\Exception\\DocumentHasUnsavedChangesException' => __DIR__ . '/..' . '/../lib/Exception/DocumentHasUnsavedChangesException.php',
4848
'OCA\\Text\\Exception\\DocumentSaveConflictException' => __DIR__ . '/..' . '/../lib/Exception/DocumentSaveConflictException.php',
49+
'OCA\\Text\\Exception\\InvalidDocumentBaseVersionEtagException' => __DIR__ . '/..' . '/../lib/Exception/InvalidDocumentBaseVersionEtagException.php',
4950
'OCA\\Text\\Exception\\InvalidSessionException' => __DIR__ . '/..' . '/../lib/Exception/InvalidSessionException.php',
5051
'OCA\\Text\\Exception\\UploadException' => __DIR__ . '/..' . '/../lib/Exception/UploadException.php',
5152
'OCA\\Text\\Exception\\VersionMismatchException' => __DIR__ . '/..' . '/../lib/Exception/VersionMismatchException.php',
@@ -60,6 +61,7 @@ class ComposerStaticInitText
6061
'OCA\\Text\\Listeners\\LoadViewerListener' => __DIR__ . '/..' . '/../lib/Listeners/LoadViewerListener.php',
6162
'OCA\\Text\\Listeners\\NodeCopiedListener' => __DIR__ . '/..' . '/../lib/Listeners/NodeCopiedListener.php',
6263
'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => __DIR__ . '/..' . '/../lib/Listeners/RegisterDirectEditorEventListener.php',
64+
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentBaseVersionEtag' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentBaseVersionEtag.php',
6365
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSession.php',
6466
'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php',
6567
'OCA\\Text\\Middleware\\SessionMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SessionMiddleware.php',

cypress/e2e/api/SessionApi.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,28 @@ describe('The session Api', function() {
344344
.then(() => connection.close())
345345
})
346346

347+
it('refuses create,push,sync,save with non-matching baseVersionEtag', function() {
348+
cy.failToCreateTextSession(undefined, 'wrongBaseVersionEtag', { filePath: '', shareToken })
349+
.its('status')
350+
.should('eql', 412)
351+
352+
connection.setBaseVersionEtag('wrongBaseVersionEtag')
353+
354+
cy.failToPushSteps({ connection, steps: [messages.update], version })
355+
.its('status')
356+
.should('equal', 412)
357+
358+
cy.failToSyncSteps(connection, { version: 0 })
359+
.its('status')
360+
.should('equal', 412)
361+
362+
cy.failToSave(connection)
363+
.its('status')
364+
.should('equal', 412)
365+
366+
cy.then(() => connection.close())
367+
})
368+
347369
it('recovers session even if last person leaves right after create', function() {
348370
let joining
349371
cy.log('Initial user pushes steps')

cypress/e2e/conflict.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ variants.forEach(function({ fixture, mime }) {
5454
cy.get('#viewer .modal-header button.header-close').click()
5555
cy.get('#viewer').should('not.exist')
5656
cy.openFile(fileName)
57-
cy.get('.text-editor .document-status .icon-error')
57+
cy.get('.text-editor .document-status')
58+
.should('contain', 'Document has been changed outside of the editor.')
5859
getWrapper()
5960
.find('#read-only-editor')
6061
.should('contain', 'Hello world')

cypress/e2e/share.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ describe('Open test.md in viewer', function() {
151151
cy.login(recipient)
152152
cy.visit('/apps/files')
153153
cy.openFile('test.md')
154-
cy.getModal().find('.empty-content__name').should('contain', 'Failed to load file')
154+
cy.getModal().find('.document-status').should('contain', 'This file cannot be displayed as download is disabled by the share')
155155
cy.getModal().getContent().should('not.exist')
156156
})
157157
})

cypress/e2e/sync.spec.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe('Sync', () => {
7474
}).as('sessionRequests')
7575
cy.wait('@dead', { timeout: 30000 })
7676
cy.get('#editor-container .document-status', { timeout: 30000 })
77-
.should('contain', 'File could not be loaded')
77+
.should('contain', 'Document could not be loaded.')
7878
.then(() => {
7979
reconnect = true
8080
})
@@ -83,7 +83,7 @@ describe('Sync', () => {
8383
.as('syncAfterRecovery')
8484
cy.wait('@syncAfterRecovery', { timeout: 30000 })
8585
cy.get('#editor-container .document-status', { timeout: 30000 })
86-
.should('not.contain', 'File could not be loaded')
86+
.should('not.contain', 'Document could not be loaded.')
8787
// FIXME: There seems to be a bug where typed words maybe lost if not waiting for the new session
8888
cy.wait('@syncAfterRecovery', { timeout: 10000 })
8989
cy.getContent().type('* more content added after the lost connection{enter}')
@@ -109,12 +109,12 @@ describe('Sync', () => {
109109

110110
cy.wait('@sessionRequests', { timeout: 30000 })
111111
cy.get('#editor-container .document-status', { timeout: 30000 })
112-
.should('contain', 'File could not be loaded')
112+
.should('contain', 'Document could not be loaded.')
113113

114114
cy.wait('@syncAfterRecovery', { timeout: 60000 })
115115

116116
cy.get('#editor-container .document-status', { timeout: 30000 })
117-
.should('not.contain', 'File could not be loaded')
117+
.should('not.contain', 'Document could not be loaded.')
118118
// FIXME: There seems to be a bug where typed words maybe lost if not waiting for the new session
119119
cy.wait('@syncAfterRecovery', { timeout: 10000 })
120120
cy.getContent().type('* more content added after the lost connection{enter}')
@@ -126,6 +126,25 @@ describe('Sync', () => {
126126
.should('include', 'after the lost connection')
127127
})
128128

129+
it('shows warning when document session got cleaned up', () => {
130+
cy.get('.save-status button')
131+
.click()
132+
cy.wait('@save')
133+
cy.uploadTestFile('test.md')
134+
135+
cy.get('#editor-container .document-status', { timeout: 30000 })
136+
.should('contain', 'Editing session has expired.')
137+
138+
// Reload button works
139+
cy.get('#editor-container .document-status a.button')
140+
.contains('Reload')
141+
.click()
142+
143+
cy.getContent()
144+
cy.get('#editor-container .document-status .notecard')
145+
.should('not.exist')
146+
})
147+
129148
it('passes the doc content from one session to the next', () => {
130149
cy.closeFile()
131150
cy.intercept({ method: 'PUT', url: '**/apps/text/session/*/create' })

cypress/support/sessions.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,50 @@ Cypress.Commands.add('createTextSession', (fileId, options = {}) => {
3636
return api.open({ fileId })
3737
})
3838

39-
Cypress.Commands.add('failToCreateTextSession', (fileId) => {
40-
const api = new SessionApi()
41-
return api.open({ fileId })
39+
Cypress.Commands.add('failToCreateTextSession', (fileId, baseVersionEtag = null, options = {}) => {
40+
const api = new SessionApi(options)
41+
return api.open({ fileId, baseVersionEtag })
4242
.then((response) => {
4343
throw new Error('Expected request to fail - but it succeeded!')
44-
})
45-
.catch((err) => err.response)
44+
}, (err) => err.response)
4645
})
4746

4847
Cypress.Commands.add('pushSteps', ({ connection, steps, version, awareness = '' }) => {
4948
return connection.push({ steps, version, awareness })
5049
.then(response => response.data)
5150
})
5251

52+
Cypress.Commands.add('failToPushSteps', ({ connection, steps, version, awareness = '' }) => {
53+
return connection.push({ steps, version, awareness })
54+
.then((response) => {
55+
throw new Error('Expected request to fail - but it succeeded!')
56+
}, (err) => err.response)
57+
})
58+
5359
Cypress.Commands.add('syncSteps', (connection, options = { version: 0 }) => {
5460
return connection.sync(options)
5561
.then(response => response.data)
5662
})
5763

64+
Cypress.Commands.add('failToSyncSteps', (connection, options = { version: 0 }) => {
65+
return connection.sync(options)
66+
.then((response) => {
67+
throw new Error('Expected request to fail - but it succeeded!')
68+
}, (err) => err.response)
69+
})
70+
5871
Cypress.Commands.add('save', (connection, options = { version: 0 }) => {
5972
return connection.save(options)
6073
.then(response => response.data)
6174
})
6275

76+
Cypress.Commands.add('failToSave', (connection, options = { version: 0 }) => {
77+
return connection.save(options)
78+
.then((response) => {
79+
throw new Error('Expected request to fail - but it succeeded!')
80+
}, (err) => err.response)
81+
})
82+
6383
// Used to test for race conditions between the last push and the close request
6484
Cypress.Commands.add('pushAndClose', ({ connection, steps, version, awareness = '' }) => {
6585
cy.log('Race between push and close')

lib/Controller/PublicSessionController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
namespace OCA\Text\Controller;
2727

28+
use OCA\Text\Middleware\Attribute\RequireDocumentBaseVersionEtag;
2829
use OCA\Text\Middleware\Attribute\RequireDocumentSession;
2930
use OCA\Text\Service\ApiService;
3031
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
@@ -80,8 +81,8 @@ protected function isPasswordProtected(): bool {
8081

8182
#[NoAdminRequired]
8283
#[PublicPage]
83-
public function create(string $token, ?string $file = null, ?string $guestName = null): DataResponse {
84-
return $this->apiService->create(null, $file, $token, $guestName);
84+
public function create(string $token, ?string $file = null, ?string $baseVersionEtag = null, ?string $guestName = null): DataResponse {
85+
return $this->apiService->create(null, $file, $baseVersionEtag, $token, $guestName);
8586
}
8687

8788
#[NoAdminRequired]
@@ -92,20 +93,23 @@ public function close(int $documentId, int $sessionId, string $sessionToken): Da
9293

9394
#[NoAdminRequired]
9495
#[PublicPage]
96+
#[RequireDocumentBaseVersionEtag]
9597
#[RequireDocumentSession]
9698
public function push(int $documentId, int $sessionId, string $sessionToken, int $version, array $steps, string $awareness, string $token): DataResponse {
9799
return $this->apiService->push($this->getSession(), $this->getDocument(), $version, $steps, $awareness, $token);
98100
}
99101

100102
#[NoAdminRequired]
101103
#[PublicPage]
104+
#[RequireDocumentBaseVersionEtag]
102105
#[RequireDocumentSession]
103106
public function sync(string $token, int $version = 0): DataResponse {
104107
return $this->apiService->sync($this->getSession(), $this->getDocument(), $version, $token);
105108
}
106109

107110
#[NoAdminRequired]
108111
#[PublicPage]
112+
#[RequireDocumentBaseVersionEtag]
109113
#[RequireDocumentSession]
110114
public function save(string $token, int $version = 0, ?string $autosaveContent = null, ?string $documentState = null, bool $force = false, bool $manualSave = false): DataResponse {
111115
return $this->apiService->save($this->getSession(), $this->getDocument(), $version, $autosaveContent, $documentState, $force, $manualSave, $token);

lib/Controller/SessionController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
namespace OCA\Text\Controller;
2727

28+
use OCA\Text\Middleware\Attribute\RequireDocumentBaseVersionEtag;
2829
use OCA\Text\Middleware\Attribute\RequireDocumentSession;
2930
use OCA\Text\Service\ApiService;
3031
use OCA\Text\Service\NotificationService;
@@ -57,8 +58,8 @@ public function __construct(
5758
}
5859

5960
#[NoAdminRequired]
60-
public function create(?int $fileId = null, ?string $file = null): DataResponse {
61-
return $this->apiService->create($fileId, $file, null, null);
61+
public function create(?int $fileId = null, ?string $file = null, ?string $baseVersionEtag = null): DataResponse {
62+
return $this->apiService->create($fileId, $file, $baseVersionEtag, null, null);
6263
}
6364

6465
#[NoAdminRequired]
@@ -69,6 +70,7 @@ public function close(int $documentId, int $sessionId, string $sessionToken): Da
6970

7071
#[NoAdminRequired]
7172
#[PublicPage]
73+
#[RequireDocumentBaseVersionEtag]
7274
#[RequireDocumentSession]
7375
public function push(int $version, array $steps, string $awareness): DataResponse {
7476
try {
@@ -81,6 +83,7 @@ public function push(int $version, array $steps, string $awareness): DataRespons
8183

8284
#[NoAdminRequired]
8385
#[PublicPage]
86+
#[RequireDocumentBaseVersionEtag]
8487
#[RequireDocumentSession]
8588
public function sync(int $version = 0): DataResponse {
8689
try {
@@ -93,6 +96,7 @@ public function sync(int $version = 0): DataResponse {
9396

9497
#[NoAdminRequired]
9598
#[PublicPage]
99+
#[RequireDocumentBaseVersionEtag]
96100
#[RequireDocumentSession]
97101
public function save(int $version = 0, ?string $autosaveContent = null, ?string $documentState = null, bool $force = false, bool $manualSave = false): DataResponse {
98102
try {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\Text\Exception;
6+
7+
class InvalidDocumentBaseVersionEtagException extends \Exception {
8+
9+
}

0 commit comments

Comments
 (0)