Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/PollApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
4 changes: 1 addition & 3 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
15 changes: 11 additions & 4 deletions lib/Db/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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();
}

/**
Expand Down
11 changes: 6 additions & 5 deletions lib/Service/CommentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
155 changes: 155 additions & 0 deletions lib/Service/DiffService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Polls\Service;

use OCP\AppFramework\Db\Entity;

/**
* DiffService is responsible for calculating the differences between two objects.
* It can be used to compare a base object with a comparison object and retrieve
* the differences in various formats.
*
*/
class DiffService {
private Entity $compareObject;
private array $diff = [];
private string $baseJson = '';

/**
* Constructor for DiffService
*
* @param Entity $baseObject The base object to compare against
* @throws \JsonException If the base object cannot be serialized to JSON
*
* @psalm-param Entity $baseObject
*/
public function __construct(
private Entity $baseObject,
) {
$this->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<string, array{old: mixed, new: mixed}>
*/
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<string, mixed>
*/
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<array-key, non-empty-array<string, array{new: mixed, old: mixed}|mixed|null>>
*/
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;
}
}
3 changes: 2 additions & 1 deletion lib/Service/MailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 6 additions & 4 deletions lib/Service/OptionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
8 changes: 4 additions & 4 deletions lib/Service/PollGroupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
Loading