diff --git a/lib/Db/MemberRequest.php b/lib/Db/MemberRequest.php index 58c822b9c..9e47a69e2 100644 --- a/lib/Db/MemberRequest.php +++ b/lib/Db/MemberRequest.php @@ -20,6 +20,9 @@ use OCA\Circles\Model\Member; use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Model\Probes\MemberProbe; +use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\TimezoneService; +use OCP\IDBConnection; /** * Class MemberRequest @@ -27,6 +30,15 @@ * @package OCA\Circles\Db */ class MemberRequest extends MemberRequestBuilder { + + public function __construct( + private IDBConnection $db, + TimezoneService $timezoneService, + ConfigService $configService, + ) { + parent::__construct($timezoneService, $configService); + } + /** * @param Member $member * @@ -186,6 +198,42 @@ public function updateLevel(Member $member): void { } + /** + * @return ?Member old owner or null if old owner was not found + */ + public function promoteToOwner(Member $member): ?Member { + try { + $this->db->beginTransaction(); + + $qb = $this->getMemberSelectSql(); + $qb->limitToCircleId($member->getCircleId()); + $qb->limitInt('level', Member::LEVEL_OWNER); + // forUpdate locks the owner row until transaction is commited, forcing concurrent + // requests to wait and read the updated current owner, preventing multiple owners + $qb->forUpdate(); + + try { + $oldOwner = $this->getItemFromRequest($qb); + } catch (MemberNotFoundException) { + $oldOwner = null; + } + + $this->updateLevel($member); + + if ($oldOwner !== null && $oldOwner->getId() !== $member->getId()) { + $oldOwner->setLevel(Member::LEVEL_ADMIN); + $this->updateLevel($oldOwner); + } + + $this->db->commit(); + + return $oldOwner; + } catch (\Throwable $e) { + $this->db->rollBack(); + throw $e; + } + } + /** * @param string $singleId * @param IFederatedUser|null $initiator diff --git a/lib/FederatedItems/MemberLevel.php b/lib/FederatedItems/MemberLevel.php index 20822f566..78eac9ba2 100644 --- a/lib/FederatedItems/MemberLevel.php +++ b/lib/FederatedItems/MemberLevel.php @@ -125,13 +125,14 @@ public function verify(FederatedEvent $event): void { public function manage(FederatedEvent $event): void { $member = clone $event->getMember(); $member->setLevel($event->getData()->gInt('level')); - $this->memberRequest->updateLevel($member); if ($member->getLevel() === Member::LEVEL_OWNER) { - $oldOwner = clone $event->getCircle()->getOwner(); - $oldOwner->setLevel(Member::LEVEL_ADMIN); - $this->memberRequest->updateLevel($oldOwner); - $this->membershipService->onUpdate($oldOwner->getSingleId()); + $oldOwner = $this->memberRequest->promoteToOwner($member); + if ($oldOwner !== null) { + $this->membershipService->onUpdate($oldOwner->getSingleId()); + } + } else { + $this->memberRequest->updateLevel($member); } $this->membershipService->onUpdate($member->getSingleId());