diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 53c52bb63c2..79785afdf56 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -36,6 +36,7 @@ use OCA\Text\Db\StepMapper; use OCA\Text\Exception\DocumentHasUnsavedChangesException; use OCA\Text\Exception\DocumentSaveConflictException; +use OCA\Text\YjsMessage; use OCP\AppFramework\Db\DoesNotExistException; use OCP\Constants; use OCP\DB\Exception; @@ -230,11 +231,16 @@ public function writeDocumentState(int $documentId, string $content): void { public function addStep(Document $document, Session $session, $steps, $version, $shareToken): array { $sessionId = $session->getId(); $documentId = $session->getDocumentId(); + $readOnly = $this->isReadOnly($this->getFileForSession($session, $shareToken), $shareToken); $stepsToInsert = []; $querySteps = []; $getStepsSinceVersion = null; $newVersion = $version; foreach ($steps as $step) { + $message = YjsMessage::fromBase64($step); + if ($readOnly && $message->isUpdate()) { + continue; + } // Steps are base64 encoded messages of the yjs protocols // https://github.com/yjs/y-protocols // Base64 encoded values smaller than "AAE" belong to sync step 1 messages. @@ -245,8 +251,8 @@ public function addStep(Document $document, Session $session, $steps, $version, array_push($stepsToInsert, $step); } } - if (sizeof($stepsToInsert) > 0) { - if ($this->isReadOnly($this->getFileForSession($session, $shareToken), $shareToken)) { + if (count($stepsToInsert) > 0) { + if ($readOnly) { throw new NotPermittedException('Read-only client tries to push steps with changes'); } $newVersion = $this->insertSteps($document, $session, $stepsToInsert, $version); diff --git a/lib/YjsMessage.php b/lib/YjsMessage.php new file mode 100644 index 00000000000..54752270966 --- /dev/null +++ b/lib/YjsMessage.php @@ -0,0 +1,98 @@ +data)); + $num = 0; + $mult = 1; + $len = count($bytes); + while ($this->pos < $len) { + $r = $bytes[$this->pos++]; + // num = num | ((r & binary.BITS7) << len) + $num = $num + ($r & 0b1111111) * $mult; + $mult *= 128; + if ($r <= 0b1111111) { + return $num; + } + // Number.MAX_SAFE_INTEGER in JS + if ($num > 9007199254740990) { + throw new \OutOfBoundsException(); + } + } + throw new InvalidArgumentException(); + } + + public function getYjsMessageType(): int { + $oldPos = $this->pos; + $this->pos = 0; + $messageType = $this->readVarUint(); + $this->pos = $oldPos; + return $messageType; + } + + public function getYjsSyncType(): int { + $oldPos = $this->pos; + $this->pos = 0; + $messageType = $this->readVarUint(); + if ($messageType !== self::YJS_MESSAGE_SYNC) { + throw new \ValueError('Message is not a sync message'); + } + $syncType = $this->readVarUint(); + $this->pos = $oldPos; + return $syncType; + } + + /** + * Based on https://github.com/yjs/y-protocols/blob/master/PROTOCOL.md#handling-read-only-users + */ + public function isUpdate(): bool { + if ($this->getYjsMessageType() === self::YJS_MESSAGE_SYNC) { + if (in_array($this->getYjsSyncType(), [self::YJS_MESSAGE_SYNC_STEP2, self::YJS_MESSAGE_SYNC_UPDATE])) { + return true; + } + } + + return false; + } + +}