From f462f825fe4d3ef0bdc23f7d3423dbb21cd65ca8 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 24 Jul 2025 00:00:36 +0200 Subject: [PATCH 1/3] apply diffs to store updates Signed-off-by: dartcafe --- lib/Controller/PollApiController.php | 2 +- lib/Controller/PollController.php | 4 +- lib/Service/DiffService.php | 155 ++++++++++++++++++ lib/Service/PollService.php | 22 ++- src/Api/modules/polls.ts | 8 +- .../Configuration/ConfigAnonymous.vue | 2 +- src/stores/poll.ts | 50 +++++- 7 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 lib/Service/DiffService.php diff --git a/lib/Controller/PollApiController.php b/lib/Controller/PollApiController.php index 6469d4dd0a..66c8fe5b14 100644 --- a/lib/Controller/PollApiController.php +++ b/lib/Controller/PollApiController.php @@ -97,7 +97,7 @@ public function add(string $type, string $title, string $votingVariant = Poll::V #[NoCSRFRequired] #[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/{pollId}', requirements: ['apiVersion' => '(v2)'])] public function update(int $pollId, array $pollConfiguration): DataResponse { - return $this->response(fn () => ['poll' => $this->pollService->update($pollId, $pollConfiguration)]); + return $this->response(fn () => $this->pollService->update($pollId, $pollConfiguration)); } /** diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index 0a7ce87c29..21312c8621 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -196,9 +196,7 @@ public function add(string $type, string $title, string $votingVariant = Poll::V #[OpenAPI(OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'PUT', url: '/poll/{pollId}')] public function update(int $pollId, array $poll): JSONResponse { - return $this->response(fn () => [ - 'poll' => $this->pollService->update($pollId, $poll), - ]); + return $this->response(fn () => $this->pollService->update($pollId, $poll)); } /** diff --git a/lib/Service/DiffService.php b/lib/Service/DiffService.php new file mode 100644 index 0000000000..c0cc448253 --- /dev/null +++ b/lib/Service/DiffService.php @@ -0,0 +1,155 @@ +init(); + } + + private function init(): void { + $this->baseJson = json_encode($this->baseObject); + } + public function setComparisonObject(\OCA\Polls\Db\Poll $compareObject): void { + $this->compareObject = $compareObject; + $this->calculateDiff(); + } + + /** + * Get the full diff between the base object and the comparison object + * + * This method returns an associative array where keys are the paths to the changed values + * and values are arrays containing 'old' and 'new' values. + * + * @return array An associative array containing the differences + * + * @psalm-return array + */ + public function getFullDiff(): array { + return $this->diff; + } + + /** + * Get the new values from the diff, preserving the structure of the original object + * This method will return an associative array where keys are the paths to the new values + * and values are the new values themselves. + * + * @return array An associative array containing the new values + * + * @psalm-return array + */ + public function getNewValuesDiff(): array { + $newValues = []; + + // Recursively search for "new" values and preserve the structure + $this->extractNewValues($this->diff, $newValues); + + return $newValues; + } + + /** + * Calculate the difference between the base object and the comparison object. + * + * This method serializes the objects to JSON, decodes them into associative arrays, + * and then compares the arrays recursively to find differences. + */ + private function calculateDiff(): void { + // Serialize the objects to JSON format + $compareJson = json_encode($this->compareObject); + + // Decode the JSON strings back to associative arrays + $baseArray = json_decode($this->baseJson, true); + $compareArray = json_decode($compareJson, true); + + // Compare the arrays recursively + $this->diff = $this->array_diff_recursive($baseArray, $compareArray); + } + + /** + * Recursively extract new values from the diff array. + * + * @param array $diff The diff array containing changes. + * @param array $newValues The array to populate with new values, preserving the structure. + */ + private function extractNewValues($diff, &$newValues): void { + foreach ($diff as $key => $value) { + if (is_array($value)) { + // If 'new' exists, add it to the correct position in the structure + if (isset($value['new'])) { + $newValues[$key] = $value['new']; + } + + // If the element is an array and the key isn't set yet, initialize it + if (!isset($newValues[$key])) { + $newValues[$key] = []; + } + + // Recursively process nested arrays + $this->extractNewValues($value, $newValues[$key]); + } + } + } + + /** + * Recursively compare two arrays and return the differences + * + * This method compares two arrays recursively and returns an associative array + * + * @param array $baseArray The base array to compare against + * @param array $compareArray The array to compare with the base array + * @return array An associative array containing the differences + * + * @psalm-return array> + */ + private function array_diff_recursive($baseArray, $compareArray): array { + $diff = []; + + foreach ($baseArray as $key => $value) { + if (is_array($value) && isset($compareArray[$key]) && is_array($compareArray[$key])) { + $recursiveDiff = $this->array_diff_recursive($value, $compareArray[$key]); + if (!empty($recursiveDiff)) { + $diff[$key] = $recursiveDiff; + } + } elseif (!isset($compareArray[$key]) || $value !== $compareArray[$key]) { + $diff[$key] = ['old' => $value, 'new' => $compareArray[$key] ?? null]; + } + } + + // Loop through the second array to find keys that are not in the first array + foreach ($compareArray as $key => $value) { + if (!isset($baseArray[$key])) { + $diff[$key] = ['old' => null, 'new' => $value]; + } + } + + return $diff; + } +} diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index 84a1566740..4162121a43 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -240,10 +240,15 @@ public function add(string $type, string $title, string $votingVariant = Poll::V /** * Update poll configuration - * @return Poll + * + * @param int $pollId Poll id + * @param array $pollConfiguration Poll configuration + * @return array + * + * @psalm-return array{poll: Poll, diff: array, changes: array} */ - public function update(int $pollId, array $pollConfiguration): Poll { - $this->poll = $this->pollMapper->find($pollId); + public function update(int $pollId, array $pollConfiguration): array { + $this->poll = $this->pollMapper->get($pollId, withRoles: true); $this->poll->request(Poll::PERMISSION_POLL_EDIT); // Validate valuess @@ -278,12 +283,19 @@ public function update(int $pollId, array $pollConfiguration): Poll { $pollConfiguration['expire'] = time(); } + $diff = new DiffService($this->poll); + $this->poll->deserializeArray($pollConfiguration); $this->poll = $this->pollMapper->update($this->poll); - $this->eventDispatcher->dispatchTyped(new PollUpdatedEvent($this->poll)); - return $this->poll; + $diff->setComparisonObject($this->poll); + + return [ + 'poll' => $this->poll, + 'diff' => $diff->getFullDiff(), + 'changes' => $diff->getNewValuesDiff(), + ]; } /** diff --git a/src/Api/modules/polls.ts b/src/Api/modules/polls.ts index ea32f171ee..c2eeaa1d5c 100644 --- a/src/Api/modules/polls.ts +++ b/src/Api/modules/polls.ts @@ -105,7 +105,13 @@ const polls = { writePoll( pollId: number, poll: PollConfiguration, - ): Promise> { + ): Promise< + AxiosResponse<{ + poll: Poll + diff: Partial + changes: Partial + }> + > { return httpInstance.request({ method: 'PUT', url: `poll/${pollId}`, diff --git a/src/components/Configuration/ConfigAnonymous.vue b/src/components/Configuration/ConfigAnonymous.vue index b0424b2e95..31cb3168b0 100644 --- a/src/components/Configuration/ConfigAnonymous.vue +++ b/src/components/Configuration/ConfigAnonymous.vue @@ -62,7 +62,7 @@ function spawnConfirmationDialog(forceDialog: boolean = false) { * */ function lockAnonymous() { - pollStore.LockAnonymous() + pollStore.lockAnonymous() emit('change') } diff --git a/src/stores/poll.ts b/src/stores/poll.ts index 1ca33004b2..76f931c2b9 100644 --- a/src/stores/poll.ts +++ b/src/stores/poll.ts @@ -437,7 +437,7 @@ export const usePollStore = defineStore('poll', { } }, - async LockAnonymous(): Promise { + async lockAnonymous(): Promise { try { await PollsAPI.lockAnonymous(this.id) } catch (error) { @@ -455,9 +455,48 @@ export const usePollStore = defineStore('poll', { } }, - write(): void { + /* Load all necessary stores based on the configuration change. + * Avoid loading all dependent stores, if not necessary and reduce + * the amount of db access and expensive mutations. + * + * TODO: Currently this is based on the changes reported by the API. + * For large polls this is still bad UX, since loading of the poll + * can take a while. Until then depending updates of the UI are + * also delayed. + * + * We keep it for the moment to avoid breaking changes. + */ + async loadConsistentPoll(changes: Partial): Promise { + const dispatches = new Set() + + if ( + changes.configuration?.maxVotesPerUser !== undefined + || changes.configuration?.maxVotesPerOption !== undefined + || changes.configuration?.hideBookedUp !== undefined + ) { + const optionsStore = useOptionsStore() + dispatches.add(optionsStore.load()) + } + + if ( + changes.configuration?.allowComment !== undefined + || changes.configuration?.forceConfidentialComments !== undefined + ) { + const commentsStore = useCommentsStore() + dispatches.add(commentsStore.load()) + } + + if (changes.configuration?.anonymous !== undefined) { + const votesStore = useVotesStore() + dispatches.add(votesStore.load()) + } + const pollsStore = usePollsStore() + dispatches.add(pollsStore.load()) + Promise.all(dispatches) + }, + write(): void { const debouncedLoad = this.$debounce(async () => { if (this.configuration.title === '') { showError(t('polls', 'Title must not be empty!')) @@ -469,11 +508,12 @@ export const usePollStore = defineStore('poll', { this.id, this.configuration, ) - this.$patch(response.data.poll) + this.$patch(response.data.changes) emit(Event.UpdatePoll, { store: 'poll', message: t('polls', 'Poll updated'), }) + this.loadConsistentPoll(response.data.changes) } catch (error) { if ((error as AxiosError)?.code === 'ERR_CANCELED') { return @@ -482,11 +522,9 @@ export const usePollStore = defineStore('poll', { error, poll: this.$state, }) + this.load() showError(t('polls', 'Error writing poll')) throw error - } finally { - this.load() - pollsStore.load() } }, 500) debouncedLoad() From 29fb31b89fe4422fcbfe5ad9f173ce363edb3595 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 24 Jul 2025 00:00:59 +0200 Subject: [PATCH 2/3] refactor permission request and fix loading archived polls Signed-off-by: dartcafe --- lib/Db/Poll.php | 15 ++++++++++---- lib/Service/CommentService.php | 11 +++++----- lib/Service/MailService.php | 3 ++- lib/Service/OptionService.php | 10 ++++++---- lib/Service/PollGroupService.php | 8 ++++---- lib/Service/PollService.php | 30 +++++++++++++++------------- lib/Service/ShareService.php | 31 ++++++++++++++++++----------- lib/Service/SubscriptionService.php | 6 ++++-- lib/Service/VoteService.php | 27 +++++++++++++------------ lib/Service/WatchService.php | 3 ++- 10 files changed, 84 insertions(+), 60 deletions(-) diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index f5f8be30a1..ebd2497203 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -559,13 +559,15 @@ private function setMiscSettingsByKey(string $key, $value): void { /** * Request a permission level and get exception if denied + * @param string $permission The permission to request + * @return Poll Returns the current poll object * @throws ForbiddenException Thrown if access is denied */ - public function request(string $permission): bool { + public function request(string $permission): Poll { if (!$this->getIsAllowed($permission)) { throw new ForbiddenException('denied permission ' . $permission); } - return true; + return $this; } /** @@ -868,8 +870,13 @@ private function getAllowChangeForeignVotes(): bool { * Checks, if poll owner is allowed to deanonymize votes **/ private function getAllowDeanonymize(): bool { - // Current user is allowed to edit the poll and the owner of the poll is unrestricted - return $this->getAnonymous() > -1 && $this->getAllowEditPoll() && $this->getUser()->getIsUnrestrictedPollOwner(); + // Deanonymization is only allowed, + // if the anonymize setting is not locked + // and the current user is allowed to edit the poll + // and the owner of the poll is unrestricted + return $this->getAnonymous() > -1 + && $this->getAllowEditPoll() + && $this->getUser()->getIsUnrestrictedPollOwner(); } /** diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index 0bc5d111ad..89bd81972c 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -36,11 +36,11 @@ public function __construct( */ public function list(int $pollId): array { try { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD); + $this->pollMapper->get($pollId, true, withRoles: true) + ->request(Poll::PERMISSION_COMMENT_ADD); } catch (Exception $e) { return []; } - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD); $comments = $this->commentMapper->findByPoll($pollId); // treat comments from the same user within 5 minutes as grouped comments @@ -66,8 +66,8 @@ public function list(int $pollId): array { * Add comment */ public function add(string $message, int $pollId, ?bool $confidential = false): Comment { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_COMMENT_ADD); + $poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_COMMENT_ADD); if ($poll->getForceConfidentialComments()) { $confidential = true; @@ -104,7 +104,8 @@ public function delete(int $commentId, bool $restore = false): Comment { $this->comment = $this->commentMapper->find($commentId); if (!$this->comment->getCurrentUserIsEntityUser()) { - $this->pollMapper->get($this->comment->getPollId(), withRoles: true)->request(Poll::PERMISSION_COMMENT_DELETE); + $this->pollMapper->get($this->comment->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_COMMENT_DELETE); } $this->comment->setDeleted($restore ? 0 : time()); diff --git a/lib/Service/MailService.php b/lib/Service/MailService.php index 4f50e8535c..5630b7bd73 100644 --- a/lib/Service/MailService.php +++ b/lib/Service/MailService.php @@ -228,7 +228,8 @@ public function sendAutoReminder(): void { * Send a confirmation mail for the poll to all participants */ public function sendConfirmations(int $pollId): SentResult { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $sentResult = new SentResult(); $participants = $this->userMapper->getParticipants($pollId); diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index 6fc12a7259..2ebbde7048 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -199,7 +199,8 @@ public function delete(int $optionId, bool $restore = false): Option { $option = $this->optionMapper->find($optionId); if (!$option->getCurrentUserIsEntityUser()) { - $this->pollMapper->get($option->getPollId(), withRoles: true)->request(Poll::PERMISSION_OPTION_DELETE); + $this->pollMapper->get($option->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_OPTION_DELETE); } $option->setDeleted($restore ? 0 : time()); @@ -330,8 +331,9 @@ public function shift(int $pollId, int $step, string $unit): array { * Copy options from $fromPoll to $toPoll */ public function clone(int $fromPollId, int $toPollId): void { - $this->pollMapper->get($fromPollId, withRoles: true)->request(Poll::PERMISSION_POLL_VIEW); - $this->pollMapper->get($toPollId, withRoles: true)->request(Poll::PERMISSION_OPTION_ADD); + $this->pollMapper->get($fromPollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_VIEW) + ->request(Poll::PERMISSION_OPTION_ADD); foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) { $option = new Option(); @@ -442,7 +444,7 @@ private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): */ private function getPoll(int $pollId, string $permission = Poll::PERMISSION_POLL_VIEW): void { if ($this->poll->getId() !== $pollId) { - $this->poll = $this->pollMapper->get($pollId, withRoles: true); + $this->poll = $this->pollMapper->get($pollId, true, withRoles: true); } $this->poll->request($permission); } diff --git a/lib/Service/PollGroupService.php b/lib/Service/PollGroupService.php index 0ca2f342c7..3b62142a43 100644 --- a/lib/Service/PollGroupService.php +++ b/lib/Service/PollGroupService.php @@ -64,8 +64,8 @@ public function addPollToPollGroup( ?int $pollGroupId = null, ?string $pollGroupName = null, ): PollGroup { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_EDIT); + $poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); // Without poll group id, we create a new poll group if ($pollGroupId === null @@ -112,8 +112,8 @@ public function removePollFromPollGroup( int $pollId, int $pollGroupId, ): ?PollGroup { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_EDIT); + $poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $pollGroup = $this->pollGroupMapper->find($pollGroupId); diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index 4162121a43..e17b631aa7 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -248,8 +248,8 @@ public function add(string $type, string $title, string $votingVariant = Poll::V * @psalm-return array{poll: Poll, diff: array, changes: array} */ public function update(int $pollId, array $pollConfiguration): array { - $this->poll = $this->pollMapper->get($pollId, withRoles: true); - $this->poll->request(Poll::PERMISSION_POLL_EDIT); + $this->poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); // Validate valuess if (isset($pollConfiguration['showResults']) && !in_array($pollConfiguration['showResults'], $this->getValidShowResults())) { @@ -336,8 +336,8 @@ public function setLastInteraction(int $pollId): void { * @return Poll */ public function toggleArchive(int $pollId): Poll { - $this->poll = $this->pollMapper->find($pollId); - $this->poll->request(Poll::PERMISSION_POLL_DELETE); + $this->poll = $this->pollMapper->find($pollId) + ->request(Poll::PERMISSION_POLL_DELETE); $this->poll->setDeleted($this->poll->getDeleted() ? 0 : time()); $this->poll = $this->pollMapper->update($this->poll); @@ -357,12 +357,12 @@ public function toggleArchive(int $pollId): Poll { */ public function delete(int $pollId): Poll { try { - $this->poll = $this->pollMapper->get($pollId, withRoles: true); + $this->poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_DELETE); } catch (DoesNotExistException $e) { throw new AlreadyDeletedException('Poll not found, assume already deleted'); } - $this->poll->request(Poll::PERMISSION_POLL_DELETE); $this->eventDispatcher->dispatchTyped(new PollDeletedEvent($this->poll)); $this->pollMapper->delete($this->poll); @@ -374,7 +374,8 @@ public function delete(int $pollId): Poll { * @return Poll */ public function close(int $pollId): Poll { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); return $this->toggleClose($pollId, time() - 5); } @@ -383,7 +384,8 @@ public function close(int $pollId): Poll { * @return Poll */ public function reopen(int $pollId): Poll { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); return $this->toggleClose($pollId, 0); } @@ -392,8 +394,8 @@ public function reopen(int $pollId): Poll { * @return Poll */ private function toggleClose(int $pollId, int $expiry): Poll { - $this->poll = $this->pollMapper->find($pollId); - $this->poll->request(Poll::PERMISSION_POLL_EDIT); + $this->poll = $this->pollMapper->find($pollId) + ->request(Poll::PERMISSION_POLL_EDIT); $this->poll->setExpire($expiry); if ($expiry > 0) { @@ -412,8 +414,8 @@ private function toggleClose(int $pollId, int $expiry): Poll { * @return Poll */ public function clone(int $pollId): Poll { - $origin = $this->pollMapper->get($pollId, withRoles: true); - $origin->request(Poll::PERMISSION_POLL_VIEW); + $origin = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_VIEW); $this->appSettings->getPollCreationAllowed(); $this->poll = new Poll(); @@ -444,8 +446,8 @@ public function clone(int $pollId): Poll { * */ public function getParticipantsEmailAddresses(int $pollId): array { - $this->poll = $this->pollMapper->get($pollId, withRoles: true); - $this->poll->request(Poll::PERMISSION_POLL_EDIT); + $this->poll = $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $votes = $this->voteMapper->findParticipantsByPoll($this->poll->getId()); $list = []; diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index bc673f6af9..ad3897621d 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -88,8 +88,8 @@ public function __construct( public function list(int $pollOrPollGroupId, string $purpose = 'poll'): array { try { if ($purpose === 'poll') { - $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_EDIT); + $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $this->shares = $this->shareMapper->findByPoll($pollOrPollGroupId, $poll->getPollGroups()); } else { $this->pollGroupMapper->find($pollOrPollGroupId); @@ -113,7 +113,8 @@ public function list(int $pollOrPollGroupId, string $purpose = 'poll'): array { */ public function listNotInvited(int $pollId): array { try { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $this->shares = $this->shareMapper->findByPollNotInvited($pollId); } catch (ForbiddenException $e) { return []; @@ -187,7 +188,8 @@ public function get(string $token): Share { */ public function setType(string $token, string $type): Share { $this->share = $this->shareMapper->findByToken($token); - $this->pollMapper->get($this->share->getPollId(), withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($this->share->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); // ATM only type user can transform to type admin and vice versa if (($type === Share::TYPE_ADMIN && $this->share->getType() === Share::TYPE_USER) @@ -206,7 +208,8 @@ public function setType(string $token, string $type): Share { public function setPublicPollEmail(string $token, string $value): Share { try { $this->share = $this->shareMapper->findByToken($token); - $this->pollMapper->get($this->share->getPollId(), withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($this->share->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $this->share->setPublicPollEmail($value); $this->share = $this->shareMapper->update($this->share); } catch (ShareNotFoundException $e) { @@ -269,7 +272,8 @@ public function setLabel(string $label, string $token): Share { $this->share = $this->shareMapper->findByToken($token); if ($this->share->getType() === Share::TYPE_PUBLIC) { - $this->pollMapper->get($this->share->getPollId(), withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($this->share->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $this->share->setLabel($label); // overwrite any possible displayName @@ -433,9 +437,11 @@ public function deleteByToken(string $token, bool $restore = false): Share { */ public function delete(Share $share, bool $restore = false): Share { if (!$share->getPollId() && $share->getGroupId()) { - $this->pollGroupMapper->find($share->getGroupId())->request(PollGroup::PERMISSION_POLL_GROUP_EDIT); + $this->pollGroupMapper->find($share->getGroupId()) + ->request(PollGroup::PERMISSION_POLL_GROUP_EDIT); } else { - $this->pollMapper->get($share->getPollId(), withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($share->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); } $share->setDeleted($restore ? 0 : time()); @@ -464,7 +470,8 @@ public function lockByToken(string $token, bool $unlock = false): Share { * @param bool $unlock Set true, if share is to be unlocked */ private function lock(Share $share, bool $unlock = false): Share { - $this->pollMapper->get($share->getPollId(), withRoles: true)->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($share->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); $share->setLocked($unlock ? 0 : time()); $this->shareMapper->update($share); @@ -586,9 +593,9 @@ public function add( string $purpose = 'poll', ): Share { if ($purpose === 'poll') { - $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_EDIT); - $poll->request(Poll::PERMISSION_SHARE_ADD); + $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT) + ->request(Poll::PERMISSION_SHARE_ADD); if ($type === UserBase::TYPE_PUBLIC) { $this->appSettings->getPublicSharesAllowed(); diff --git a/lib/Service/SubscriptionService.php b/lib/Service/SubscriptionService.php index 7c4cfe7b7e..f2ec26fc25 100644 --- a/lib/Service/SubscriptionService.php +++ b/lib/Service/SubscriptionService.php @@ -27,7 +27,8 @@ public function __construct( } public function get(int $pollId): bool { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_VIEW); + $this->pollMapper->get($pollId, true, withRoles: true) + ->request(Poll::PERMISSION_POLL_VIEW); try { $this->subscriptionMapper->findByPollAndUser($pollId, $this->userSession->getCurrentUserId()); @@ -53,7 +54,8 @@ public function set(bool $setToSubscribed, int $pollId): bool { } else { try { // $this->pollMapper->get($pollId, withRoles: true); - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_SUBSCRIBE); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_POLL_SUBSCRIBE); $this->add($pollId, $this->userSession->getCurrentUserId()); } catch (ForbiddenException $e) { return false; diff --git a/lib/Service/VoteService.php b/lib/Service/VoteService.php index b40a051a02..2df3e334a8 100644 --- a/lib/Service/VoteService.php +++ b/lib/Service/VoteService.php @@ -41,16 +41,17 @@ public function __construct( * @return Vote[] */ public function list(int $pollId): array { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_VIEW); + $poll = $this->pollMapper->get($pollId, true, withRoles: true) + ->request(Poll::PERMISSION_POLL_VIEW); - if (!$poll->getIsAllowed(Poll::PERMISSION_POLL_RESULTS_VIEW)) { + try { + $poll->request(Poll::PERMISSION_POLL_RESULTS_VIEW); + $votes = $this->voteMapper->findByPoll($pollId); + } catch (ForbiddenException $e) { // Just return the participants votes, no further anoymizing or obfuscating is nessecary - return $this->voteMapper->findByPollAndUser($pollId, ($this->userSession->getCurrentUserId())); + $votes = $this->voteMapper->findByPollAndUser($pollId, ($this->userSession->getCurrentUserId())); } - $votes = $this->voteMapper->findByPoll($pollId); - return $votes; } @@ -87,8 +88,8 @@ public function set(Option|int $optionOrOptionIdoptionId, string $setTo): ?Vote } else { $option = $this->optionMapper->find($optionOrOptionIdoptionId); } - $poll = $this->pollMapper->get($option->getPollId(), withRoles: true); - $poll->request(Poll::PERMISSION_VOTE_EDIT); + $poll = $this->pollMapper->get($option->getPollId(), withRoles: true) + ->request(Poll::PERMISSION_VOTE_EDIT); if ($option->getIsLocked()) { $this->checkVoteLimit($option); @@ -140,8 +141,8 @@ public function set(Option|int $optionOrOptionIdoptionId, string $setTo): ?Vote */ public function getOprhanedVotes(int $pollId): array { try { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_POLL_EDIT); + $this->pollMapper->get($pollId, true, withRoles: true) + ->request(Poll::PERMISSION_POLL_EDIT); return $this->voteMapper->findOrphanedByPoll($pollId); } catch (ForbiddenException $e) { return []; @@ -155,8 +156,8 @@ public function getOprhanedVotes(int $pollId): array { * @return Vote[] */ public function deleteOrphanedVotes(int $pollId): array { - $poll = $this->pollMapper->get($pollId, withRoles: true); - $poll->request(Poll::PERMISSION_VOTE_FOREIGN_CHANGE); + $this->pollMapper->get($pollId, withRoles: true) + ->request(Poll::PERMISSION_VOTE_FOREIGN_CHANGE); // delete all votes of the poll, which are not assigned to an option $votes = $this->voteMapper->findOrphanedByPoll($pollId); @@ -185,7 +186,7 @@ public function deleteUserFromPoll(int $pollId, string $userId = '', bool $delet // Set default right to delete all votes of the user $checkRight = Poll::PERMISSION_VOTE_FOREIGN_CHANGE; if ($userId === $this->userSession->getCurrentUserId()) { - // allow current user to remove his votes + // allow current user to remove his own votes $checkRight = Poll::PERMISSION_VOTE_EDIT; } diff --git a/lib/Service/WatchService.php b/lib/Service/WatchService.php index a1bdf3ccc5..e2bf74682f 100644 --- a/lib/Service/WatchService.php +++ b/lib/Service/WatchService.php @@ -35,7 +35,8 @@ public function __construct( */ public function watchUpdates(int $pollId, ?int $offset = null): array { if ($pollId) { - $this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_VIEW); + $this->pollMapper->get($pollId, true, withRoles: true) + ->request(Poll::PERMISSION_POLL_VIEW); } $start = time(); From 44d43194fb0e0e661004405876ced90f70bcac03 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 24 Jul 2025 00:03:16 +0200 Subject: [PATCH 3/3] changelog Signed-off-by: dartcafe --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f6f74d405..d2f1e6b349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,14 @@ All notable changes to this project will be documented in this file. - Make vote cell focusable - Make shadow of sticky items transparent - Changed experimental comments layout + - improve poll loading times again by applying diffs after updates ### Fixes - Force list view mode initially on mobile viewports - Fix some visual issues of the vote page - Bring back indicator for confirmed options after closing the poll - Add CSP to allow worker + - fix loading archived polls ## [8.1.4] - 2025-07-15 ### Fixes