Skip to content

Commit 240854c

Browse files
committed
manual update for missing files
Signed-off-by: samin-z <samin.zavarkesh@gmail.com>
1 parent c142d3a commit 240854c

5 files changed

Lines changed: 83 additions & 25 deletions

File tree

lib/Service/ApiService.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,11 @@ public function push(Session $session, Document $document, int $version, array $
188188
$this->addToPushQueue($document, [$awareness, ...array_values($steps)]);
189189
} catch (InvalidArgumentException $e) {
190190
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_UNPROCESSABLE_ENTITY);
191-
} catch (DoesNotExistException|NotPermittedException) {
191+
} catch (DoesNotExistException) {
192192
// Either no write access or session was removed in the meantime (#3875).
193193
return new DataResponse(['error' => $this->l10n->t('Editing session has expired. Please reload the page.')], Http::STATUS_PRECONDITION_FAILED);
194+
} catch (NotPermittedException) {
195+
return new DataResponse(['error' => $this->l10n->t('This document is read-only.')], Http::STATUS_FORBIDDEN);
194196
}
195197
return new DataResponse($result);
196198
}
@@ -228,6 +230,7 @@ public function sync(Session $session, Document $document, int $version = 0, ?st
228230

229231
// ensure file is still present and accessible
230232
$file = $this->documentService->getFileForSession($session, $shareToken);
233+
$result['readOnly'] = $this->documentService->isReadOnly($file, $shareToken);
231234
$this->documentService->assertNoOutsideConflict($document, $file);
232235
} catch (NotPermittedException|NotFoundException|InvalidPathException $e) {
233236
$this->logger->info($e->getMessage(), ['exception' => $e]);
@@ -275,6 +278,10 @@ public function save(Session $session, Document $document, int $version = 0, ?st
275278
} catch (LockedException) {
276279
// Ignore locked exception since it might happen due to an autosave action happening at the same time
277280
}
281+
} catch (NotPermittedException) {
282+
return new DataResponse([
283+
'error' => $this->l10n->t('Read-only permission cannot save document changes. Please reload the page.')
284+
], Http::STATUS_FORBIDDEN);
278285
} catch (NotFoundException) {
279286
return new DataResponse([], Http::STATUS_NOT_FOUND);
280287
} catch (Exception $e) {

lib/Service/DocumentService.php

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ public function writeDocumentState(int $documentId, string $content): void {
207207
*/
208208
public function addStep(Document $document, Session $session, array $steps, int $version, ?int $recoveryAttempt, ?string $shareToken): array {
209209
$documentId = $session->getDocumentId();
210-
$readOnly = $this->isReadOnlyCached($session, $shareToken);
210+
$file = $this->getFileForSession($session, $shareToken);
211+
$readOnly = $this->isReadOnly($file, $shareToken);
211212
$stepsToInsert = [];
212213
$stepsIncludeQuery = false;
213214
$documentState = null;
@@ -372,7 +373,7 @@ public function autosave(Document $document, ?File $file, int $version, ?string
372373
}
373374

374375
if ($this->isReadOnly($file, $shareToken)) {
375-
return $document;
376+
throw new NotPermittedException('Read-only permission cannot save document changes. Please reload the page.');
376377
}
377378

378379
$this->assertNoOutsideConflict($document, $file, $force);
@@ -590,30 +591,14 @@ public function getFileByShareToken(string $shareToken, ?string $path = null): F
590591
}
591592
throw new \InvalidArgumentException('No proper share data');
592593
}
593-
594-
public function isReadOnlyCached(Session $session, ?string $shareToken = null): bool {
595-
$cacheKey = 'read-only-' . $session->getId();
596-
$isReadOnly = $this->cache->get($cacheKey);
597-
if ($isReadOnly === null) {
598-
$file = $this->getFileForSession($session, $shareToken);
599-
$isReadOnly = $this->isReadOnly($file, $shareToken);
600-
$this->cache->set($cacheKey, $isReadOnly, 60 * 5);
601-
return $isReadOnly;
602-
}
603-
604-
return $isReadOnly;
605-
}
606-
607594
public function isReadOnly(File $file, ?string $token): bool {
608-
$readOnly = true;
595+
$readOnly = !$file->isUpdateable();
609596
if ($token !== null) {
610597
try {
611598
$this->checkSharePermissions($token, Constants::PERMISSION_UPDATE);
612-
$readOnly = false;
613599
} catch (NotFoundException $e) {
600+
$readOnly = true;
614601
}
615-
} else {
616-
$readOnly = !$file->isUpdateable();
617602
}
618603

619604
$lockInfo = $this->getLockInfo($file);

src/components/Editor.vue

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<script>
7575
import Vue, { ref, set, watch } from 'vue'
7676
import { getCurrentUser } from '@nextcloud/auth'
77+
import { showWarning } from '@nextcloud/dialogs'
7778
import { loadState } from '@nextcloud/initial-state'
7879
import { isPublicShare } from '@nextcloud/sharing/public'
7980
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
@@ -471,6 +472,7 @@ export default {
471472
bus.on('stateChange', this.onStateChange)
472473
bus.on('idle', this.onIdle)
473474
bus.on('save', this.onSave)
475+
bus.on('permissionChange', this.onPermissionChange)
474476
},
475477
476478
unlistenSyncServiceEvents() {
@@ -483,6 +485,7 @@ export default {
483485
bus.off('stateChange', this.onStateChange)
484486
bus.off('idle', this.onIdle)
485487
bus.off('save', this.onSave)
488+
bus.off('permissionChange', this.onPermissionChange)
486489
},
487490
488491
reconnect() {
@@ -669,7 +672,17 @@ export default {
669672
}
670673
671674
if (type === ERROR_TYPE.PUSH_FORBIDDEN) {
672-
this.hasConnectionIssue = true
675+
this.readOnly = true
676+
this.editMode = false
677+
if (this.$editor) {
678+
this.$editor.setEditable(this.editMode)
679+
}
680+
showWarning(
681+
t(
682+
'text',
683+
'Your editing permissions have been revoked. The document is now read-only.',
684+
),
685+
)
673686
this.emit('push:forbidden')
674687
return
675688
}
@@ -722,6 +735,26 @@ export default {
722735
})
723736
},
724737
738+
onPermissionChange({ readOnly }) {
739+
this.readOnly = readOnly
740+
this.editMode = !readOnly && !this.openReadOnlyEnabled
741+
if (this.$editor) {
742+
this.$editor.setEditable(this.editMode)
743+
}
744+
if (readOnly) {
745+
showWarning(
746+
t(
747+
'text',
748+
'Your editing permissions have been revoked. The document is now read-only.',
749+
),
750+
)
751+
} else {
752+
showWarning(
753+
t('text', 'You now have edit permissions for this document.'),
754+
)
755+
}
756+
},
757+
725758
onFocus() {
726759
this.emit('focus')
727760
},

src/services/PollingBackend.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ const FETCH_INTERVAL_MAX = 5000
2929
const FETCH_INTERVAL_SINGLE_EDITOR = 5000
3030

3131
/**
32-
* Interval to check for changes for read only users
32+
* Interval to check for permission changes while read-only.
33+
* Step updates are rare here; keep polling fast so edit permission can be restored.
3334
* @type {number}
3435
*/
35-
const FETCH_INTERVAL_READ_ONLY = 30000
36+
const FETCH_INTERVAL_READ_ONLY = 5000
3637

3738
/**
3839
* Interval to fetch for changes when a browser window is considered invisible by the
@@ -59,6 +60,8 @@ class PollingBackend {
5960
#syncService
6061
/** @type {Connection} */
6162
#connection
63+
/** @type {boolean} */
64+
#readOnly = false
6265

6366
#lastPoll
6467
#fetchInterval
@@ -73,6 +76,7 @@ class PollingBackend {
7376
this.#fetchInterval = FETCH_INTERVAL
7477
this.#fetchRetryCounter = 0
7578
this.#lastPoll = 0
79+
this.#readOnly = connection.state?.document?.readOnly ?? false
7680
}
7781

7882
connect() {
@@ -126,6 +130,18 @@ class PollingBackend {
126130
const { document, sessions } = data
127131
this.#fetchRetryCounter = 0
128132

133+
if (data.readOnly !== undefined && data.readOnly !== this.#readOnly) {
134+
this.#readOnly = data.readOnly
135+
this.#syncService.bus.emit('permissionChange', {
136+
readOnly: this.#readOnly,
137+
})
138+
if (data.readOnly) {
139+
this.maximumReadOnlyTimer()
140+
} else {
141+
this.resetRefetchTimer()
142+
}
143+
}
144+
129145
this.#syncService.bus.emit('change', { document, sessions })
130146
this.#syncService.receiveSteps(data)
131147

@@ -138,7 +154,7 @@ class PollingBackend {
138154
}
139155
const disconnect = Date.now() - COLLABORATOR_DISCONNECT_TIME
140156
const alive = sessions.filter((s) => s.lastContact * 1000 > disconnect)
141-
if (this.#syncService.isReadOnly) {
157+
if (this.#readOnly) {
142158
this.maximumReadOnlyTimer()
143159
} else if (alive.length < 2) {
144160
this.maximumRefetchTimer()

src/services/SyncService.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
/* eslint-disable jsdoc/valid-types */
77

8+
import { showError } from '@nextcloud/dialogs'
89
import mitt from 'mitt'
910
import debounce from 'debounce'
1011

@@ -116,6 +117,9 @@ export declare type EventTypes = {
116117
/* Emitted once a document becomes idle */
117118
idle: void
118119

120+
/* Emitted if the read only state of the document has changed */
121+
permissionChange: { readOnly: boolean }
122+
119123
}
120124

121125
class SyncService {
@@ -374,6 +378,19 @@ class SyncService {
374378
this.autosave.clear()
375379
} catch (e) {
376380
logger.error('Failed to save document.', { error: e })
381+
const response = (
382+
e as { response?: { status?: number; data?: { error?: string } } }
383+
).response
384+
if (response?.status === 403) {
385+
// Document is now read-only; permissionChange from sync will update the UI
386+
return
387+
}
388+
if (response?.status === 412) {
389+
this.bus.emit('error', { type: ERROR_TYPE.LOAD_ERROR, data: response })
390+
if (response.data?.error) {
391+
showError(response.data.error)
392+
}
393+
}
377394
throw e
378395
}
379396
}

0 commit comments

Comments
 (0)