diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index d72c32971b8..81146729c0d 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -61,4 +61,5 @@ 'OCA\\Text\\Service\\SessionService' => $baseDir . '/../lib/Service/SessionService.php', 'OCA\\Text\\Service\\WorkspaceService' => $baseDir . '/../lib/Service/WorkspaceService.php', 'OCA\\Text\\TextFile' => $baseDir . '/../lib/TextFile.php', + 'OCA\\Text\\YjsMessage' => $baseDir . '/../lib/YjsMessage.php', ); diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index e15e7fa5700..ecb3bb34c60 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -76,6 +76,7 @@ class ComposerStaticInitText 'OCA\\Text\\Service\\SessionService' => __DIR__ . '/..' . '/../lib/Service/SessionService.php', 'OCA\\Text\\Service\\WorkspaceService' => __DIR__ . '/..' . '/../lib/Service/WorkspaceService.php', 'OCA\\Text\\TextFile' => __DIR__ . '/..' . '/../lib/TextFile.php', + 'OCA\\Text\\YjsMessage' => __DIR__ . '/..' . '/../lib/YjsMessage.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index 1bf5f8b1fec..97169549c6b 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\AppFramework\Db\Entity; use OCP\Constants; @@ -227,11 +228,9 @@ public function addStep($documentId, $sessionId, $steps, $version): array { $getStepsSinceVersion = null; $newVersion = $version; foreach ($steps as $step) { - // 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. - // These messages query other participants for their current state. - if ($step < "AAE") { + $message = YjsMessage::fromBase64($step); + // Filter out query steps as they would just trigger clients to send their steps again + if ($message->getYjsMessageType() === YjsMessage::YJS_MESSAGE_SYNC && $message->getYjsSyncType() === YjsMessage::YJS_MESSAGE_SYNC_STEP1) { array_push($querySteps, $step); } else { array_push($stepsToInsert, $step); @@ -245,8 +244,9 @@ public function addStep($documentId, $sessionId, $steps, $version): array { $allSteps = $this->getSteps($documentId, $getStepsSinceVersion); $stepsToReturn = []; foreach ($allSteps as $step) { - if ($step < "AAQ") { - array_push($stepsToReturn, $step); + $message = YjsMessage::fromBase64($step); + if ($message->getYjsMessageType() === YjsMessage::YJS_MESSAGE_SYNC) { + $stepsToReturn[] = $step; } } return [ diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index 34b1e8b6dcc..0ba14c51207 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -28,6 +28,7 @@ use OCA\Text\Db\Session; use OCA\Text\Db\SessionMapper; +use OCA\Text\YjsMessage; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DirectEditing\IManager; @@ -240,6 +241,12 @@ public function updateSessionAwareness(int $documentId, int $sessionId, string $ if (empty($message)) { return $session; } + + $decoded = YjsMessage::fromBase64($message); + if ($decoded->getYjsMessageType() !== YjsMessage::YJS_MESSAGE_AWARENESS) { + throw new \ValueError('Message passed was not an awareness message'); + } + $session->setLastAwarenessMessage($message); return $this->sessionMapper->update($session); } diff --git a/lib/YjsMessage.php b/lib/YjsMessage.php new file mode 100644 index 00000000000..c5c7103b971 --- /dev/null +++ b/lib/YjsMessage.php @@ -0,0 +1,88 @@ +data, $this->pos, 4)); + $segments = []; + $offset = 0; + foreach ($bytes as $byte) { + $offset++; + $segments[] = (0x7f & $byte); + + if (($byte & 0x80) === 0) { + $integer = 0; + foreach($segments as $segment) { + $integer <<= 7; + $integer |= (0x7f & $segment); + } + $this->pos += $offset; + return $integer; + } + } + if (($byte & 0x80) !== 0) { + throw new InvalidArgumentException('Incomplete byte sequence.'); + } + throw new InvalidArgumentException('Incomplete byte sequence.'); + } + + 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; + } + +} diff --git a/tests/unit/YjsMessageTest.php b/tests/unit/YjsMessageTest.php new file mode 100644 index 00000000000..310fbf1ce50 --- /dev/null +++ b/tests/unit/YjsMessageTest.php @@ -0,0 +1,42 @@ +getYjsMessageType(); + self::assertEquals($type, $unpack1, 'type'); + if ($subtype !== null) { + $unpack2 = $buffer->getYjsSyncType(); + self::assertEquals($subtype, $unpack2); + } + } +}