diff --git a/src/Appwrite/Databases/PresenceState.php b/src/Appwrite/Databases/PresenceState.php new file mode 100644 index 0000000000..a29ad46bb3 --- /dev/null +++ b/src/Appwrite/Databases/PresenceState.php @@ -0,0 +1,131 @@ +isApp($authorization->getRoles()); + $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); + + $allowedPermissions = [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + Database::PERMISSION_WRITE, + ]; + + $permissions = Permission::aggregate($permissions, $allowedPermissions); + + if (\is_null($permissions)) { + $permissions = []; + if (!empty($user->getId()) && !$isPrivilegedUser) { + foreach ($allowedPermissions as $permission) { + $permissions[] = (new Permission($permission, 'user', $user->getId()))->toString(); + } + } + } + + if (!$isAPIKey && !$isPrivilegedUser) { + $this->assertPermissionsAgainstAuthorization($permissions, $authorization); + } + + sort($permissions, SORT_STRING); + $document->setAttribute('$permissions', $permissions); + + return $document; + } + + public function upsertForUser(Database $dbForProject, Document $presenceDocument, string $presenceId, string $userId): Document + { + if ($presenceId !== 'unique()') { + $presenceDocument->setAttribute('$id', $presenceId); + } + + try { + return $dbForProject->upsertDocument('presenceLogs', $presenceDocument); + } catch (DuplicateException $e) { + return $this->upsertFallback($dbForProject, $presenceDocument, $presenceId, $userId, $e); + } catch (NotFoundException $e) { + throw new Exception(Exception::DOCUMENT_NOT_FOUND, params: [$presenceId], previous: $e); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage(), previous: $e); + } catch (ConflictException $e) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT, $e->getMessage(), previous: $e); + } + } + + private function upsertFallback( + Database $dbForProject, + Document $presenceDocument, + string $presenceId, + string $userId, + DuplicateException $previous + ): Document { + try { + return $dbForProject->withTransaction(function () use ($dbForProject, $presenceDocument, $presenceId, $userId, $previous) { + $existingPresence = $dbForProject->findOne('presenceLogs', [Query::equal('userId', [$userId])]); + + if ($existingPresence->isEmpty()) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS, params: [$presenceId], previous: $previous); + } + + // Lock the current state before update to avoid races on duplicate fallback. + $currentPresence = $dbForProject->getDocument('presenceLogs', $existingPresence->getId(), forUpdate: true); + + if ($currentPresence->isEmpty()) { + throw new Exception(Exception::DOCUMENT_NOT_FOUND, params: [$existingPresence->getId()]); + } + + return $dbForProject->updateDocument('presenceLogs', $currentPresence->getId(), $presenceDocument); + }); + } catch (DuplicateException $e) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS, params: [$presenceId], previous: $e); + } catch (NotFoundException $e) { + throw new Exception(Exception::DOCUMENT_NOT_FOUND, params: [$presenceId], previous: $e); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage(), previous: $e); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage(), previous: $e); + } catch (ConflictException $e) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT, $e->getMessage(), previous: $e); + } + } + + private function assertPermissionsAgainstAuthorization(array $permissions, Authorization $authorization): void + { + foreach (Database::PERMISSIONS as $type) { + foreach ($permissions as $permission) { + $permission = Permission::parse($permission); + if ($permission->getPermission() != $type) { + continue; + } + + $role = (new Role( + $permission->getRole(), + $permission->getIdentifier(), + $permission->getDimension() + ))->toString(); + + if (!$authorization->hasRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $authorization->getRoles()) . ')'); + } + } + } + } +}