From 82cb92169786cc8e0726a029196b146c90f8b931 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Wed, 6 Aug 2025 13:18:09 +0200 Subject: [PATCH 01/26] fix display of creation date Signed-off-by: dartcafe --- src/components/Poll/PollInfoLine.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Poll/PollInfoLine.vue b/src/components/Poll/PollInfoLine.vue index 103b20505a..d84aead42c 100644 --- a/src/components/Poll/PollInfoLine.vue +++ b/src/components/Poll/PollInfoLine.vue @@ -128,7 +128,7 @@ const subTexts = computed(() => { }) const dateCreatedRelative = computed(() => - pollStore.getExpirationDateTime.toRelative(), + pollStore.getCreationDateTime.toRelative(), ) const closeToClosing = computed( From efcd46abe0177021ad0cca16a0ead0398ee5cff8 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 7 Aug 2025 09:16:47 +0200 Subject: [PATCH 02/26] fix access to poll via shared poll group Signed-off-by: dartcafe --- lib/Db/PollMapper.php | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/Db/PollMapper.php b/lib/Db/PollMapper.php index 32272389d9..7e3f20fbc7 100644 --- a/lib/Db/PollMapper.php +++ b/lib/Db/PollMapper.php @@ -37,28 +37,13 @@ public function __construct( * @return Poll */ public function get(int $id, bool $getDeleted = false, bool $withRoles = false): Poll { - $qb = $this->db->getQueryBuilder(); - $qb->select(self::TABLE . '.*') - ->from($this->getTableName(), self::TABLE) - ->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->groupBy(self::TABLE . '.id'); + $qb = $this->buildQuery(false); + $qb->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); if (!$getDeleted) { $qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT))); } - if ($withRoles) { - $pollGroupsAlias = 'poll_groups'; - $currentUserId = $this->userSession->getCurrentUserId(); - $currentUserParam = $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR); - - $this->subQueryMaxDate($qb, self::TABLE); - - $this->joinUserRole($qb, self::TABLE, $currentUserParam); - $this->joinGroupShares($qb, self::TABLE); - $this->joinPollGroups($qb, self::TABLE, $pollGroupsAlias); - $this->joinPollGroupShares($qb, $pollGroupsAlias, $currentUserParam, $pollGroupsAlias); - } return $this->findEntity($qb); } From 7bfe26a02acdf8c52e25e1f14f9be810d7dffa20 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:51:07 +0200 Subject: [PATCH 03/26] rename const Signed-off-by: dartcafe --- lib/Db/Poll.php | 22 ++++++++++++---------- lib/Service/OptionService.php | 6 +++--- lib/Service/PollService.php | 8 ++++---- lib/Service/ShareService.php | 2 +- lib/Service/SubscriptionService.php | 2 +- lib/Service/VoteService.php | 2 +- lib/Service/WatchService.php | 2 +- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index ebd2497203..5321ba674e 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -81,7 +81,9 @@ class Poll extends EntityWithUser implements JsonSerializable { public const TYPE_DATE = 'datePoll'; public const TYPE_TEXT = 'textPoll'; public const VARIANT_SIMPLE = 'simple'; + /** @deprecated use ACCESS_PRIVATE instead */ public const ACCESS_HIDDEN = 'hidden'; + /** @deprecated use ACCESS_OPEN instead */ public const ACCESS_PUBLIC = 'public'; public const ACCESS_PRIVATE = 'private'; public const ACCESS_OPEN = 'open'; @@ -107,15 +109,15 @@ class Poll extends EntityWithUser implements JsonSerializable { public const ROLE_NONE = 'none'; public const PERMISSION_OVERRIDE = 'override_permission'; - public const PERMISSION_POLL_VIEW = 'view'; - public const PERMISSION_POLL_EDIT = 'edit'; - public const PERMISSION_POLL_CHANGE_OWNER = 'changeOwner'; - public const PERMISSION_POLL_DELETE = 'delete'; - public const PERMISSION_POLL_ARCHIVE = 'archive'; - public const PERMISSION_POLL_RESULTS_VIEW = 'seeResults'; + public const PERMISSION_POLL_ACCESS = 'accessPoll'; + public const PERMISSION_POLL_EDIT = 'editPoll'; + public const PERMISSION_POLL_CHANGE_OWNER = 'changePollOwner'; + public const PERMISSION_POLL_DELETE = 'deletePoll'; + public const PERMISSION_POLL_ARCHIVE = 'archivePoll'; + public const PERMISSION_POLL_RESULTS_VIEW = 'seePollResults'; public const PERMISSION_POLL_USERNAMES_VIEW = 'seeUserNames'; - public const PERMISSION_POLL_TAKEOVER = 'takeOver'; - public const PERMISSION_POLL_SUBSCRIBE = 'subscribe'; + public const PERMISSION_POLL_TAKEOVER = 'takeOverPoll'; + public const PERMISSION_POLL_SUBSCRIBE = 'subscribePoll'; public const PERMISSION_COMMENT_ADD = 'addComment'; public const PERMISSION_COMMENT_DELETE = 'deleteComment'; public const PERMISSION_OPTION_ADD = 'addOptions'; @@ -301,7 +303,7 @@ public function getPermissionsArray(): array { 'seeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW), 'subscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE), 'takeOver' => $this->getIsAllowed(self::PERMISSION_POLL_TAKEOVER), - 'view' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW), + 'view' => $this->getIsAllowed(self::PERMISSION_POLL_ACCESS), 'vote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT), ]; } @@ -583,7 +585,7 @@ public function getIsAllowed(string $permission): bool { self::PERMISSION_OPTION_DELETE => $this->getAllowDeleteOption(), self::PERMISSION_OPTIONS_REORDER => $this->getAllowReorderOptions(), self::PERMISSION_OVERRIDE => true, - self::PERMISSION_POLL_VIEW => $this->getAllowAccessPoll(), + self::PERMISSION_POLL_ACCESS => $this->getAllowAccessPoll(), self::PERMISSION_POLL_EDIT => $this->getAllowEditPoll(), self::PERMISSION_POLL_DELETE => $this->getAllowDeletePoll(), self::PERMISSION_POLL_ARCHIVE => $this->getAllowEditPoll(), diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index 2ebbde7048..afa4797a1b 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -59,7 +59,7 @@ public function get(int $optionId): Option { * @psalm-return array */ public function list(int $pollId): array { - $this->getPoll($pollId, Poll::PERMISSION_POLL_VIEW); + $this->getPoll($pollId, Poll::PERMISSION_POLL_ACCESS); try { $this->options = $this->optionMapper->findByPoll($pollId, !$this->poll->getIsAllowed(Poll::PERMISSION_POLL_RESULTS_VIEW)); @@ -332,7 +332,7 @@ public function shift(int $pollId, int $step, string $unit): array { */ public function clone(int $fromPollId, int $toPollId): void { $this->pollMapper->get($fromPollId, withRoles: true) - ->request(Poll::PERMISSION_POLL_VIEW) + ->request(Poll::PERMISSION_POLL_ACCESS) ->request(Poll::PERMISSION_OPTION_ADD); foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) { @@ -442,7 +442,7 @@ private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): * * @return void */ - private function getPoll(int $pollId, string $permission = Poll::PERMISSION_POLL_VIEW): void { + private function getPoll(int $pollId, string $permission = Poll::PERMISSION_POLL_ACCESS): void { if ($this->poll->getId() !== $pollId) { $this->poll = $this->pollMapper->get($pollId, true, withRoles: true); } diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index e17b631aa7..fb0998111a 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -60,7 +60,7 @@ public function listPolls(): array { } return array_values(array_filter($pollList, function (Poll $poll): bool { - return $poll->getIsAllowed(Poll::PERMISSION_POLL_VIEW); + return $poll->getIsAllowed(Poll::PERMISSION_POLL_ACCESS); })); } @@ -74,7 +74,7 @@ public function search(ISearchQuery $query): array { foreach ($polls as $poll) { try { - $poll->request(Poll::PERMISSION_POLL_VIEW); + $poll->request(Poll::PERMISSION_POLL_ACCESS); $pollList[] = $poll; } catch (ForbiddenException $e) { continue; @@ -175,7 +175,7 @@ public function get(int $pollId, $lightweight = false) { } else { $this->poll = $this->pollMapper->find($pollId); } - $this->poll->request(Poll::PERMISSION_POLL_VIEW); + $this->poll->request(Poll::PERMISSION_POLL_ACCESS); return $this->poll; } catch (DoesNotExistException $e) { throw new NotFoundException('Poll not found'); @@ -415,7 +415,7 @@ private function toggleClose(int $pollId, int $expiry): Poll { */ public function clone(int $pollId): Poll { $origin = $this->pollMapper->get($pollId, withRoles: true) - ->request(Poll::PERMISSION_POLL_VIEW); + ->request(Poll::PERMISSION_POLL_ACCESS); $this->appSettings->getPollCreationAllowed(); $this->poll = new Poll(); diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index de83055634..e0a4080cae 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -170,7 +170,7 @@ public function request(string $token): Share { return $this->share; } - if (!$poll->getIsAllowed(Poll::PERMISSION_POLL_VIEW)) { + if (!$poll->getIsAllowed(Poll::PERMISSION_POLL_ACCESS)) { throw new ForbiddenException('User is not allowed to access this poll'); } diff --git a/lib/Service/SubscriptionService.php b/lib/Service/SubscriptionService.php index f2ec26fc25..b687a76bfd 100644 --- a/lib/Service/SubscriptionService.php +++ b/lib/Service/SubscriptionService.php @@ -28,7 +28,7 @@ public function __construct( public function get(int $pollId): bool { $this->pollMapper->get($pollId, true, withRoles: true) - ->request(Poll::PERMISSION_POLL_VIEW); + ->request(Poll::PERMISSION_POLL_ACCESS); try { $this->subscriptionMapper->findByPollAndUser($pollId, $this->userSession->getCurrentUserId()); diff --git a/lib/Service/VoteService.php b/lib/Service/VoteService.php index 26ccc0146c..476b6c0521 100644 --- a/lib/Service/VoteService.php +++ b/lib/Service/VoteService.php @@ -41,7 +41,7 @@ public function __construct( */ public function list(int $pollId): array { $poll = $this->pollMapper->get($pollId, true, withRoles: true) - ->request(Poll::PERMISSION_POLL_VIEW); + ->request(Poll::PERMISSION_POLL_ACCESS); try { $poll->request(Poll::PERMISSION_POLL_RESULTS_VIEW); diff --git a/lib/Service/WatchService.php b/lib/Service/WatchService.php index 2ae2c1e328..b8cc2c939b 100644 --- a/lib/Service/WatchService.php +++ b/lib/Service/WatchService.php @@ -37,7 +37,7 @@ public function __construct( public function watchUpdates(int $pollId, string $mode, ?int $offset = null): array { if ($pollId) { $this->pollMapper->get($pollId, true, withRoles: true) - ->request(Poll::PERMISSION_POLL_VIEW); + ->request(Poll::PERMISSION_POLL_ACCESS); } $start = time(); From cefe239cddad9327d6c5f2a3ccb57f91bd576acb Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:51:44 +0200 Subject: [PATCH 04/26] fix typecast Signed-off-by: dartcafe --- lib/Db/Poll.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index 5321ba674e..45fb8c5737 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -65,7 +65,7 @@ * @method void setVotingVariant(string $value) * * Magic functions for joined columns - * @method int getShareToken() + * @method string getShareToken() * @method int getCurrentUserVotes() * @method int getCurrentUserVotesYes() * @method int getCurrentUserVotesNo() From c52a3fe71338c847f5d1398172d90607e648ae29 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:52:33 +0200 Subject: [PATCH 05/26] remove obsolete tidy step Signed-off-by: dartcafe --- lib/Db/TableManager.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Db/TableManager.php b/lib/Db/TableManager.php index 6bf06a902e..2024928a00 100644 --- a/lib/Db/TableManager.php +++ b/lib/Db/TableManager.php @@ -349,8 +349,6 @@ public function deleteAllDuplicates(?IOutput $output = null): array { } private function deleteDuplicates(string $table, array $columns):int { - $this->watchMapper->deleteOldEntries(time()); - $qb = $this->connection->getQueryBuilder(); if ($this->schema->hasTable($this->dbPrefix . $table)) { From 0c2a2189d787f37f389417ee8bb8358128ad51fa Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:54:48 +0200 Subject: [PATCH 06/26] ensure Poll entity satisfies getPollOwnerId() Signed-off-by: dartcafe --- lib/Db/Poll.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index 45fb8c5737..4e4fd52e31 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -342,6 +342,10 @@ public function getExpired(): bool { ); } + public function getPollOwnerId() { + return $this->getOwner(); + } + public function getUserRole(): string { if ($this->getCurrentUserIsEntityUser()) { return self::ROLE_OWNER; From 9413d55d0333c30af563d4f5ca61799cc37cebbb Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:55:29 +0200 Subject: [PATCH 07/26] fix db types and add unique index for share token Signed-off-by: dartcafe --- lib/Migration/TableSchema.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index 53954cd266..ea834a3bf6 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -70,7 +70,10 @@ abstract class TableSchema { Option::TABLE => ['name' => 'UNIQ_options', 'unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], Log::TABLE => ['name' => 'UNIQ_unprocessed', 'unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], Subscription::TABLE => ['name' => 'UNIQ_subscription', 'unique' => true, 'columns' => ['poll_id', 'user_id']], - Share::TABLE => ['name' => 'UNIQ_shares', 'unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id']], + Share::TABLE => [ + 'name' => 'UNIQ_shares', 'unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id'], + 'name' => 'UNIQ_token', 'unique' => true, 'columns' => ['token'] + ], Vote::TABLE => ['name' => 'UNIQ_votes', 'unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], Preferences::TABLE => ['name' => 'UNIQ_preferences', 'unique' => true, 'columns' => ['user_id']], Watch::TABLE => ['name' => 'UNIQ_watch', 'unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], @@ -243,8 +246,8 @@ abstract class TableSchema { ], Share::TABLE => [ 'id' => ['type' => Types::BIGINT, 'options' => ['autoincrement' => true, 'notnull' => true, 'length' => 20]], - 'poll_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => false, 'default' => null, 'length' => 20]], - 'group_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => false, 'default' => null, 'length' => 20]], + 'poll_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => null, 'length' => 20]], + 'group_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => null, 'length' => 20]], 'token' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 64]], 'type' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 64]], 'label' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 256]], From dd9a66d5d784aa958a0feb2a4c2b6a2af078dee3 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sat, 9 Aug 2025 18:56:09 +0200 Subject: [PATCH 08/26] remove FK from Shares table in repair step Signed-off-by: dartcafe --- lib/Migration/RepairSteps/CreateIndices.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Migration/RepairSteps/CreateIndices.php b/lib/Migration/RepairSteps/CreateIndices.php index 4d7a31fbba..6dfa323eea 100644 --- a/lib/Migration/RepairSteps/CreateIndices.php +++ b/lib/Migration/RepairSteps/CreateIndices.php @@ -11,6 +11,7 @@ use Doctrine\DBAL\Schema\Schema; use OCA\Polls\Db\IndexManager; +use OCA\Polls\Db\Share; use OCP\IDBConnection; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -36,6 +37,9 @@ public function run(IOutput $output): void { $this->schema = $this->connection->createSchema(); $this->indexManager->setSchema($this->schema); + // remove foreign keys from the share table + // cannot be used anymore since v8.0.0 + $messages = array_merge($messages, $this->indexManager->removeForeignKeysFromTable(Share::TABLE)); $messages = array_merge($messages, $this->indexManager->createForeignKeyConstraints()); $messages = array_merge($messages, $this->indexManager->createIndices()); $this->connection->migrateToSchema($this->schema); From c77b399240e3ec64b41283db3790b2210dadd1d3 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:01:01 +0200 Subject: [PATCH 09/26] rewritten share access Signed-off-by: dartcafe --- lib/Controller/PublicController.php | 6 +-- lib/Controller/ShareApiController.php | 3 +- lib/Controller/ShareController.php | 18 ------- lib/Db/Poll.php | 2 +- lib/Service/ShareService.php | 70 ++++++++++++++++----------- 5 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index c9e9b6cfba..8fcfebf02a 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -431,7 +431,7 @@ public function setDisplayName(string $token, string $displayName): JSONResponse #[FrontpageRoute(verb: 'PUT', url: '/s/{token}/email/{emailAddress}')] public function setEmailAddress(string $token, string $emailAddress = ''): JSONResponse { return $this->response(fn () => [ - 'share' => $this->shareService->setEmailAddress($this->shareService->get($token), $emailAddress) + 'share' => $this->shareService->setEmailAddress($this->shareService->request($token), $emailAddress) ]); } @@ -445,7 +445,7 @@ public function setEmailAddress(string $token, string $emailAddress = ''): JSONR #[FrontpageRoute(verb: 'DELETE', url: '/s/{token}/email')] public function deleteEmailAddress(string $token): JSONResponse { return $this->response(fn () => [ - 'share' => $this->shareService->deleteEmailAddress($this->shareService->get($token)) + 'share' => $this->shareService->deleteEmailAddress($this->shareService->request($token)) ]); } @@ -477,7 +477,7 @@ public function register(string $token, string $displayName, string $emailAddres #[OpenAPI(OpenAPI::SCOPE_IGNORE)] #[FrontpageRoute(verb: 'POST', url: '/s/{token}/resend')] public function resendInvitation(string $token): JSONResponse { - $share = $this->shareService->get($token); + $share = $this->shareService->request($token); return $this->response(fn () => [ 'share' => $share, 'sentResult' => $this->mailService->sendInvitation($share, new SentResult()), diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php index c5418f7f39..8d8b33ff26 100644 --- a/lib/Controller/ShareApiController.php +++ b/lib/Controller/ShareApiController.php @@ -48,11 +48,12 @@ public function list(int $pollId): DataResponse { * Get share by token */ #[CORS] + #[PublicPage] #[NoAdminRequired] #[NoCSRFRequired] #[ApiRoute(verb: 'GET', url: '/api/v1.0/share/{token}', requirements: ['apiVersion' => '(v2)'])] public function get(string $token): DataResponse { - return $this->response(fn () => ['share' => $this->shareService->get($token)]); + return $this->response(fn () => ['share' => $this->shareService->request($token)]); } /** diff --git a/lib/Controller/ShareController.php b/lib/Controller/ShareController.php index 9ed23afaf4..bffef77b06 100644 --- a/lib/Controller/ShareController.php +++ b/lib/Controller/ShareController.php @@ -251,22 +251,4 @@ public function resolveGroup(string $token): JSONResponse { 'shares' => $this->shareService->resolveGroupByToken($token) ]); } - - /** - * Set email address - * @param string $token Share token - * @param string $emailAddress Email address - * @deprecated 8.0.0 Use PUT /s/{token}/email/{emailAddress} - */ - #[NoAdminRequired] - #[OpenAPI(OpenAPI::SCOPE_IGNORE)] - #[FrontpageRoute(verb: 'PUT', url: '/share/{token}/email')] - public function setEmailAddress(string $token, string $emailAddress = ''): JSONResponse { - return $this->response(fn () => [ - 'share' => $this->shareService->setEmailAddress( - $this->shareService->get($token), - $emailAddress - ) - ]); - } } diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index 4e4fd52e31..e2aee15f01 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -611,7 +611,7 @@ public function getIsAllowed(string $permission): bool { * getIsInvolved - Is current user involved in current poll? * @return bool Returns true, if the current user is involved in the poll via share, as a participant or as the poll owner. */ - private function getIsInvolved(): bool { + public function getIsInvolved(): bool { return ( $this->getIsPollOwner() || $this->getIsParticipant() diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index e0a4080cae..1f896b2f83 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -130,7 +130,7 @@ public function listNotInvited(int $pollId): array { * If user is not authorized for this poll, create a personal share * for this user and return the created share instead of the public share */ - private function convertPublicShareToPersonalShare(): void { + private function convertPublicShareToPersonalShare(): Share { try { $this->share = $this->createNewShare( $this->share->getPollId(), @@ -144,51 +144,57 @@ private function convertPublicShareToPersonalShare(): void { // remove the public token from session $this->userSession->setShareToken($this->share->getToken()); } + return $this->share; } + /** - * Get share by token for accessing the poll + * Request access to a share by share token * * @param string $token Token of share to get */ public function request(string $token): Share { - $this->share = $this->shareMapper->findByToken($token); $this->validateShareType(); + $poll = $this->pollMapper->get($this->share->getPollId(), withRoles: true); + // deletes the displayname, to avoid displayname preset in case of public polls if ($this->share->getType() === Share::TYPE_PUBLIC) { $this->share->setDisplayName(''); } - $poll = $this->pollMapper->get($this->share->getPollId(), withRoles: true); - if ($poll->getIsAllowed(Poll::PERMISSION_VOTE_EDIT) - || $poll->getIsAllowed(Poll::PERMISSION_POLL_EDIT) - ) { - // user is allowed to access the poll, continue without creating a new share + if ($poll->getIsInvolved()) { + // user is already involved in the poll + if ($poll->getShareToken()) { + // return personal share, if exists + return $this->shareMapper->findByToken($poll->getShareToken()); + } + // Otherwise return the requested share return $this->share; } - if (!$poll->getIsAllowed(Poll::PERMISSION_POLL_ACCESS)) { - throw new ForbiddenException('User is not allowed to access this poll'); - } - // Exception: logged in user, accesses the poll via public share link - if ($this->share->getType() === Share::TYPE_PUBLIC && $this->userSession->getIsLoggedIn()) { - $this->convertPublicShareToPersonalShare(); + if ($this->share->getType() === Share::TYPE_PUBLIC) { + // Exception: logged in user, accesses the poll via public share link + if ($this->userSession->getIsLoggedIn()) { + return $this->convertPublicShareToPersonalShare(); + } + return $this->share; } + // Exception for convertable (email and contact) shares if (in_array($this->share->getType(), Share::CONVERATABLE_PUBLIC_SHARES, true)) { - $this->convertPersonalPublicShareToExternalShare(); + return $this->convertPersonalPublicShareToExternalShare(); } return $this->share; } /** - * Get share by token for accessing the poll + * Get share by token without access check * * @param string $token Token of share to get */ @@ -305,14 +311,14 @@ public function setLabel(string $label, string $token): Share { } /** - * Delete emailAddress of the personal share + * Delete email address of the personal share */ public function deleteEmailAddress(Share $share): Share { if ($share->getType() === Share::TYPE_EXTERNAL) { $share->setEmailAddress(''); return $this->shareMapper->update($share); } else { - throw new InvalidShareTypeException('Email address can only be set in external shares.'); + throw new InvalidShareTypeException('Email address can only be removed from external shares.'); } } @@ -330,11 +336,11 @@ private function convertPersonalPublicShareToExternalShare( ?string $emailAddress = null, ?string $timeZone = null, ?string $language = null, - ): void { + ): Share { // paranoia double check if (!in_array($this->share->getType(), Share::CONVERATABLE_PUBLIC_SHARES, true)) { - return; + throw new InvalidShareTypeException('Cannot convert share type ' . $this->share->getType() . ' to external share'); } $initialUserId = $this->share->getUserId(); @@ -357,6 +363,7 @@ private function convertPersonalPublicShareToExternalShare( $this->share->setUserId($this->generatePublicUserId()); $this->share = $this->shareMapper->update($this->share); $this->convertDependingObjects($initialUserId, $this->share->getUserId(), $this->share->getPollId()); + return $this->share; } /** @@ -660,21 +667,30 @@ private function sortByCategory(): void { } /** - * Validate if share type is allowed to be used in a public poll - * or is accessibale for use by the current user + * Validate if share type is allowed to be used to acess a poll + * or is accessible for use by the current user */ private function validateShareType(): void { $valid = match ($this->share->getType()) { - Share::TYPE_PUBLIC, Share::TYPE_EMAIL, Share::TYPE_EXTERNAL => true, - Share::TYPE_USER => $this->share->getUserId() === $this->userSession->getCurrentUserId(), - Share::TYPE_ADMIN => $this->share->getUserId() === $this->userSession->getCurrentUserId(), + // Public shares are always allowed + Share::TYPE_PUBLIC => true, + + // External shares are only allowed, if the user is not logged in + Share::TYPE_CONTACT, Share::TYPE_EMAIL, Share::TYPE_EXTERNAL + => !$this->userSession->getIsLoggedIn(), + + // User and Admin shares are only allowed, if the user is logged in and the share belongs to the current user + Share::TYPE_USER, Share::TYPE_ADMIN + => $this->userSession->getIsLoggedIn() && $this->share->getUserId() === $this->userSession->getCurrentUserId(), + // Note: $this->share->getUserId() is actually the group name in case of Share::TYPE_GROUP - Share::TYPE_GROUP => $this->userSession->getCurrentUser()->getIsInGroup($this->share->getUserId()), + Share::TYPE_GROUP => $this->userSession->getIsLoggedIn() && $this->userSession->getCurrentUser()->getIsInGroup($this->share->getUserId()), default => throw new ForbiddenException("Invalid share type {$this->share->getType()}"), }; + if (!$valid) { - throw new ForbiddenException("User is not allowed to use this share for poll access ({$this->share->getType()})"); + throw new ForbiddenException("User is not allowed to use this share ({$this->share->getType()})"); } } From fcfd6312001ee740ab29d9049b3a1998d9fc2dfa Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:01:25 +0200 Subject: [PATCH 10/26] added some public OCS endpoints Signed-off-by: dartcafe --- lib/Controller/ShareApiController.php | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/Controller/ShareApiController.php b/lib/Controller/ShareApiController.php index 8d8b33ff26..beac905187 100644 --- a/lib/Controller/ShareApiController.php +++ b/lib/Controller/ShareApiController.php @@ -8,6 +8,7 @@ namespace OCA\Polls\Controller; +use OCA\Polls\Attributes\ShareTokenRequired; use OCA\Polls\Model\SentResult; use OCA\Polls\Service\MailService; use OCA\Polls\Service\ShareService; @@ -16,6 +17,7 @@ use OCP\AppFramework\Http\Attribute\CORS; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; @@ -56,6 +58,53 @@ public function get(string $token): DataResponse { return $this->response(fn () => ['share' => $this->shareService->request($token)]); } + + #[CORS] + #[PublicPage] + #[ShareTokenRequired] + #[NoAdminRequired] + #[NoCSRFRequired] + #[ApiRoute(verb: 'POST', url: 'api/v1.0/s/{token}/register')] + public function register(string $token, string $displayName, string $emailAddress = '', string $timeZone = ''): DataResponse { + return $this->response(fn () => [ + 'share' => $this->shareService->register($token, $displayName, $emailAddress, $timeZone), + ], Http::STATUS_CREATED); + } + + /** + * Set EmailAddress + * @param string $token Share token + * @param string $emailAddress New email address + */ + #[CORS] + #[PublicPage] + #[ShareTokenRequired] + #[NoAdminRequired] + #[NoCSRFRequired] + #[ApiRoute(verb: 'PUT', url: 'api/v1.0/s/{token}/email/{emailAddress}')] + public function setEmailAddress(string $token, string $emailAddress = ''): DataResponse { + return $this->response(fn () => [ + 'share' => $this->shareService->setEmailAddress($this->shareService->request($token), $emailAddress) + ]); + } + + /** + * Delete email address from share + * @param string $token Share token + * @return DataResponse + */ + #[CORS] + #[PublicPage] + #[ShareTokenRequired] + #[NoAdminRequired] + #[NoCSRFRequired] + #[ApiRoute(verb: 'DELETE', url: 'api/v1.0/s/{token}/email')] + public function deleteEmailAddress(string $token): DataResponse { + return $this->response(fn () => [ + 'share' => $this->shareService->deleteEmailAddress($this->shareService->request($token)) + ]); + } + /** * Add share * @param int $pollId poll id From 1220660122fdc9fa7f718d4f1bfce14603a5633b Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:03:08 +0200 Subject: [PATCH 11/26] fix schema definition and usage Signed-off-by: dartcafe --- lib/Command/Db/ResetWatch.php | 7 +++++-- lib/Db/IndexManager.php | 6 ++++-- lib/Db/TableManager.php | 20 +++++++++++--------- lib/Migration/TableSchema.php | 34 ++++++++++++++++++++++++---------- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/lib/Command/Db/ResetWatch.php b/lib/Command/Db/ResetWatch.php index 79dfb5fe4a..4e543c1bd6 100644 --- a/lib/Command/Db/ResetWatch.php +++ b/lib/Command/Db/ResetWatch.php @@ -38,7 +38,7 @@ public function __construct( protected function runCommands(): int { $tableName = Watch::TABLE; - $indexValues = TableSchema::UNIQUE_INDICES[$tableName]; + $uniqueIndices = TableSchema::UNIQUE_INDICES[$tableName]; $columns = TableSchema::TABLES[$tableName]; $messages = $this->tableManager->removeWatch(); @@ -49,7 +49,10 @@ protected function runCommands(): int { $this->tableManager->setSchema($this->schema); $messages = $this->tableManager->createTable($tableName, $columns); - $messages[] = $this->indexManager->createIndex($tableName, $indexValues['name'], $indexValues['columns'], $indexValues['unique']); + + foreach ($uniqueIndices as $index) { + $messages[] = $this->indexManager->createIndex($tableName, $index['name'], $index['columns'], $index['unique']); + } $this->connection->migrateToSchema($this->schema); diff --git a/lib/Db/IndexManager.php b/lib/Db/IndexManager.php index f5e289a40a..d619bcea9e 100644 --- a/lib/Db/IndexManager.php +++ b/lib/Db/IndexManager.php @@ -42,8 +42,10 @@ public function setSchema(Schema &$schema): void { public function createIndices(): array { $messages = []; - foreach (TableSchema::UNIQUE_INDICES as $tableName => $values) { - $messages[] = $this->createIndex($tableName, $values['name'], $values['columns'], $values['unique']); + foreach (TableSchema::UNIQUE_INDICES as $tableName => $uniqueIndices) { + foreach ($uniqueIndices as $values) { + $messages[] = $this->createIndex($tableName, $values['name'], $values['columns'], $values['unique']); + } } foreach (TableSchema::COMMON_INDICES as $index) { diff --git a/lib/Db/TableManager.php b/lib/Db/TableManager.php index 2024928a00..f9d77a2c80 100644 --- a/lib/Db/TableManager.php +++ b/lib/Db/TableManager.php @@ -33,7 +33,6 @@ public function __construct( private OptionMapper $optionMapper, private VoteMapper $voteMapper, private Schema $schema, - private WatchMapper $watchMapper, ) { $this->setUp(); } @@ -332,16 +331,19 @@ public function removeOrphaned(): array { */ public function deleteAllDuplicates(?IOutput $output = null): array { $messages = []; - foreach (TableSchema::UNIQUE_INDICES as $tableName => $index) { - $count = $this->deleteDuplicates($tableName, $index['columns']); + foreach (TableSchema::UNIQUE_INDICES as $tableName => $uniqueIndices) { + foreach ($uniqueIndices as $index) { - if ($count) { - $messages[] = 'Removed ' . $count . ' duplicate records from ' . $this->dbPrefix . $tableName; - $this->logger->info(end($messages)); - } + $count = $this->deleteDuplicates($tableName, $index['columns']); + + if ($count) { + $messages[] = 'Removed ' . $count . ' duplicate records from ' . $this->dbPrefix . $tableName; + $this->logger->info(end($messages)); + } - if ($output && $count) { - $output->info(end($messages)); + if ($output && $count) { + $output->info(end($messages)); + } } } return $messages; diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index ea834a3bf6..ce3d124ce6 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -64,20 +64,34 @@ abstract class TableSchema { /** * define unique indices, which are not primary keys - * table => ['name' => 'indexName', 'unique' => true, 'columns' => ['column1', 'column2']] + * table => [['name' => 'indexName', 'unique' => true, 'columns' => ['column1', 'column2']], ...] */ public const UNIQUE_INDICES = [ - Option::TABLE => ['name' => 'UNIQ_options', 'unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], - Log::TABLE => ['name' => 'UNIQ_unprocessed', 'unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], - Subscription::TABLE => ['name' => 'UNIQ_subscription', 'unique' => true, 'columns' => ['poll_id', 'user_id']], + Option::TABLE => [ + ['name' => 'UNIQ_options', 'unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], + ], + Log::TABLE => [ + ['name' => 'UNIQ_unprocessed', 'unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], + ], + Subscription::TABLE => [ + ['name' => 'UNIQ_subscription', 'unique' => true, 'columns' => ['poll_id', 'user_id']], + ], Share::TABLE => [ - 'name' => 'UNIQ_shares', 'unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id'], - 'name' => 'UNIQ_token', 'unique' => true, 'columns' => ['token'] + ['name' => 'UNIQ_shares', 'unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id']], + ['name' => 'UNIQ_token', 'unique' => true, 'columns' => ['token']], + ], + Vote::TABLE => [ + ['name' => 'UNIQ_votes', 'unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], + ], + Preferences::TABLE => [ + ['name' => 'UNIQ_preferences', 'unique' => true, 'columns' => ['user_id']], + ], + Watch::TABLE => [ + ['name' => 'UNIQ_watch', 'unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], + ], + PollGroup::RELATION_TABLE => [ + ['name' => 'UNIQ_poll_group_relation', 'unique' => true, 'columns' => ['poll_id', 'group_id']], ], - Vote::TABLE => ['name' => 'UNIQ_votes', 'unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], - Preferences::TABLE => ['name' => 'UNIQ_preferences', 'unique' => true, 'columns' => ['user_id']], - Watch::TABLE => ['name' => 'UNIQ_watch', 'unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], - PollGroup::RELATION_TABLE => ['name' => 'UNIQ_poll_group_relation', 'unique' => true, 'columns' => ['poll_id', 'group_id']], ]; /** From 946b7872964b4a327c4d85bb0ddd5ae323035aaf Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:05:58 +0200 Subject: [PATCH 12/26] tidy pollmapper Signed-off-by: dartcafe --- lib/Db/PollMapper.php | 15 +-------------- lib/Service/CommentService.php | 6 +++--- lib/Service/MailService.php | 2 +- lib/Service/OptionService.php | 6 +++--- lib/Service/PollGroupService.php | 4 ++-- lib/Service/PollService.php | 30 +++++++++++++---------------- lib/Service/ShareService.php | 18 ++++++++--------- lib/Service/SubscriptionService.php | 7 ++----- lib/Service/VoteService.php | 6 +++--- lib/Service/WatchService.php | 2 +- tests/Unit/Db/PollMapperTest.php | 2 +- 11 files changed, 39 insertions(+), 59 deletions(-) diff --git a/lib/Db/PollMapper.php b/lib/Db/PollMapper.php index 7e3f20fbc7..f244437a21 100644 --- a/lib/Db/PollMapper.php +++ b/lib/Db/PollMapper.php @@ -36,7 +36,7 @@ public function __construct( * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result * @return Poll */ - public function get(int $id, bool $getDeleted = false, bool $withRoles = false): Poll { + public function get(int $id, bool $getDeleted = false): Poll { $qb = $this->buildQuery(false); $qb->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); @@ -47,19 +47,6 @@ public function get(int $id, bool $getDeleted = false, bool $withRoles = false): return $this->findEntity($qb); } - /** - * Get poll with joins for operations with permissions and user informations - * @throws \OCP\AppFramework\Db\DoesNotExistException if not found - * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result - * @return Poll - */ - public function find(int $id): Poll { - $qb = $this->buildQuery(); - $qb->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - - return $this->findEntity($qb); - } - /** * @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @return Poll[] diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index 89bd81972c..1ea7320fcb 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -36,7 +36,7 @@ public function __construct( */ public function list(int $pollId): array { try { - $this->pollMapper->get($pollId, true, withRoles: true) + $this->pollMapper->get($pollId, true) ->request(Poll::PERMISSION_COMMENT_ADD); } catch (Exception $e) { return []; @@ -66,7 +66,7 @@ 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 = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_COMMENT_ADD); if ($poll->getForceConfidentialComments()) { @@ -104,7 +104,7 @@ 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) + $this->pollMapper->get($this->comment->getPollId()) ->request(Poll::PERMISSION_COMMENT_DELETE); } diff --git a/lib/Service/MailService.php b/lib/Service/MailService.php index 5630b7bd73..5dc49b944e 100644 --- a/lib/Service/MailService.php +++ b/lib/Service/MailService.php @@ -228,7 +228,7 @@ 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) + $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); $sentResult = new SentResult(); diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index afa4797a1b..135976e079 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -199,7 +199,7 @@ public function delete(int $optionId, bool $restore = false): Option { $option = $this->optionMapper->find($optionId); if (!$option->getCurrentUserIsEntityUser()) { - $this->pollMapper->get($option->getPollId(), withRoles: true) + $this->pollMapper->get($option->getPollId()) ->request(Poll::PERMISSION_OPTION_DELETE); } @@ -331,7 +331,7 @@ 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) + $this->pollMapper->get($fromPollId) ->request(Poll::PERMISSION_POLL_ACCESS) ->request(Poll::PERMISSION_OPTION_ADD); @@ -444,7 +444,7 @@ private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): */ private function getPoll(int $pollId, string $permission = Poll::PERMISSION_POLL_ACCESS): void { if ($this->poll->getId() !== $pollId) { - $this->poll = $this->pollMapper->get($pollId, true, withRoles: true); + $this->poll = $this->pollMapper->get($pollId, true); } $this->poll->request($permission); } diff --git a/lib/Service/PollGroupService.php b/lib/Service/PollGroupService.php index 3b62142a43..03b10503e7 100644 --- a/lib/Service/PollGroupService.php +++ b/lib/Service/PollGroupService.php @@ -64,7 +64,7 @@ public function addPollToPollGroup( ?int $pollGroupId = null, ?string $pollGroupName = null, ): PollGroup { - $poll = $this->pollMapper->get($pollId, withRoles: true) + $poll = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); // Without poll group id, we create a new poll group @@ -112,7 +112,7 @@ public function removePollFromPollGroup( int $pollId, int $pollGroupId, ): ?PollGroup { - $poll = $this->pollMapper->get($pollId, withRoles: true) + $poll = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); $pollGroup = $this->pollGroupMapper->find($pollGroupId); diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index fb0998111a..fa0c453ccd 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -139,7 +139,7 @@ public function takeover(int $pollId, ?UserBase $targetUser = null): Poll { */ public function transferPoll(int|Poll $poll, string|UserBase $targetUser): Poll { if (!($poll instanceof Poll)) { - $poll = $this->pollMapper->get($poll, withRoles: true); + $poll = $this->pollMapper->get($poll); } $poll->request(Poll::PERMISSION_POLL_CHANGE_OWNER); @@ -168,13 +168,9 @@ public function transferPoll(int|Poll $poll, string|UserBase $targetUser): Poll * get poll configuration * @return Poll */ - public function get(int $pollId, $lightweight = false) { + public function get(int $pollId) { try { - if ($lightweight) { - $this->poll = $this->pollMapper->get($pollId, withRoles: true); - } else { - $this->poll = $this->pollMapper->find($pollId); - } + $this->poll = $this->pollMapper->get($pollId); $this->poll->request(Poll::PERMISSION_POLL_ACCESS); return $this->poll; } catch (DoesNotExistException $e) { @@ -184,7 +180,7 @@ public function get(int $pollId, $lightweight = false) { public function getPollOwnerFromDB(int $pollId): UserBase { try { - $poll = $this->pollMapper->get($pollId, withRoles: true); + $poll = $this->pollMapper->get($pollId); return $poll->getUser(); } catch (DoesNotExistException $e) { throw new NotFoundException('Poll not found'); @@ -248,7 +244,7 @@ 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 = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); // Validate valuess @@ -303,7 +299,7 @@ public function update(int $pollId, array $pollConfiguration): array { * @return Poll */ public function lockAnonymous(int $pollId): Poll { - $this->poll = $this->pollMapper->find($pollId); + $this->poll = $this->pollMapper->get($pollId); // Only possible, if poll is already anonymized if ($this->poll->getAnonymous() < 1) { @@ -336,7 +332,7 @@ public function setLastInteraction(int $pollId): void { * @return Poll */ public function toggleArchive(int $pollId): Poll { - $this->poll = $this->pollMapper->find($pollId) + $this->poll = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_DELETE); $this->poll->setDeleted($this->poll->getDeleted() ? 0 : time()); @@ -357,7 +353,7 @@ 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) ->request(Poll::PERMISSION_POLL_DELETE); } catch (DoesNotExistException $e) { throw new AlreadyDeletedException('Poll not found, assume already deleted'); @@ -374,7 +370,7 @@ public function delete(int $pollId): Poll { * @return Poll */ public function close(int $pollId): Poll { - $this->pollMapper->get($pollId, withRoles: true) + $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); return $this->toggleClose($pollId, time() - 5); } @@ -384,7 +380,7 @@ public function close(int $pollId): Poll { * @return Poll */ public function reopen(int $pollId): Poll { - $this->pollMapper->get($pollId, withRoles: true) + $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); return $this->toggleClose($pollId, 0); } @@ -394,7 +390,7 @@ public function reopen(int $pollId): Poll { * @return Poll */ private function toggleClose(int $pollId, int $expiry): Poll { - $this->poll = $this->pollMapper->find($pollId) + $this->poll = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); $this->poll->setExpire($expiry); @@ -414,7 +410,7 @@ private function toggleClose(int $pollId, int $expiry): Poll { * @return Poll */ public function clone(int $pollId): Poll { - $origin = $this->pollMapper->get($pollId, withRoles: true) + $origin = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_ACCESS); $this->appSettings->getPollCreationAllowed(); @@ -446,7 +442,7 @@ public function clone(int $pollId): Poll { * */ public function getParticipantsEmailAddresses(int $pollId): array { - $this->poll = $this->pollMapper->get($pollId, withRoles: true) + $this->poll = $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); $votes = $this->voteMapper->findParticipantsByPoll($this->poll->getId()); diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index 1f896b2f83..0e54fb0eea 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -88,7 +88,7 @@ public function __construct( public function list(int $pollOrPollGroupId, string $purpose = 'poll'): array { try { if ($purpose === 'poll') { - $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true) + $poll = $this->pollMapper->get($pollOrPollGroupId) ->request(Poll::PERMISSION_POLL_EDIT); $this->shares = $this->shareMapper->findByPoll($pollOrPollGroupId, $poll->getPollGroups()); } else { @@ -113,7 +113,7 @@ public function list(int $pollOrPollGroupId, string $purpose = 'poll'): array { */ public function listNotInvited(int $pollId): array { try { - $this->pollMapper->get($pollId, withRoles: true) + $this->pollMapper->get($pollId) ->request(Poll::PERMISSION_POLL_EDIT); $this->shares = $this->shareMapper->findByPollNotInvited($pollId); } catch (ForbiddenException $e) { @@ -157,7 +157,7 @@ public function request(string $token): Share { $this->validateShareType(); - $poll = $this->pollMapper->get($this->share->getPollId(), withRoles: true); + $poll = $this->pollMapper->get($this->share->getPollId()); // deletes the displayname, to avoid displayname preset in case of public polls if ($this->share->getType() === Share::TYPE_PUBLIC) { @@ -207,7 +207,7 @@ 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) + $this->pollMapper->get($this->share->getPollId()) ->request(Poll::PERMISSION_POLL_EDIT); // ATM only type user can transform to type admin and vice versa @@ -227,7 +227,7 @@ 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) + $this->pollMapper->get($this->share->getPollId()) ->request(Poll::PERMISSION_POLL_EDIT); $this->share->setPublicPollEmail($value); $this->share = $this->shareMapper->update($this->share); @@ -291,7 +291,7 @@ 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) + $this->pollMapper->get($this->share->getPollId()) ->request(Poll::PERMISSION_POLL_EDIT); $this->share->setLabel($label); @@ -460,7 +460,7 @@ public function delete(Share $share, bool $restore = false): Share { $this->pollGroupMapper->find($share->getGroupId()) ->request(PollGroup::PERMISSION_POLL_GROUP_EDIT); } else { - $this->pollMapper->get($share->getPollId(), withRoles: true) + $this->pollMapper->get($share->getPollId()) ->request(Poll::PERMISSION_POLL_EDIT); } @@ -490,7 +490,7 @@ 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) + $this->pollMapper->get($share->getPollId()) ->request(Poll::PERMISSION_POLL_EDIT); $share->setLocked($unlock ? 0 : time()); @@ -613,7 +613,7 @@ public function add( string $purpose = 'poll', ): Share { if ($purpose === 'poll') { - $poll = $this->pollMapper->get($pollOrPollGroupId, withRoles: true) + $poll = $this->pollMapper->get($pollOrPollGroupId) ->request(Poll::PERMISSION_POLL_EDIT) ->request(Poll::PERMISSION_SHARE_ADD); diff --git a/lib/Service/SubscriptionService.php b/lib/Service/SubscriptionService.php index b687a76bfd..b6e93f7296 100644 --- a/lib/Service/SubscriptionService.php +++ b/lib/Service/SubscriptionService.php @@ -27,8 +27,7 @@ public function __construct( } public function get(int $pollId): bool { - $this->pollMapper->get($pollId, true, withRoles: true) - ->request(Poll::PERMISSION_POLL_ACCESS); + $this->pollMapper->get($pollId, true)->request(Poll::PERMISSION_POLL_ACCESS); try { $this->subscriptionMapper->findByPollAndUser($pollId, $this->userSession->getCurrentUserId()); @@ -53,9 +52,7 @@ 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)->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 476b6c0521..35c7069395 100644 --- a/lib/Service/VoteService.php +++ b/lib/Service/VoteService.php @@ -40,7 +40,7 @@ public function __construct( * @return Vote[] */ public function list(int $pollId): array { - $poll = $this->pollMapper->get($pollId, true, withRoles: true) + $poll = $this->pollMapper->get($pollId, true) ->request(Poll::PERMISSION_POLL_ACCESS); try { @@ -87,7 +87,7 @@ public function set(Option|int $optionOrOptionIdoptionId, string $setTo): ?Vote } else { $option = $this->optionMapper->find($optionOrOptionIdoptionId); } - $poll = $this->pollMapper->get($option->getPollId(), withRoles: true) + $poll = $this->pollMapper->get($option->getPollId()) ->request(Poll::PERMISSION_VOTE_EDIT); if ($option->getIsLocked()) { @@ -152,7 +152,7 @@ public function deleteUserFromPoll(int $pollId, string $userId = '', bool $delet $checkRight = Poll::PERMISSION_VOTE_EDIT; } - $this->pollMapper->get($pollId, withRoles: true)->request($checkRight); + $this->pollMapper->get($pollId)->request($checkRight); return $this->delete($pollId, $userId, $deleteOnlyOrphaned); } diff --git a/lib/Service/WatchService.php b/lib/Service/WatchService.php index b8cc2c939b..bb8bc8272c 100644 --- a/lib/Service/WatchService.php +++ b/lib/Service/WatchService.php @@ -36,7 +36,7 @@ public function __construct( */ public function watchUpdates(int $pollId, string $mode, ?int $offset = null): array { if ($pollId) { - $this->pollMapper->get($pollId, true, withRoles: true) + $this->pollMapper->get($pollId, true) ->request(Poll::PERMISSION_POLL_ACCESS); } diff --git a/tests/Unit/Db/PollMapperTest.php b/tests/Unit/Db/PollMapperTest.php index 688800543e..f85ac5d772 100644 --- a/tests/Unit/Db/PollMapperTest.php +++ b/tests/Unit/Db/PollMapperTest.php @@ -57,7 +57,7 @@ public function testUpdate() { */ public function testFind() { foreach ($this->polls as $poll) { - $this->assertInstanceOf(Poll::class, $this->pollMapper->find($poll->getId())); + $this->assertInstanceOf(Poll::class, $this->pollMapper->get($poll->getId())); } } From f2208903ac7ec5b8f350077280a981290c8fe5d5 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:38:36 +0200 Subject: [PATCH 13/26] just a test Signed-off-by: dartcafe --- tests/Unit/Db/PollMapperTest.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/Unit/Db/PollMapperTest.php b/tests/Unit/Db/PollMapperTest.php index f85ac5d772..e6e38034e8 100644 --- a/tests/Unit/Db/PollMapperTest.php +++ b/tests/Unit/Db/PollMapperTest.php @@ -31,12 +31,17 @@ protected function setUp(): void { $this->fm->instance('OCA\Polls\Db\Poll'), $this->fm->instance('OCA\Polls\Db\Poll') ]; - foreach ($this->polls as &$poll) { + // foreach ($this->polls as &$poll) { + // $poll = $this->pollMapper->insert($poll); + // } + // unset($poll); + } + + public function testAdd() { + foreach ($this->polls as $poll) { $poll = $this->pollMapper->insert($poll); } - unset($poll); } - /** * testUpdate */ From 391524b2dcc2acb5b83b739002d0c6137e468fa4 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:40:59 +0200 Subject: [PATCH 14/26] more Signed-off-by: dartcafe --- tests/Unit/Db/PollMapperTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Unit/Db/PollMapperTest.php b/tests/Unit/Db/PollMapperTest.php index e6e38034e8..8a72971806 100644 --- a/tests/Unit/Db/PollMapperTest.php +++ b/tests/Unit/Db/PollMapperTest.php @@ -40,6 +40,7 @@ protected function setUp(): void { public function testAdd() { foreach ($this->polls as $poll) { $poll = $this->pollMapper->insert($poll); + $this->assertInstanceOf(Poll::class, $this->pollMapper->get($poll->getId())); } } /** From 279c516d079ebb38f97fe210253af490cd970f72 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Sun, 10 Aug 2025 18:53:11 +0200 Subject: [PATCH 15/26] don't create deleted test units Signed-off-by: dartcafe --- lib/Service/PollService.php | 2 +- tests/Unit/Db/PollMapperTest.php | 12 +++--------- tests/Unit/Factories/PollFactory.php | 16 +++++++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php index fa0c453ccd..d3975b2880 100644 --- a/lib/Service/PollService.php +++ b/lib/Service/PollService.php @@ -214,7 +214,7 @@ public function add(string $type, string $title, string $votingVariant = Poll::V // create new poll before resetting all values to // ensure that the poll has all required values and an id - // latter checks mai fail if the poll has no id + // later checks may fail if the poll has no id $this->poll = $this->pollMapper->insert($this->poll); $this->poll->setDescription(''); diff --git a/tests/Unit/Db/PollMapperTest.php b/tests/Unit/Db/PollMapperTest.php index 8a72971806..f85ac5d772 100644 --- a/tests/Unit/Db/PollMapperTest.php +++ b/tests/Unit/Db/PollMapperTest.php @@ -31,18 +31,12 @@ protected function setUp(): void { $this->fm->instance('OCA\Polls\Db\Poll'), $this->fm->instance('OCA\Polls\Db\Poll') ]; - // foreach ($this->polls as &$poll) { - // $poll = $this->pollMapper->insert($poll); - // } - // unset($poll); - } - - public function testAdd() { - foreach ($this->polls as $poll) { + foreach ($this->polls as &$poll) { $poll = $this->pollMapper->insert($poll); - $this->assertInstanceOf(Poll::class, $this->pollMapper->get($poll->getId())); } + unset($poll); } + /** * testUpdate */ diff --git a/tests/Unit/Factories/PollFactory.php b/tests/Unit/Factories/PollFactory.php index 8893d25200..7ce5f343ba 100644 --- a/tests/Unit/Factories/PollFactory.php +++ b/tests/Unit/Factories/PollFactory.php @@ -5,28 +5,30 @@ */ use League\FactoryMuffin\Faker\Facade as Faker; - +use OCA\Polls\Db\Poll; /** * General factory for the poll model. */ $fm->define('OCA\Polls\Db\Poll')->setDefinitions([ 'type' => 'textPoll', 'title' => Faker::text(124), + 'votingVariant' => Poll::VARIANT_SIMPLE, 'description' => Faker::text(255), 'owner' => Faker::firstNameMale(), 'created' => function () { $date = new DateTime('today'); return $date->getTimestamp(); }, - 'expire' => function () { - $date = new DateTime('tomorrow'); + 'lastInteraction' => function () { + $date = new DateTime('today'); return $date->getTimestamp(); }, - 'deleted' => function () { - $date = new DateTime('+1 month'); + 'expire' => function () { + $date = new DateTime('tomorrow'); return $date->getTimestamp(); }, - 'access' => 'public', + 'deleted' => 0, + 'access' => Poll::ACCESS_OPEN, 'anonymous' => 0, 'allowComment' => 1, 'allowMaybe' => 1, @@ -37,7 +39,7 @@ }, 'voteLimit' => 0, 'optionLimit' => 0, - 'showResults' => 'always', + 'showResults' => Poll::SHOW_RESULTS_ALWAYS, 'adminAccess' => 0, 'hideBookedUp' => 0, 'useNo' => 0, From 20cd64fbb60e129205a0a510e9d2cbd968fa1f13 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Wed, 13 Aug 2025 13:29:27 +0200 Subject: [PATCH 16/26] Change indices handling and optimization Signed-off-by: dartcafe --- appinfo/info.xml | 18 +-- lib/AppInfo/Application.php | 3 + lib/Command/Db/CleanMigrations.php | 3 +- lib/Command/Db/CreateIndices.php | 13 ++- lib/Command/Db/Purge.php | 4 + lib/Command/Db/Rebuild.php | 21 ++-- lib/Command/Db/RemoveFKConstraints.php | 71 ++++++++++++ lib/Command/Db/RemoveIndices.php | 38 +------ lib/Command/Db/RemoveUniqueIndices.php | 60 ++++++++++ lib/Command/Db/ResetWatch.php | 8 +- lib/Command/Db/removeOrphanedVotes.php | 64 ----------- lib/Db/IndexManager.php | 45 +++++--- lib/Db/Option.php | 13 ++- lib/Db/OptionMapper.php | 10 +- lib/Db/PollMapper.php | 4 +- lib/Db/TableManager.php | 30 +++-- lib/Db/Vote.php | 3 + lib/Db/VoteMapper.php | 11 +- lib/Helper/Hash.php | 43 ++++++++ lib/Listener/AddMissingIndicesListener.php | 35 ++++++ lib/Migration/RepairSteps/CreateIndices.php | 6 +- lib/Migration/RepairSteps/Install.php | 2 +- lib/Migration/TableSchema.php | 53 +++++++-- .../Version080300Date20250812231603.php | 103 ++++++++++++++++++ lib/Model/UserBase.php | 4 +- lib/Service/OptionService.php | 3 +- lib/UserSession.php | 3 +- 27 files changed, 483 insertions(+), 188 deletions(-) create mode 100644 lib/Command/Db/RemoveFKConstraints.php create mode 100644 lib/Command/Db/RemoveUniqueIndices.php delete mode 100644 lib/Command/Db/removeOrphanedVotes.php create mode 100644 lib/Helper/Hash.php create mode 100644 lib/Listener/AddMissingIndicesListener.php create mode 100644 lib/Migration/Version080300Date20250812231603.php diff --git a/appinfo/info.xml b/appinfo/info.xml index b707ebe5ab..0355c936fe 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -19,9 +19,9 @@ https://help.nextcloud.com/tag/polls-app https://github.com/nextcloud/polls/issues https://github.com/nextcloud/polls.git - https://raw.githubusercontent.com/nextcloud/polls/master/screenshots/overview.png - https://raw.githubusercontent.com/nextcloud/polls/master/screenshots/vote.png - https://raw.githubusercontent.com/nextcloud/polls/master/screenshots/edit-poll.png + https://raw.githubusercontent.com/nextcloud/polls/main/screenshots/overview.png + https://raw.githubusercontent.com/nextcloud/polls/main/screenshots/vote.png + https://raw.githubusercontent.com/nextcloud/polls/main/screenshots/edit-poll.png @@ -47,15 +47,17 @@ - OCA\Polls\Command\Share\Add - OCA\Polls\Command\Share\Remove + OCA\Polls\Command\Db\CleanMigrations + OCA\Polls\Command\Db\CreateIndices OCA\Polls\Command\Db\Purge - OCA\Polls\Command\Db\RemoveIndices OCA\Polls\Command\Db\Rebuild - OCA\Polls\Command\Db\CreateIndices - OCA\Polls\Command\Db\CleanMigrations + OCA\Polls\Command\Db\RemoveFKConstraints + OCA\Polls\Command\Db\RemoveIndices + OCA\Polls\Command\Db\RemoveUniqueIndices OCA\Polls\Command\Db\ResetWatch OCA\Polls\Command\Poll\TransferOwnership + OCA\Polls\Command\Share\Add + OCA\Polls\Command\Share\Remove OCA\Polls\Settings\AdminSettings diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d92633f54e..48ea5b9d75 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -44,6 +44,7 @@ use OCA\Polls\Event\ShareTypeChangedEvent; use OCA\Polls\Event\VoteEvent; use OCA\Polls\Event\VoteSetEvent; +use OCA\Polls\Listener\AddMissingIndicesListener; use OCA\Polls\Listener\CommentListener; use OCA\Polls\Listener\GroupDeletedListener; use OCA\Polls\Listener\OptionListener; @@ -64,6 +65,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Collaboration\Reference\RenderReferenceEvent; +use OCP\DB\Events\AddMissingIndicesEvent; use OCP\Group\Events\GroupDeletedEvent; use OCP\IAppConfig; use OCP\IDBConnection; @@ -91,6 +93,7 @@ public function register(IRegistrationContext $context): void { include_once __DIR__ . '/../../vendor/autoload.php'; $this->registerServices($context); + $context->registerEventListener(AddMissingIndicesEvent::class, AddMissingIndicesListener::class); $context->registerEventListener(RenderReferenceEvent::class, PollsReferenceListener::class); $context->registerMiddleWare(RequestAttributesMiddleware::class); $context->registerNotifierService(Notifier::class); diff --git a/lib/Command/Db/CleanMigrations.php b/lib/Command/Db/CleanMigrations.php index 95fedcc261..1ff2b04468 100644 --- a/lib/Command/Db/CleanMigrations.php +++ b/lib/Command/Db/CleanMigrations.php @@ -20,8 +20,7 @@ class CleanMigrations extends Command { protected string $name = parent::NAME_PREFIX . 'db:clean-migrations'; protected string $description = 'Remove old migrations entries from Nextcloud\'s migration table'; protected array $operationHints = [ - 'All polls tables will get checked against the current schema.', - 'NO data migration will be executed, so make sure you have a backup of your database.', + 'Note: NO data migration will be executed, so make sure you have a backup of your database.', ]; public function __construct( diff --git a/lib/Command/Db/CreateIndices.php b/lib/Command/Db/CreateIndices.php index 8d1aa7b429..1f8fefd6b7 100644 --- a/lib/Command/Db/CreateIndices.php +++ b/lib/Command/Db/CreateIndices.php @@ -18,10 +18,11 @@ */ class CreateIndices extends Command { protected string $name = parent::NAME_PREFIX . 'index:create'; - protected string $description = 'Add all indices and foreign key constraints'; + protected string $description = 'Add unique indices and foreign key constraints'; protected array $operationHints = [ - 'Adds indices and foreing key constraints.', - 'NO data migration will be executed, so make sure you have a backup of your database.', + 'Adds unique indices and foreign key constraints.', + 'To create the optional indices, run the command \'occ db:add-missing-indices\'', + 'Note: NO data migration will be executed, so make sure you have a backup of your database.', ]; public function __construct( @@ -38,7 +39,7 @@ protected function runCommands(): int { $this->schema = $this->connection->createSchema(); $this->indexManager->setSchema($this->schema); $this->addForeignKeyConstraints(); - $this->addIndices(); + $this->addUniqueIndices(); $this->connection->migrateToSchema($this->schema); return 0; @@ -56,9 +57,9 @@ private function addForeignKeyConstraints(): void { /** * Create index for $table */ - private function addIndices(): void { + private function addUniqueIndices(): void { $this->printComment('Add indices'); - $messages = $this->indexManager->createIndices(); + $messages = $this->indexManager->createUniqueIndices(); $this->printInfo($messages, ' - '); } } diff --git a/lib/Command/Db/Purge.php b/lib/Command/Db/Purge.php index 4dbcba7f4a..26bf499bae 100644 --- a/lib/Command/Db/Purge.php +++ b/lib/Command/Db/Purge.php @@ -25,6 +25,7 @@ class Purge extends Command { ' - delete Polls\'s app config records from oc_appconfig.', ' ', 'after running this command call \'occ app:remove polls \'', + 'Note: Make sure you have a backup of your database.', ]; public function __construct( @@ -38,6 +39,9 @@ protected function runCommands(): int { $this->tableManager->setConnection($this->connection); $messages = $this->tableManager->purgeTables(); $this->printInfo($messages, ' - '); + $this->printInfo($messages, 'Polls has been completely wiped off the database.'); + $this->printInfo($messages, ''); + $this->printInfo($messages, '!!! Now call \'occ app:remove polls \' to remove the app completely.'); return 0; } } diff --git a/lib/Command/Db/Rebuild.php b/lib/Command/Db/Rebuild.php index a55cd52a25..623815c405 100644 --- a/lib/Command/Db/Rebuild.php +++ b/lib/Command/Db/Rebuild.php @@ -21,14 +21,15 @@ class Rebuild extends Command { protected string $name = parent::NAME_PREFIX . 'db:rebuild'; protected string $description = 'Rebuilds poll\'s table structure'; protected array $operationHints = [ - 'All polls tables will get checked against the current schema.', - 'NO data migration will be executed, so make sure you have a backup of your database.', - '', + 'All polls tables will get checked and eventually updated against the current schema.', '*****************************', '** Please understand **', '*****************************', - 'The process will also recreate all indices and foreign key constraints.', + 'The process will also remove all optional indices.', 'This can lead to a database performance impact on the app after the recreation is done.', + '', + 'To recreate the optional indices, run the command \'occ db:add-missing-indices\'', + 'Note: NO data migration will be executed, so make sure you have a backup of your database.', ]; public function __construct( @@ -69,9 +70,11 @@ protected function runCommands(): int { $this->printComment('Step 5. Remove invalid records (orphaned and duplicates)'); $this->cleanTables(); - $this->printComment('Step 6. Recreate indices and foreign key constraints'); + $this->printComment('Step 6. Recreate unique indices and foreign key constraints'); $this->addForeignKeyConstraints(); - $this->addIndices(); + $this->addUniqueIndices(); + + $this->printComment('Execute \'occ db:add-missing-indices\' to add missing optional indices'); $this->connection->migrateToSchema($this->schema); @@ -90,9 +93,9 @@ private function addForeignKeyConstraints(): void { /** * Create index for $table */ - private function addIndices(): void { - $this->printComment(' - Add indices'); - $messages = $this->indexManager->createIndices(); + private function addUniqueIndices(): void { + $this->printComment(' - Add unique indices'); + $messages = $this->indexManager->createUniqueIndices(); $this->printInfo($messages, ' '); } diff --git a/lib/Command/Db/RemoveFKConstraints.php b/lib/Command/Db/RemoveFKConstraints.php new file mode 100644 index 0000000000..d33e51594d --- /dev/null +++ b/lib/Command/Db/RemoveFKConstraints.php @@ -0,0 +1,71 @@ +schema = $this->connection->createSchema(); + $this->indexManager->setSchema($this->schema); + $this->deleteForeignKeyConstraints(); + $this->deleteGenericIndices(); + $this->connection->migrateToSchema($this->schema); + return 0; + } + + /** + * remove all generic indices (the only generic indices should + * result from the FK Constraints) + */ + private function deleteGenericIndices(): void { + $this->printComment('Remove generic indices'); + $messages = $this->indexManager->removeAllGenericIndices(); + $this->printInfo($messages, ' - '); + } + + /** + * remove on delete fk contraint from all tables referencing the main polls table + */ + private function deleteForeignKeyConstraints(): void { + $this->printComment('Remove foreign key constraints and generic indices'); + $messages = $this->indexManager->removeAllForeignKeyConstraints(); + $this->printInfo($messages, ' - '); + } +} diff --git a/lib/Command/Db/RemoveIndices.php b/lib/Command/Db/RemoveIndices.php index 0b99f353d2..da9b7583d8 100644 --- a/lib/Command/Db/RemoveIndices.php +++ b/lib/Command/Db/RemoveIndices.php @@ -18,10 +18,11 @@ */ class RemoveIndices extends Command { protected string $name = parent::NAME_PREFIX . 'index:remove'; - protected string $description = 'Remove all indices and foreign key constraints'; + protected string $description = 'Remove all optional indices'; protected array $operationHints = [ - 'Removes all indices and foreign key constraints.', - 'NO data migration will be executed, so make sure you have a backup of your database.', + 'Removes all optional indices. Removing them may decrease your database query performance.', + 'To recreate optional indices, run the command \'occ db:add-missing-indices\'', + 'Note: NO data migration will be executed, so make sure you have a backup of your database.', ]; public function __construct( @@ -36,45 +37,16 @@ protected function runCommands(): int { // remove constraints and indices $this->schema = $this->connection->createSchema(); $this->indexManager->setSchema($this->schema); - $this->deleteForeignKeyConstraints(); - $this->deleteGenericIndices(); - $this->deleteUniqueIndices(); $this->deleteNamedIndices(); $this->connection->migrateToSchema($this->schema); return 0; } - /** - * remove on delete fk contraint from all tables referencing the main polls table - */ - private function deleteForeignKeyConstraints(): void { - $this->printComment('Remove foreign key constraints and generic indices'); - $messages = $this->indexManager->removeAllForeignKeyConstraints(); - $this->printInfo($messages, ' - '); - } - - /** - * remove all generic indices - */ - private function deleteGenericIndices(): void { - $this->printComment('Remove generic indices'); - $messages = $this->indexManager->removeAllGenericIndices(); - $this->printInfo($messages, ' - '); - } - - /** - * remove all unique indices - */ - private function deleteUniqueIndices(): void { - $this->printComment('Remove unique indices'); - $messages = $this->indexManager->removeAllUniqueIndices(); - $this->printInfo($messages, ' - '); - } /** * remove all named indices */ private function deleteNamedIndices(): void { - $this->printComment('Remove common indices'); + $this->printComment('Remove optional indices'); $messages = $this->indexManager->removeNamedIndices(); $this->printInfo($messages, ' - '); } diff --git a/lib/Command/Db/RemoveUniqueIndices.php b/lib/Command/Db/RemoveUniqueIndices.php new file mode 100644 index 0000000000..8492f66828 --- /dev/null +++ b/lib/Command/Db/RemoveUniqueIndices.php @@ -0,0 +1,60 @@ +schema = $this->connection->createSchema(); + $this->indexManager->setSchema($this->schema); + $this->deleteUniqueIndices(); + $this->connection->migrateToSchema($this->schema); + return 0; + } + + /** + * remove all unique indices + */ + private function deleteUniqueIndices(): void { + $this->printComment('Remove unique indices'); + $messages = $this->indexManager->removeAllUniqueIndices(); + $this->printInfo($messages, ' - '); + } +} diff --git a/lib/Command/Db/ResetWatch.php b/lib/Command/Db/ResetWatch.php index 4e543c1bd6..aca55c6c45 100644 --- a/lib/Command/Db/ResetWatch.php +++ b/lib/Command/Db/ResetWatch.php @@ -38,8 +38,6 @@ public function __construct( protected function runCommands(): int { $tableName = Watch::TABLE; - $uniqueIndices = TableSchema::UNIQUE_INDICES[$tableName]; - $columns = TableSchema::TABLES[$tableName]; $messages = $this->tableManager->removeWatch(); $this->printInfo($messages, ' - '); @@ -48,10 +46,10 @@ protected function runCommands(): int { $this->indexManager->setSchema($this->schema); $this->tableManager->setSchema($this->schema); - $messages = $this->tableManager->createTable($tableName, $columns); + $messages = $this->tableManager->createTable($tableName); - foreach ($uniqueIndices as $index) { - $messages[] = $this->indexManager->createIndex($tableName, $index['name'], $index['columns'], $index['unique']); + foreach (TableSchema::UNIQUE_INDICES[$tableName] as $name => $definition) { + $messages[] = $this->indexManager->createIndex($tableName, $name, $definition['columns'], $definition['unique']); } $this->connection->migrateToSchema($this->schema); diff --git a/lib/Command/Db/removeOrphanedVotes.php b/lib/Command/Db/removeOrphanedVotes.php deleted file mode 100644 index be787c1693..0000000000 --- a/lib/Command/Db/removeOrphanedVotes.php +++ /dev/null @@ -1,64 +0,0 @@ -schema = $this->connection->createSchema(); - $this->indexManager->setSchema($this->schema); - $this->addForeignKeyConstraints(); - $this->addIndices(); - $this->connection->migrateToSchema($this->schema); - - return 0; - } - - /** - * add an on delete fk contraint to all tables referencing the main polls table - */ - private function addForeignKeyConstraints(): void { - $this->printComment('Add foreign key constraints'); - $messages = $this->indexManager->createForeignKeyConstraints(); - $this->printInfo($messages, ' - '); - } - - /** - * Create index for $table - */ - private function addIndices(): void { - $this->printComment('Add indices'); - $messages = $this->indexManager->createIndices(); - $this->printInfo($messages, ' - '); - } -} diff --git a/lib/Db/IndexManager.php b/lib/Db/IndexManager.php index d619bcea9e..6f2fe43334 100644 --- a/lib/Db/IndexManager.php +++ b/lib/Db/IndexManager.php @@ -35,21 +35,37 @@ public function setSchema(Schema &$schema): void { } /** - * Create all indices + * Create unique indices * * @return string[] logged messages */ - public function createIndices(): array { + public function createUniqueIndices(): array { $messages = []; foreach (TableSchema::UNIQUE_INDICES as $tableName => $uniqueIndices) { - foreach ($uniqueIndices as $values) { - $messages[] = $this->createIndex($tableName, $values['name'], $values['columns'], $values['unique']); + foreach ($uniqueIndices as $name => $definition) { + $messages[] = $this->createIndex($tableName, $name, $definition['columns'], $definition['unique']); } } + return $messages; + } + + /** + * Create optional indices + * Usually they should be created by the AddMissingIndicesListener + * + * @return string[] logged messages + */ + public function createOptionalIndices(): array { + $messages = []; - foreach (TableSchema::COMMON_INDICES as $index) { - $messages[] = $this->createIndex($index['table'], $index['name'], $index['columns'], $index['unique']); + // Only install optional indices if this is the first installation of polls + // Leave the index creation for existing installations to AddMissingIndicesListener, + // because initializing and rebuild of an index can take a long time + foreach (TableSchema::OPTIONAL_INDICES as $table => $indices) { + foreach ($indices as $name => $definition) { + $messages[] = $this->createIndex($table, $name, $definition['columns'], $definition['unique'] ?? false); + } } return $messages; @@ -144,11 +160,8 @@ public function removeAllForeignKeyConstraints(): array { public function removeAllGenericIndices(): array { $messages = []; - foreach (TableSchema::FK_INDICES as $child) { - foreach (array_keys($child) as $table) { - $messages = array_merge($messages, $this->removeForeignKeysFromTable($table)); - $messages = array_merge($messages, $this->removeGenericIndicesFromTable($table)); - } + foreach (array_keys(TableSchema::TABLES) as $table) { + $messages = array_merge($messages, $this->removeGenericIndicesFromTable($table)); } return $messages; @@ -161,10 +174,12 @@ public function removeAllGenericIndices(): array { public function removeNamedIndices(): array { $messages = []; - foreach (TableSchema::COMMON_INDICES as $index) { - $message = $this->removeNamedIndexFromTable($index['table'], $index['name']); - if ($message !== null && $message !== '') { - $messages[] = $message; + foreach (TableSchema::OPTIONAL_INDICES as $table => $indices) { + foreach (array_keys($indices) as $name) { + $message = $this->removeNamedIndexFromTable($table, $name); + if ($message !== null && $message !== '') { + $messages[] = $message; + } } } diff --git a/lib/Db/Option.php b/lib/Db/Option.php index 8ac8af5d88..7d0b986eb4 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -14,6 +14,7 @@ use DateTimeZone; use JsonSerializable; use OCA\Polls\Exceptions\InsufficientAttributesException; +use OCA\Polls\Helper\Hash; use OCA\Polls\Model\SimpleOption; use OCA\Polls\Model\UserBase; use OCP\IL10N; @@ -36,6 +37,8 @@ * @method void setPollOptionText(string $value) * @method string getPollOptionHash() * @method void setPollOptionHash(string $value) + * @method string getPollOptionHashBin() + * @method void setPollOptionHashBin(string $value) * @method int getReleased() * @method void setReleased(int $value) * @method int getTimestamp() @@ -62,6 +65,7 @@ class Option extends EntityWithUser implements JsonSerializable { protected int $pollId = 0; protected string $pollOptionText = ''; protected string $pollOptionHash = ''; + protected string $pollOptionHashBin = ''; protected int $timestamp = 0; protected int $duration = 0; protected int $order = 0; @@ -220,7 +224,14 @@ public function syncOption(): void { } private function updateHash(): void { - $this->setPollOptionHash(hash('md5', $this->getPollId() . $this->getPollOptionText() . $this->getTimestamp())); + $this->setPollOptionHash(Hash::getOptionHash( + $this->getPollId(), + $this->getPollOptionText() + )); + $this->setPollOptionHashBin((Hash::getOptionHashBin( + $this->getPollId(), + $this->getPollOptionText() + ))); } public function getPollOptionText(): string { diff --git a/lib/Db/OptionMapper.php b/lib/Db/OptionMapper.php index d28b3b0679..f9c63a0aa7 100644 --- a/lib/Db/OptionMapper.php +++ b/lib/Db/OptionMapper.php @@ -66,13 +66,13 @@ public function findByPoll(int $pollId, bool $hideResults = false, bool $getDele /** * @return Option * @param int $pollId - * @param string $pollOptionText option text + * @param string $pollOptionHashBin option text as binary hashed value (pollId and PollOptionText) * @param bool $getDeleted also search for deleted options */ - public function findByPollAndText(int $pollId, string $pollOptionText, bool $getDeleted = false): Option { + public function findByPollAndText(int $pollId, string $pollOptionHashBin, bool $getDeleted = false): Option { $qb = $this->buildQuery(); $qb->where($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_text', $qb->createNamedParameter($pollOptionText, IQueryBuilder::PARAM_STR))); + ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_hash_bin', $qb->createNamedParameter($pollOptionHashBin, IQueryBuilder::PARAM_STR))); if (!$getDeleted) { $qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT))); } @@ -235,7 +235,7 @@ protected function joinVotesCount( $joinAlias, $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), - $qb->expr()->eq($joinAlias . '.vote_option_text', $fromAlias . '.poll_option_text'), + $qb->expr()->eq($joinAlias . '.vote_option_hash_bin', $fromAlias . '.poll_option_hash_bin'), ) ) // Count number of votes for this option @@ -290,7 +290,7 @@ protected function joinCurrentUserVote( $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), $qb->expr()->eq($joinAlias . '.user_id', $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR)), - $qb->expr()->eq($joinAlias . '.vote_option_text', $fromAlias . '.poll_option_text'), + $qb->expr()->eq($joinAlias . '.vote_option_hash_bin', $fromAlias . '.poll_option_hash_bin'), ) ); } diff --git a/lib/Db/PollMapper.php b/lib/Db/PollMapper.php index f244437a21..c39358cb10 100644 --- a/lib/Db/PollMapper.php +++ b/lib/Db/PollMapper.php @@ -171,7 +171,7 @@ public function deleteByUserId(string $userId): void { /** * Build the enhanced query with joined tables */ - protected function buildQuery($detailed = true): IQueryBuilder { + protected function buildQuery(bool $detailed = true): IQueryBuilder { $qb = $this->db->getQueryBuilder(); $qb->select(self::TABLE . '.*') @@ -434,7 +434,7 @@ protected function subQueryOrphanedVotesCount( $optionAlias, $expr->andX( $expr->eq($optionAlias . '.poll_id', $subAlias . '.poll_id'), - $expr->eq($optionAlias . '.poll_option_text', $subAlias . '.vote_option_text'), + $expr->eq($optionAlias . '.poll_option_hash_bin', $subAlias . '.vote_option_hash_bin'), $expr->eq($optionAlias . '.deleted', $expr->literal(0, IQueryBuilder::PARAM_INT)) ) ) diff --git a/lib/Db/TableManager.php b/lib/Db/TableManager.php index f9d77a2c80..cb84e3abef 100644 --- a/lib/Db/TableManager.php +++ b/lib/Db/TableManager.php @@ -13,6 +13,7 @@ use Doctrine\DBAL\Types\Type; use Exception; use OCA\Polls\AppConstants; +use OCA\Polls\Helper\Hash; use OCA\Polls\Migration\TableSchema; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IConfig; @@ -59,8 +60,6 @@ public function setConnection(IDBConnection &$connection): void { */ public function purgeTables(): array { $messages = []; - - // drop all child tables $droppedTables = []; // First drop all tables that have foreign key constraints @@ -90,7 +89,7 @@ public function purgeTables(): array { } } - if (!$droppedTables) { + if ($droppedTables) { $this->logger->info('Dropped tables', $droppedTables); } @@ -142,17 +141,17 @@ public function removeWatch(): array { * * @psalm-return non-empty-list */ - public function createTable(string $tableName, array $columns): array { + public function createTable(string $tableName): array { $messages = []; + $columns = TableSchema::TABLES[$tableName]; + $ocTable = $this->dbPrefix . $tableName; - $tableName = $this->dbPrefix . $tableName; - - if ($this->schema->hasTable($tableName)) { - $table = $this->schema->getTable($tableName); + if ($this->schema->hasTable($ocTable)) { + $table = $this->schema->getTable($ocTable); $messages[] = 'Validating table ' . $table->getName(); $tableCreated = false; } else { - $table = $this->schema->createTable($tableName); + $table = $this->schema->createTable($ocTable); $tableCreated = true; $messages[] = 'Creating table ' . $table->getName(); } @@ -188,8 +187,8 @@ public function createTable(string $tableName, array $columns): array { public function createTables(): array { $messages = []; - foreach (TableSchema::TABLES as $tableName => $columns) { - $messages = array_merge($messages, $this->createTable($tableName, $columns)); + foreach (array_keys(TableSchema::TABLES) as $tableName) { + $messages = array_merge($messages, $this->createTable($tableName)); } return $messages; } @@ -332,9 +331,9 @@ public function removeOrphaned(): array { public function deleteAllDuplicates(?IOutput $output = null): array { $messages = []; foreach (TableSchema::UNIQUE_INDICES as $tableName => $uniqueIndices) { - foreach ($uniqueIndices as $index) { + foreach ($uniqueIndices as $definition) { - $count = $this->deleteDuplicates($tableName, $index['columns']); + $count = $this->deleteDuplicates($tableName, $definition['columns']); if ($count) { $messages[] = 'Removed ' . $count . ' duplicate records from ' . $this->dbPrefix . $tableName; @@ -448,8 +447,6 @@ public function migrateOptionsToHash(): array { foreach ($this->optionMapper->getAll(includeNull: true) as $option) { try { $option->syncOption(); - // $option->setPollOptionHash(hash('md5', $option->getPollId() . $option->getPollOptionText() . $option->getTimestamp())); - $this->optionMapper->update($option); $count++; } catch (Exception $e) { @@ -474,7 +471,8 @@ public function migrateOptionsToHash(): array { if ($table->hasColumn('vote_option_hash')) { foreach ($this->voteMapper->getAll(includeNull: true) as $vote) { try { - $vote->setVoteOptionHash(hash('md5', $vote->getPollId() . $vote->getUserId() . $vote->getVoteOptionText())); + $vote->setVoteOptionHash(Hash::getOptionHash($vote->getPollId(), $vote->getVoteOptionText())); + $vote->setVoteOptionHashBin(Hash::getOptionHashBin($vote->getPollId(), $vote->getVoteOptionText())); $this->voteMapper->update($vote); $count++; } catch (Exception $e) { diff --git a/lib/Db/Vote.php b/lib/Db/Vote.php index 5ce56387c1..bf965a9e8e 100644 --- a/lib/Db/Vote.php +++ b/lib/Db/Vote.php @@ -24,6 +24,8 @@ * @method void setVoteOptionText(string $value) * @method string getVoteOptionHash() * @method void setVoteOptionHash(string $value) + * @method string getVoteOptionHashBin() + * @method void setVoteOptionHashBin(string $value) * @method string getVoteAnswer() * @method void setVoteAnswer(string $value) * @method int getDeleted() @@ -45,6 +47,7 @@ class Vote extends EntityWithUser implements JsonSerializable { protected int $voteOptionId = 0; protected string $voteOptionText = ''; protected string $voteOptionHash = ''; + protected string $voteOptionHashBin = ''; protected string $voteAnswer = ''; protected int $deleted = 0; diff --git a/lib/Db/VoteMapper.php b/lib/Db/VoteMapper.php index 5061a3b2b8..6d14ff2067 100644 --- a/lib/Db/VoteMapper.php +++ b/lib/Db/VoteMapper.php @@ -8,6 +8,7 @@ namespace OCA\Polls\Db; +use OCA\Polls\Helper\Hash; use OCA\Polls\UserSession; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\Entity; @@ -32,13 +33,15 @@ public function __construct( } public function update(Entity $entity): Vote { - $entity->setVoteOptionHash(hash('md5', $entity->getPollId() . $entity->getUserId() . $entity->getVoteOptionText())); + $entity->setVoteOptionHash(Hash::getOptionHash($entity->getPollId(), $entity->getVoteOptionText())); + $entity->setVoteOptionHashBin(Hash::getOptionHashBin($entity->getPollId(), $entity->getVoteOptionText())); $entity = parent::update($entity); return $this->find($entity->getId()); } public function insert(Entity $entity): Vote { - $entity->setVoteOptionHash(hash('md5', $entity->getPollId() . $entity->getUserId() . $entity->getVoteOptionText())); + $entity->setVoteOptionHash(Hash::getOptionHash($entity->getPollId(), $entity->getVoteOptionText())); + $entity->setVoteOptionHashBin(Hash::getOptionHashBin($entity->getPollId(), $entity->getVoteOptionText())); $entity = parent::insert($entity); return $this->find($entity->getId()); } @@ -170,7 +173,7 @@ public function removeOrphanedVotes(): int { 'votes', Option::TABLE, 'options', - 'votes.poll_id = options.poll_id AND votes.vote_option_text = options.poll_option_text' + 'votes.poll_id = options.poll_id AND votes.vote_option_hash_bin = options.poll_option_hash_bin' ); $qb->where('options.poll_id IS NULL'); @@ -256,7 +259,7 @@ protected function joinOption( $joinAlias, $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), - $qb->expr()->eq($joinAlias . '.poll_option_text', $fromAlias . '.vote_option_text'), + $qb->expr()->eq($joinAlias . '.poll_option_hash_bin', $fromAlias . '.vote_option_hash_bin'), $qb->expr()->eq($joinAlias . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)), ) ); diff --git a/lib/Helper/Hash.php b/lib/Helper/Hash.php new file mode 100644 index 0000000000..c3ca5ce948 --- /dev/null +++ b/lib/Helper/Hash.php @@ -0,0 +1,43 @@ + + */ +class AddMissingIndicesListener implements IEventListener { + public function handle(Event $event): void { + if (!($event instanceof AddMissingIndicesEvent)) { + return; + } + + foreach (TableSchema::OPTIONAL_INDICES as $table => $indices) { + foreach ($indices as $name => $definition) { + $event->addMissingIndex( + $table, + $name, + $definition['columns'], + ); + } + } + } +} \ No newline at end of file diff --git a/lib/Migration/RepairSteps/CreateIndices.php b/lib/Migration/RepairSteps/CreateIndices.php index 6dfa323eea..aa6b825995 100644 --- a/lib/Migration/RepairSteps/CreateIndices.php +++ b/lib/Migration/RepairSteps/CreateIndices.php @@ -28,7 +28,7 @@ public function __construct( } public function getName() { - return 'Polls - Create indices and foreign key constraints'; + return 'Polls - Create all unique and optional indices and foreign key constraints'; } public function run(IOutput $output): void { @@ -40,8 +40,10 @@ public function run(IOutput $output): void { // remove foreign keys from the share table // cannot be used anymore since v8.0.0 $messages = array_merge($messages, $this->indexManager->removeForeignKeysFromTable(Share::TABLE)); + $messages = array_merge($messages, $this->indexManager->createForeignKeyConstraints()); - $messages = array_merge($messages, $this->indexManager->createIndices()); + $messages = array_merge($messages, $this->indexManager->createUniqueIndices()); + $messages = array_merge($messages, $this->indexManager->createOptionalIndices()); $this->connection->migrateToSchema($this->schema); foreach ($messages as $message) { diff --git a/lib/Migration/RepairSteps/Install.php b/lib/Migration/RepairSteps/Install.php index 34062d9926..ae3d9fc1e1 100644 --- a/lib/Migration/RepairSteps/Install.php +++ b/lib/Migration/RepairSteps/Install.php @@ -36,7 +36,7 @@ public function run(IOutput $output): void { $this->indexManager->setSchema($this->schema); $messages = array_merge($messages, $this->indexManager->createForeignKeyConstraints()); - $messages = array_merge($messages, $this->indexManager->createIndices()); + $messages = array_merge($messages, $this->indexManager->createUniqueIndices()); $this->connection->migrateToSchema($this->schema); diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index ce3d124ce6..97a3762cc8 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -57,40 +57,69 @@ abstract class TableSchema { /** * define useful common indices, which are not unique * table => ['name' => 'indexName', 'unique' => false, 'columns' => ['column1', 'column2']] + * @deprecated since 8.3.0, use OPTIONAL_INDICES instead */ public const COMMON_INDICES = [ 'polls_polls_owners_non_deleted' => ['table' => Poll::TABLE, 'name' => 'polls_polls_owners_non_deleted', 'unique' => false, 'columns' => ['owner', 'deleted']], ]; + public const OPTIONAL_INDICES = [ + Poll::TABLE => [ + 'polls_polls_owners_non_deleted' => ['unique' => false, 'columns' => ['owner', 'deleted']], + 'polls_polls_deleted' => ['unique' => false, 'columns' => ['deleted']], + 'polls_polls_owners' => ['unique' => false, 'columns' => ['owner']], + ], + PollGroup::RELATION_TABLE => [ + 'polls_groups_polls' => ['unique' => false, 'columns' => ['poll_id', 'group_id']], + ], + Option::TABLE => [ + 'polls_options' => ['unique' => false, 'columns' => ['poll_id', 'deleted']], + 'polls_options_hash' => ['unique' => false, 'columns' => ['poll_id', 'poll_option_hash_bin', 'deleted']], + 'polls_options_owner' => ['unique' => false, 'columns' => ['poll_id', 'owner']], + ], + Share::TABLE => [ + 'polls_shares_user' => ['unique' => false, 'columns' => ['poll_id', 'user_id', 'deleted']], + 'polls_shares_types' => ['unique' => false, 'columns' => ['poll_id', 'type', 'deleted']], + 'polls_group_shares_user' => ['unique' => false, 'columns' => ['group_id', 'user_id', 'deleted']], + ], + Vote::TABLE => [ + 'polls_votes_answers' => ['unique' => false, 'columns' => ['poll_id', 'user_id']], + 'polls_votes_user' => ['unique' => false, 'columns' => ['poll_id', 'vote_answer', 'user_id']], + 'polls_votes_hash' => ['unique' => false, 'columns' => ['poll_id', 'vote_option_hash_bin', 'deleted']], + ], + ]; + /** * define unique indices, which are not primary keys * table => [['name' => 'indexName', 'unique' => true, 'columns' => ['column1', 'column2']], ...] */ public const UNIQUE_INDICES = [ Option::TABLE => [ - ['name' => 'UNIQ_options', 'unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], + 'UNIQ_options' => ['unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], + 'UNIQ_options_bin' => ['unique' => true, 'columns' => ['poll_id', 'poll_option_hash_bin', 'timestamp']], ], Log::TABLE => [ - ['name' => 'UNIQ_unprocessed', 'unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], + 'UNIQ_unprocessed' => ['unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], ], Subscription::TABLE => [ - ['name' => 'UNIQ_subscription', 'unique' => true, 'columns' => ['poll_id', 'user_id']], + 'UNIQ_subscription' => ['unique' => true, 'columns' => ['poll_id', 'user_id']], ], Share::TABLE => [ - ['name' => 'UNIQ_shares', 'unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id']], - ['name' => 'UNIQ_token', 'unique' => true, 'columns' => ['token']], + 'UNIQ_shares' => ['unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id']], + 'UNIQ_token' => ['unique' => true, 'columns' => ['token']], ], Vote::TABLE => [ - ['name' => 'UNIQ_votes', 'unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], + 'UNIQ_votes' => ['unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], + 'UNIQ_votes_bin' => ['unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash_bin']], ], Preferences::TABLE => [ - ['name' => 'UNIQ_preferences', 'unique' => true, 'columns' => ['user_id']], + 'UNIQ_preferences' => ['unique' => true, 'columns' => ['user_id']], ], Watch::TABLE => [ - ['name' => 'UNIQ_watch', 'unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], + 'UNIQ_watch' => ['unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], ], PollGroup::RELATION_TABLE => [ - ['name' => 'UNIQ_poll_group_relation', 'unique' => true, 'columns' => ['poll_id', 'group_id']], + 'UNIQ_poll_group_relation' => ['unique' => true, 'columns' => ['poll_id', 'group_id']], ], ]; @@ -229,7 +258,8 @@ abstract class TableSchema { 'id' => ['type' => Types::BIGINT, 'options' => ['autoincrement' => true, 'notnull' => true, 'length' => 20]], 'poll_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'poll_option_text' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 1024]], - 'poll_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 256]], + 'poll_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 32]], + 'poll_option_hash_bin' => ['type' => Types::BINARY, 'options' => ['notnull' => false, 'default' => '', 'length' => 16]], 'timestamp' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'duration' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'order' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], @@ -244,7 +274,8 @@ abstract class TableSchema { 'user_id' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 256]], 'vote_option_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'vote_option_text' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 1024]], - 'vote_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 256]], + 'vote_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 32]], + 'vote_option_hash_bin' => ['type' => Types::BINARY, 'options' => ['notnull' => false, 'default' => '', 'length' => 16]], 'vote_answer' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 64]], 'deleted' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], ], diff --git a/lib/Migration/Version080300Date20250812231603.php b/lib/Migration/Version080300Date20250812231603.php new file mode 100644 index 0000000000..561adb9ee7 --- /dev/null +++ b/lib/Migration/Version080300Date20250812231603.php @@ -0,0 +1,103 @@ +schema = $schemaClosure(); + $messages = $this->createTables(); + + foreach ($messages as $message) { + $output->info('Polls - ' . $message); + }; + + return $this->schema; + } + + /** + * @return string[] + * + * @psalm-return non-empty-list + */ + public function createTable(string $tableName, array $columns): array { + $messages = []; + + if ($this->schema->hasTable($tableName)) { + $table = $this->schema->getTable($tableName); + $messages[] = 'Validating table ' . $table->getName(); + $tableCreated = false; + } else { + $table = $this->schema->createTable($tableName); + $tableCreated = true; + $messages[] = 'Creating table ' . $table->getName(); + } + + foreach ($columns as $columnName => $columnDefinition) { + if ($table->hasColumn($columnName)) { + $column = $table->getColumn($columnName); + if (Type::lookupName($column->getType()) !== $columnDefinition['type']) { + $messages[] = 'Migrated type of ' . $table->getName() . '[\'' . $columnName . '\'] from ' . Type::lookupName($column->getType()) . ' to ' . $columnDefinition['type']; + $column->setType(Type::getType($columnDefinition['type'])); + } + $column->setOptions($columnDefinition['options']); + + // force change to current options definition + $table->modifyColumn($columnName, $columnDefinition['options']); + } else { + $table->addColumn($columnName, $columnDefinition['type'], $columnDefinition['options']); + $messages[] = 'Added ' . $table->getName() . ', ' . $columnName . ' (' . $columnDefinition['type'] . ')'; + } + } + + if ($tableCreated) { + $table->setPrimaryKey(['id']); + } + return $messages; + } + + /** + * @return string[] + * + * @psalm-return non-empty-list + */ + public function createTables(): array { + $messages = []; + + foreach (TableSchema::TABLES as $tableName => $columns) { + $messages = array_merge($messages, $this->createTable($tableName, $columns)); + } + return $messages; + } +} diff --git a/lib/Model/UserBase.php b/lib/Model/UserBase.php index 6cbbabc0e7..00a1f8ef52 100644 --- a/lib/Model/UserBase.php +++ b/lib/Model/UserBase.php @@ -11,6 +11,7 @@ use DateTimeZone; use JsonSerializable; use OCA\Polls\Helper\Container; +use OCA\Polls\Helper\Hash; use OCA\Polls\Model\Group\Circle; use OCA\Polls\Model\Group\ContactGroup; use OCA\Polls\Model\Group\Group; @@ -108,8 +109,7 @@ public function getPrincipalUri(): string { * hash the real userId to obfuscate the real userId */ public function getHashedUserId(): string { - // TODO: add a session salt - return hash('md5', $this->id); + return Hash::getUserIdHash($this->id); } /** diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index 135976e079..3a649cfb1a 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -123,6 +123,7 @@ public function add(int $pollId, SimpleOption $simpleOption, bool $voteYes = fal try { // Insert the new option + /** @var Option $newOption */ $newOption = $this->optionMapper->insert($newOption); } catch (Exception $e) { // TODO: Change exception catch to actual exception @@ -133,7 +134,7 @@ public function add(int $pollId, SimpleOption $simpleOption, bool $voteYes = fal // Option already exists, so we need to update the existing one // and remove deleted setting - $option = $this->optionMapper->findByPollAndText($pollId, $newOption->getPollOptionText(), true); + $option = $this->optionMapper->findByPollAndText($pollId, $newOption->getPollOptionHashBin(), true); $option->setDeleted(0); $newOption = $this->optionMapper->update($option); diff --git a/lib/UserSession.php b/lib/UserSession.php index 26a5ca30be..10be5077ec 100644 --- a/lib/UserSession.php +++ b/lib/UserSession.php @@ -12,6 +12,7 @@ use OCA\Polls\Db\Share; use OCA\Polls\Db\ShareMapper; use OCA\Polls\Db\UserMapper; +use OCA\Polls\Helper\Hash; use OCA\Polls\Model\User\Cron; use OCA\Polls\Model\User\Ghost; use OCA\Polls\Model\UserBase; @@ -173,7 +174,7 @@ public function getClientId(): string { } public function getClientIdHashed(): string { - return hash('md5', $this->getClientId()); + return Hash::getClientIdHash($this->getClientId()); } public function setClientId(string $clientId): void { From 423a6d87376a7f32816b7377e610a119df617a93 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Wed, 13 Aug 2025 14:03:31 +0200 Subject: [PATCH 17/26] type hints and docblock Signed-off-by: dartcafe --- lib/Db/Option.php | 1 + lib/Db/Vote.php | 1 + lib/Helper/Hash.php | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/lib/Db/Option.php b/lib/Db/Option.php index 7d0b986eb4..aa1bb1b70d 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -65,6 +65,7 @@ class Option extends EntityWithUser implements JsonSerializable { protected int $pollId = 0; protected string $pollOptionText = ''; protected string $pollOptionHash = ''; + /** @var string $pollOptionHashBin binary 16-byte MD5 hash */ protected string $pollOptionHashBin = ''; protected int $timestamp = 0; protected int $duration = 0; diff --git a/lib/Db/Vote.php b/lib/Db/Vote.php index bf965a9e8e..dcf5af59e9 100644 --- a/lib/Db/Vote.php +++ b/lib/Db/Vote.php @@ -47,6 +47,7 @@ class Vote extends EntityWithUser implements JsonSerializable { protected int $voteOptionId = 0; protected string $voteOptionText = ''; protected string $voteOptionHash = ''; + /** @var string $voteOptionHashBin binary 16-byte MD5 hash */ protected string $voteOptionHashBin = ''; protected string $voteAnswer = ''; protected int $deleted = 0; diff --git a/lib/Helper/Hash.php b/lib/Helper/Hash.php index c3ca5ce948..f7aedf99fa 100644 --- a/lib/Helper/Hash.php +++ b/lib/Helper/Hash.php @@ -25,17 +25,48 @@ * @param string $optionText */ abstract class Hash { + /** + * Generate a hash for an option based on the poll ID and option text. + * This is used to create a unique identifier for options. + * + * @param int $pollId + * @param string $optionText + * @return string + */ public static function getOptionHash(int $pollId, string $optionText): string { return hash('md5', $pollId . $optionText); } + /** + * Generate a binary hash for an option based on the poll ID and option text. + * This is used to create a unique identifier for options in binary format. + * + * @param int $pollId + * @param string $optionText + * @return string binary 16-byte MD5 hash + */ public static function getOptionHashBin(int $pollId, string $optionText): string { return hash('md5', $pollId . $optionText, true); } + /** + * Generate a hash for a client ID. + * This is used to create a unique identifier for clients. + * + * @param string $clientId + * @return string + */ public static function getClientIdHash(string $clientId): string { return hash('md5', $clientId); } + + /** + * Generate a hash for a user ID. + * This is used to create a unique identifier for users. + * + * @param string $userId + * @return string + */ public static function getUserIdHash(string $userId): string { // TODO: add a session salt return hash('md5', $userId); From 0fbed05fd0d0e2a39524bf8787b41bfc960b2c8a Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 10:18:11 +0200 Subject: [PATCH 18/26] adjust icon style and fix color Signed-off-by: dartcafe --- img/app.svg | 10 +++++----- img/polls-dark.svg | 8 +++++--- img/polls.svg | 8 +++++--- src/components/AppIcons/PollsAppIcon.vue | 6 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/img/app.svg b/img/app.svg index f94d056b24..e33d6ebb5c 100644 --- a/img/app.svg +++ b/img/app.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/img/polls-dark.svg b/img/polls-dark.svg index 7dfcf77b34..544b9313c2 100644 --- a/img/polls-dark.svg +++ b/img/polls-dark.svg @@ -1,5 +1,7 @@ - - - + + + + + diff --git a/img/polls.svg b/img/polls.svg index 410ea6fc34..5db2d53358 100644 --- a/img/polls.svg +++ b/img/polls.svg @@ -1,5 +1,7 @@ - - - + + + + + diff --git a/src/components/AppIcons/PollsAppIcon.vue b/src/components/AppIcons/PollsAppIcon.vue index 9195475583..f8138e2795 100644 --- a/src/components/AppIcons/PollsAppIcon.vue +++ b/src/components/AppIcons/PollsAppIcon.vue @@ -27,9 +27,9 @@ const { :height="size" viewBox="0 0 32 32"> - - - + + + {{ title }} From 0076e83067bcfd36f5580dfb736fef766ed8fb99 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 10:34:04 +0200 Subject: [PATCH 19/26] fixes and tidy Signed-off-by: dartcafe --- appinfo/info.xml | 2 +- lib/Command/Db/RemoveFKConstraints.php | 1 - ...eIndices.php => RemoveOptionalIndices.php} | 2 +- lib/Command/Db/RemoveUniqueIndices.php | 1 - lib/Command/Db/ResetWatch.php | 5 +- lib/Db/IndexManager.php | 16 +-- lib/Listener/AddMissingIndicesListener.php | 16 +-- lib/Migration/TableSchema.php | 108 ++++++------------ 8 files changed, 54 insertions(+), 97 deletions(-) rename lib/Command/Db/{RemoveIndices.php => RemoveOptionalIndices.php} (95%) diff --git a/appinfo/info.xml b/appinfo/info.xml index 0355c936fe..4a3f6e726e 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -52,7 +52,7 @@ OCA\Polls\Command\Db\Purge OCA\Polls\Command\Db\Rebuild OCA\Polls\Command\Db\RemoveFKConstraints - OCA\Polls\Command\Db\RemoveIndices + OCA\Polls\Command\Db\RemoveOptionalIndices OCA\Polls\Command\Db\RemoveUniqueIndices OCA\Polls\Command\Db\ResetWatch OCA\Polls\Command\Poll\TransferOwnership diff --git a/lib/Command/Db/RemoveFKConstraints.php b/lib/Command/Db/RemoveFKConstraints.php index d33e51594d..7d60ba5349 100644 --- a/lib/Command/Db/RemoveFKConstraints.php +++ b/lib/Command/Db/RemoveFKConstraints.php @@ -12,7 +12,6 @@ use OCA\Polls\Command\Command; use OCA\Polls\Db\IndexManager; use OCP\IDBConnection; -use Symfony\Component\Console\Question\ConfirmationQuestion; /** * @psalm-api diff --git a/lib/Command/Db/RemoveIndices.php b/lib/Command/Db/RemoveOptionalIndices.php similarity index 95% rename from lib/Command/Db/RemoveIndices.php rename to lib/Command/Db/RemoveOptionalIndices.php index da9b7583d8..437dd99284 100644 --- a/lib/Command/Db/RemoveIndices.php +++ b/lib/Command/Db/RemoveOptionalIndices.php @@ -17,7 +17,7 @@ * @psalm-api */ class RemoveIndices extends Command { - protected string $name = parent::NAME_PREFIX . 'index:remove'; + protected string $name = parent::NAME_PREFIX . 'index:remove:optional'; protected string $description = 'Remove all optional indices'; protected array $operationHints = [ 'Removes all optional indices. Removing them may decrease your database query performance.', diff --git a/lib/Command/Db/RemoveUniqueIndices.php b/lib/Command/Db/RemoveUniqueIndices.php index 8492f66828..2d87b9e2ca 100644 --- a/lib/Command/Db/RemoveUniqueIndices.php +++ b/lib/Command/Db/RemoveUniqueIndices.php @@ -12,7 +12,6 @@ use OCA\Polls\Command\Command; use OCA\Polls\Db\IndexManager; use OCP\IDBConnection; -use Symfony\Component\Console\Question\ConfirmationQuestion; /** * @psalm-api diff --git a/lib/Command/Db/ResetWatch.php b/lib/Command/Db/ResetWatch.php index aca55c6c45..7380b36c75 100644 --- a/lib/Command/Db/ResetWatch.php +++ b/lib/Command/Db/ResetWatch.php @@ -23,8 +23,7 @@ class ResetWatch extends Command { protected string $name = parent::NAME_PREFIX . 'db:reset-watch'; protected string $description = 'Resets the Watch table'; protected array $operationHints = [ - 'All polls tables will get checked against the current schema.', - 'NO data migration will be executed, so make sure you have a backup of your database.', + 'Removes and recreates the watch table to set ids to zero', ]; public function __construct( @@ -49,7 +48,7 @@ protected function runCommands(): int { $messages = $this->tableManager->createTable($tableName); foreach (TableSchema::UNIQUE_INDICES[$tableName] as $name => $definition) { - $messages[] = $this->indexManager->createIndex($tableName, $name, $definition['columns'], $definition['unique']); + $messages[] = $this->indexManager->createIndex($tableName, $name, $definition['columns'], true); } $this->connection->migrateToSchema($this->schema); diff --git a/lib/Db/IndexManager.php b/lib/Db/IndexManager.php index 6f2fe43334..ebd34a5d10 100644 --- a/lib/Db/IndexManager.php +++ b/lib/Db/IndexManager.php @@ -36,6 +36,8 @@ public function setSchema(Schema &$schema): void { /** * Create unique indices + * Unique indices are crucial for the correct operation of the polls app. + * This for they have to be updated on every update. * * @return string[] logged messages */ @@ -44,7 +46,7 @@ public function createUniqueIndices(): array { foreach (TableSchema::UNIQUE_INDICES as $tableName => $uniqueIndices) { foreach ($uniqueIndices as $name => $definition) { - $messages[] = $this->createIndex($tableName, $name, $definition['columns'], $definition['unique']); + $messages[] = $this->createIndex($tableName, $name, $definition['columns'], true); } } return $messages; @@ -53,18 +55,16 @@ public function createUniqueIndices(): array { /** * Create optional indices * Usually they should be created by the AddMissingIndicesListener + * or on first time installation of polls. * * @return string[] logged messages */ public function createOptionalIndices(): array { $messages = []; - // Only install optional indices if this is the first installation of polls - // Leave the index creation for existing installations to AddMissingIndicesListener, - // because initializing and rebuild of an index can take a long time foreach (TableSchema::OPTIONAL_INDICES as $table => $indices) { foreach ($indices as $name => $definition) { - $messages[] = $this->createIndex($table, $name, $definition['columns'], $definition['unique'] ?? false); + $messages[] = $this->createIndex($table, $name, $definition['columns']); } } @@ -73,6 +73,8 @@ public function createOptionalIndices(): array { /** * add on delete fk contraints to all tables referencing the main polls table + * Foreign key constraints are crucial for the correct operation of the polls app. + * This for they have to be updated on every update. * * @return string[] logged messages */ @@ -89,7 +91,7 @@ public function createForeignKeyConstraints(): array { } /** - * add an on delete fk contraint + * add one on delete fk contraint * * @param string $parentTableName name of referred table * @param string $childTableName name of referring table @@ -106,7 +108,7 @@ public function createForeignKeyConstraint(string $parentTableName, string $chil } /** - * Create named index for table + * Create one named index for table * * @param string $tableName name of table to add the index to * @param string $indexName index name diff --git a/lib/Listener/AddMissingIndicesListener.php b/lib/Listener/AddMissingIndicesListener.php index 315ffa93db..ffc937fb12 100644 --- a/lib/Listener/AddMissingIndicesListener.php +++ b/lib/Listener/AddMissingIndicesListener.php @@ -24,12 +24,12 @@ public function handle(Event $event): void { foreach (TableSchema::OPTIONAL_INDICES as $table => $indices) { foreach ($indices as $name => $definition) { - $event->addMissingIndex( - $table, - $name, - $definition['columns'], - ); - } - } + $event->addMissingIndex( + $table, + $name, + $definition['columns'], + ); + } + } } -} \ No newline at end of file +} diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index 97a3762cc8..78dff40fcb 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -8,7 +8,6 @@ namespace OCA\Polls\Migration; -use Doctrine\DBAL\Types\Type; use OCA\Polls\Db\Comment; use OCA\Polls\Db\Log; use OCA\Polls\Db\Option; @@ -19,7 +18,6 @@ use OCA\Polls\Db\Subscription; use OCA\Polls\Db\Vote; use OCA\Polls\Db\Watch; -use OCP\DB\ISchemaWrapper; use OCP\DB\Types; /** @@ -29,12 +27,6 @@ */ abstract class TableSchema { - // deprecated since 8.1.0, but keeb these constants to prevent broken updates - // from a version prior to 8.1.0; Fix was implemented with v8.1.2 - public const FK_PARENT_TABLE = Poll::TABLE; - public const FK_CHILD_TABLES = []; - public const FK_OTHER_TABLES = []; - /** * define all foreign key indices * Parentable => [Childable => ['constraintColumn' => 'columnName']] @@ -63,63 +55,71 @@ abstract class TableSchema { 'polls_polls_owners_non_deleted' => ['table' => Poll::TABLE, 'name' => 'polls_polls_owners_non_deleted', 'unique' => false, 'columns' => ['owner', 'deleted']], ]; + /** + * define useful optional indices, which are not unique + * tableName => [ + * indexName => ['columns' => [column1, column2, ...]], + * ...] + */ public const OPTIONAL_INDICES = [ Poll::TABLE => [ - 'polls_polls_owners_non_deleted' => ['unique' => false, 'columns' => ['owner', 'deleted']], - 'polls_polls_deleted' => ['unique' => false, 'columns' => ['deleted']], - 'polls_polls_owners' => ['unique' => false, 'columns' => ['owner']], + 'polls_polls_owners_non_deleted' => ['columns' => ['owner', 'deleted']], + 'polls_polls_deleted' => ['columns' => ['deleted']], + 'polls_polls_owners' => ['columns' => ['owner']], ], PollGroup::RELATION_TABLE => [ - 'polls_groups_polls' => ['unique' => false, 'columns' => ['poll_id', 'group_id']], + 'polls_groups_polls' => ['columns' => ['poll_id', 'group_id']], ], Option::TABLE => [ - 'polls_options' => ['unique' => false, 'columns' => ['poll_id', 'deleted']], - 'polls_options_hash' => ['unique' => false, 'columns' => ['poll_id', 'poll_option_hash_bin', 'deleted']], - 'polls_options_owner' => ['unique' => false, 'columns' => ['poll_id', 'owner']], + 'polls_options' => ['columns' => ['poll_id', 'deleted']], + 'polls_options_hash' => ['columns' => ['poll_id', 'poll_option_hash_bin', 'deleted']], + 'polls_options_owner' => ['columns' => ['poll_id', 'owner']], ], Share::TABLE => [ - 'polls_shares_user' => ['unique' => false, 'columns' => ['poll_id', 'user_id', 'deleted']], - 'polls_shares_types' => ['unique' => false, 'columns' => ['poll_id', 'type', 'deleted']], - 'polls_group_shares_user' => ['unique' => false, 'columns' => ['group_id', 'user_id', 'deleted']], + 'polls_shares_user' => ['columns' => ['poll_id', 'user_id', 'deleted']], + 'polls_shares_types' => ['columns' => ['poll_id', 'type', 'deleted']], + 'polls_group_shares_user' => ['columns' => ['group_id', 'user_id', 'deleted']], ], Vote::TABLE => [ - 'polls_votes_answers' => ['unique' => false, 'columns' => ['poll_id', 'user_id']], - 'polls_votes_user' => ['unique' => false, 'columns' => ['poll_id', 'vote_answer', 'user_id']], - 'polls_votes_hash' => ['unique' => false, 'columns' => ['poll_id', 'vote_option_hash_bin', 'deleted']], + 'polls_votes_answers' => ['columns' => ['poll_id', 'user_id']], + 'polls_votes_user' => ['columns' => ['poll_id', 'vote_answer', 'user_id']], + 'polls_votes_hash' => ['columns' => ['poll_id', 'vote_option_hash_bin', 'deleted']], ], ]; /** * define unique indices, which are not primary keys - * table => [['name' => 'indexName', 'unique' => true, 'columns' => ['column1', 'column2']], ...] + * tableName => [ + * indexName => ['columns' => [column1, column2, ...]], + * ...] */ public const UNIQUE_INDICES = [ Option::TABLE => [ - 'UNIQ_options' => ['unique' => true, 'columns' => ['poll_id', 'poll_option_hash', 'timestamp']], - 'UNIQ_options_bin' => ['unique' => true, 'columns' => ['poll_id', 'poll_option_hash_bin', 'timestamp']], + 'UNIQ_options' => ['columns' => ['poll_id', 'poll_option_hash', 'timestamp']], + 'UNIQ_options_bin' => ['columns' => ['poll_id', 'poll_option_hash_bin', 'timestamp']], ], Log::TABLE => [ - 'UNIQ_unprocessed' => ['unique' => true, 'columns' => ['processed', 'poll_id', 'user_id', 'message_id']], + 'UNIQ_unprocessed' => ['columns' => ['processed', 'poll_id', 'user_id', 'message_id']], ], Subscription::TABLE => [ - 'UNIQ_subscription' => ['unique' => true, 'columns' => ['poll_id', 'user_id']], + 'UNIQ_subscription' => ['columns' => ['poll_id', 'user_id']], ], Share::TABLE => [ - 'UNIQ_shares' => ['unique' => true, 'columns' => ['poll_id', 'group_id', 'user_id']], - 'UNIQ_token' => ['unique' => true, 'columns' => ['token']], + 'UNIQ_shares' => ['columns' => ['poll_id', 'group_id', 'user_id']], + 'UNIQ_token' => ['columns' => ['token']], ], Vote::TABLE => [ - 'UNIQ_votes' => ['unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash']], - 'UNIQ_votes_bin' => ['unique' => true, 'columns' => ['poll_id', 'user_id', 'vote_option_hash_bin']], + 'UNIQ_votes' => ['columns' => ['poll_id', 'user_id', 'vote_option_hash']], + 'UNIQ_votes_bin' => ['columns' => ['poll_id', 'user_id', 'vote_option_hash_bin']], ], Preferences::TABLE => [ - 'UNIQ_preferences' => ['unique' => true, 'columns' => ['user_id']], + 'UNIQ_preferences' => ['columns' => ['user_id']], ], Watch::TABLE => [ - 'UNIQ_watch' => ['unique' => true, 'columns' => ['poll_id', 'table', 'session_id']], + 'UNIQ_watch' => ['columns' => ['poll_id', 'table', 'session_id']], ], PollGroup::RELATION_TABLE => [ - 'UNIQ_poll_group_relation' => ['unique' => true, 'columns' => ['poll_id', 'group_id']], + 'UNIQ_poll_group_relation' => ['columns' => ['poll_id', 'group_id']], ], ]; @@ -333,46 +333,4 @@ abstract class TableSchema { 'preferences' => ['type' => Types::TEXT, 'options' => ['notnull' => false, 'default' => null, 'length' => 65535]], ], ]; - - /** - * Iterate over tables and make sure, they are created or updated - * according to the currently valid schema - * @psalm-api - */ - public static function createOrUpdateSchema(ISchemaWrapper &$schema): array { - $messages = []; - foreach (self::TABLES as $tableName => $columns) { - $tableCreated = false; - - if ($schema->hasTable($tableName)) { - $messages[] = 'Validating table ' . $tableName; - $table = $schema->getTable($tableName); - } else { - $messages[] = 'Creating table ' . $tableName; - $table = $schema->createTable($tableName); - $tableCreated = true; - } - - foreach ($columns as $columnName => $columnDefinition) { - if ($table->hasColumn($columnName)) { - $column = $table->getColumn($columnName); - $column->setOptions($columnDefinition['options']); - if (Type::lookupName($column->getType()) !== $columnDefinition['type']) { - $messages[] = 'Migrating type of ' . $tableName . ', ' . $columnName . ' to ' . $columnDefinition['type']; - $column->setType(Type::getType($columnDefinition['type'])); - } - - // force change to current options definition - $table->modifyColumn($columnName, $columnDefinition['options']); - } else { - $table->addColumn($columnName, $columnDefinition['type'], $columnDefinition['options']); - } - } - - if ($tableCreated) { - $table->setPrimaryKey(['id']); - } - } - return $messages; - } } From 3f8b7442bb3e944afe95e19dbbb2bc19460c8a76 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 12:26:21 +0200 Subject: [PATCH 20/26] try type definition Signed-off-by: dartcafe --- lib/Db/Option.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Db/Option.php b/lib/Db/Option.php index aa1bb1b70d..03650bc4c3 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -12,6 +12,7 @@ use DateTime; use DateTimeImmutable; use DateTimeZone; +use Doctrine\DBAL\Types\Types; use JsonSerializable; use OCA\Polls\Exceptions\InsufficientAttributesException; use OCA\Polls\Helper\Hash; @@ -66,7 +67,7 @@ class Option extends EntityWithUser implements JsonSerializable { protected string $pollOptionText = ''; protected string $pollOptionHash = ''; /** @var string $pollOptionHashBin binary 16-byte MD5 hash */ - protected string $pollOptionHashBin = ''; + protected string $pollOptionHashBin; protected int $timestamp = 0; protected int $duration = 0; protected int $order = 0; @@ -104,6 +105,7 @@ public function __construct() { $this->addType('votesNo', 'integer'); $this->addType('votesMaybe', 'integer'); $this->addType('showResults', 'integer'); + $this->addType('pollOptionHashBin', Types::BINARY); } /** From 4afacbedba83b2005bf8ea45b4e74956716a26e7 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 12:52:09 +0200 Subject: [PATCH 21/26] next Signed-off-by: dartcafe --- lib/Db/Option.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/Option.php b/lib/Db/Option.php index 03650bc4c3..447be46e5a 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -67,7 +67,7 @@ class Option extends EntityWithUser implements JsonSerializable { protected string $pollOptionText = ''; protected string $pollOptionHash = ''; /** @var string $pollOptionHashBin binary 16-byte MD5 hash */ - protected string $pollOptionHashBin; + protected string $pollOptionHashBin = ''; protected int $timestamp = 0; protected int $duration = 0; protected int $order = 0; From a28e212cf9efcab32ea6871974a3d0508008e244 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 13:19:49 +0200 Subject: [PATCH 22/26] assert failing again Signed-off-by: dartcafe --- lib/Db/OptionMapper.php | 2 +- lib/Db/Vote.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Db/OptionMapper.php b/lib/Db/OptionMapper.php index f9c63a0aa7..6ca1122498 100644 --- a/lib/Db/OptionMapper.php +++ b/lib/Db/OptionMapper.php @@ -72,7 +72,7 @@ public function findByPoll(int $pollId, bool $hideResults = false, bool $getDele public function findByPollAndText(int $pollId, string $pollOptionHashBin, bool $getDeleted = false): Option { $qb = $this->buildQuery(); $qb->where($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_hash_bin', $qb->createNamedParameter($pollOptionHashBin, IQueryBuilder::PARAM_STR))); + ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_hash_bin', $qb->createNamedParameter($pollOptionHashBin, IQueryBuilder::PARAM_LOB))); if (!$getDeleted) { $qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT))); } diff --git a/lib/Db/Vote.php b/lib/Db/Vote.php index dcf5af59e9..d99a4e500b 100644 --- a/lib/Db/Vote.php +++ b/lib/Db/Vote.php @@ -8,6 +8,7 @@ namespace OCA\Polls\Db; +use Doctrine\DBAL\Types\Types; use JsonSerializable; /** @@ -61,6 +62,7 @@ public function __construct( $this->addType('pollId', 'integer'); $this->addType('voteOptionId', 'integer'); $this->addType('deleted', 'integer'); + $this->addType('voteOptionHashBin', Types::BINARY); } private function getAnswerSymbol(): string { From 98ad85d42413842578bb735dd822d34be21c00e5 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 13:48:30 +0200 Subject: [PATCH 23/26] fix class Signed-off-by: dartcafe --- lib/Command/Db/RemoveOptionalIndices.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/Db/RemoveOptionalIndices.php b/lib/Command/Db/RemoveOptionalIndices.php index 437dd99284..23fcf37a01 100644 --- a/lib/Command/Db/RemoveOptionalIndices.php +++ b/lib/Command/Db/RemoveOptionalIndices.php @@ -16,7 +16,7 @@ /** * @psalm-api */ -class RemoveIndices extends Command { +class RemoveOptionalIndices extends Command { protected string $name = parent::NAME_PREFIX . 'index:remove:optional'; protected string $description = 'Remove all optional indices'; protected array $operationHints = [ From f2ae807d40b350e5878cdf6de4655370c8c282e4 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 13:48:42 +0200 Subject: [PATCH 24/26] remove binary hash Signed-off-by: dartcafe --- lib/Db/Option.php | 7 ------- lib/Db/OptionMapper.php | 10 +++++----- lib/Db/PollMapper.php | 2 +- lib/Db/TableManager.php | 1 - lib/Db/Vote.php | 4 ---- lib/Db/VoteMapper.php | 6 ++---- lib/Helper/Hash.php | 1 + lib/Migration/TableSchema.php | 16 ++++++++++------ lib/Service/OptionService.php | 3 +-- 9 files changed, 20 insertions(+), 30 deletions(-) diff --git a/lib/Db/Option.php b/lib/Db/Option.php index 447be46e5a..805dca2af5 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -66,8 +66,6 @@ class Option extends EntityWithUser implements JsonSerializable { protected int $pollId = 0; protected string $pollOptionText = ''; protected string $pollOptionHash = ''; - /** @var string $pollOptionHashBin binary 16-byte MD5 hash */ - protected string $pollOptionHashBin = ''; protected int $timestamp = 0; protected int $duration = 0; protected int $order = 0; @@ -105,7 +103,6 @@ public function __construct() { $this->addType('votesNo', 'integer'); $this->addType('votesMaybe', 'integer'); $this->addType('showResults', 'integer'); - $this->addType('pollOptionHashBin', Types::BINARY); } /** @@ -231,10 +228,6 @@ private function updateHash(): void { $this->getPollId(), $this->getPollOptionText() )); - $this->setPollOptionHashBin((Hash::getOptionHashBin( - $this->getPollId(), - $this->getPollOptionText() - ))); } public function getPollOptionText(): string { diff --git a/lib/Db/OptionMapper.php b/lib/Db/OptionMapper.php index 6ca1122498..9524f4bf49 100644 --- a/lib/Db/OptionMapper.php +++ b/lib/Db/OptionMapper.php @@ -66,13 +66,13 @@ public function findByPoll(int $pollId, bool $hideResults = false, bool $getDele /** * @return Option * @param int $pollId - * @param string $pollOptionHashBin option text as binary hashed value (pollId and PollOptionText) + * @param string $pollOptionHash option text as hashed value (pollId and PollOptionText) * @param bool $getDeleted also search for deleted options */ - public function findByPollAndText(int $pollId, string $pollOptionHashBin, bool $getDeleted = false): Option { + public function findByPollAndText(int $pollId, string $pollOptionHash, bool $getDeleted = false): Option { $qb = $this->buildQuery(); $qb->where($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_hash_bin', $qb->createNamedParameter($pollOptionHashBin, IQueryBuilder::PARAM_LOB))); + ->andWhere($qb->expr()->eq(self::TABLE . '.poll_option_hash', $qb->createNamedParameter($pollOptionHash, IQueryBuilder::PARAM_STR))); if (!$getDeleted) { $qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT))); } @@ -235,7 +235,7 @@ protected function joinVotesCount( $joinAlias, $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), - $qb->expr()->eq($joinAlias . '.vote_option_hash_bin', $fromAlias . '.poll_option_hash_bin'), + $qb->expr()->eq($joinAlias . '.vote_option_hash', $fromAlias . '.poll_option_hash'), ) ) // Count number of votes for this option @@ -290,7 +290,7 @@ protected function joinCurrentUserVote( $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), $qb->expr()->eq($joinAlias . '.user_id', $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR)), - $qb->expr()->eq($joinAlias . '.vote_option_hash_bin', $fromAlias . '.poll_option_hash_bin'), + $qb->expr()->eq($joinAlias . '.vote_option_hash', $fromAlias . '.poll_option_hash'), ) ); } diff --git a/lib/Db/PollMapper.php b/lib/Db/PollMapper.php index c39358cb10..78b74943da 100644 --- a/lib/Db/PollMapper.php +++ b/lib/Db/PollMapper.php @@ -434,7 +434,7 @@ protected function subQueryOrphanedVotesCount( $optionAlias, $expr->andX( $expr->eq($optionAlias . '.poll_id', $subAlias . '.poll_id'), - $expr->eq($optionAlias . '.poll_option_hash_bin', $subAlias . '.vote_option_hash_bin'), + $expr->eq($optionAlias . '.poll_option_hash', $subAlias . '.vote_option_hash'), $expr->eq($optionAlias . '.deleted', $expr->literal(0, IQueryBuilder::PARAM_INT)) ) ) diff --git a/lib/Db/TableManager.php b/lib/Db/TableManager.php index cb84e3abef..297ea8f461 100644 --- a/lib/Db/TableManager.php +++ b/lib/Db/TableManager.php @@ -472,7 +472,6 @@ public function migrateOptionsToHash(): array { foreach ($this->voteMapper->getAll(includeNull: true) as $vote) { try { $vote->setVoteOptionHash(Hash::getOptionHash($vote->getPollId(), $vote->getVoteOptionText())); - $vote->setVoteOptionHashBin(Hash::getOptionHashBin($vote->getPollId(), $vote->getVoteOptionText())); $this->voteMapper->update($vote); $count++; } catch (Exception $e) { diff --git a/lib/Db/Vote.php b/lib/Db/Vote.php index d99a4e500b..924dc0d190 100644 --- a/lib/Db/Vote.php +++ b/lib/Db/Vote.php @@ -8,7 +8,6 @@ namespace OCA\Polls\Db; -use Doctrine\DBAL\Types\Types; use JsonSerializable; /** @@ -48,8 +47,6 @@ class Vote extends EntityWithUser implements JsonSerializable { protected int $voteOptionId = 0; protected string $voteOptionText = ''; protected string $voteOptionHash = ''; - /** @var string $voteOptionHashBin binary 16-byte MD5 hash */ - protected string $voteOptionHashBin = ''; protected string $voteAnswer = ''; protected int $deleted = 0; @@ -62,7 +59,6 @@ public function __construct( $this->addType('pollId', 'integer'); $this->addType('voteOptionId', 'integer'); $this->addType('deleted', 'integer'); - $this->addType('voteOptionHashBin', Types::BINARY); } private function getAnswerSymbol(): string { diff --git a/lib/Db/VoteMapper.php b/lib/Db/VoteMapper.php index 6d14ff2067..5de1ec3f7f 100644 --- a/lib/Db/VoteMapper.php +++ b/lib/Db/VoteMapper.php @@ -34,14 +34,12 @@ public function __construct( public function update(Entity $entity): Vote { $entity->setVoteOptionHash(Hash::getOptionHash($entity->getPollId(), $entity->getVoteOptionText())); - $entity->setVoteOptionHashBin(Hash::getOptionHashBin($entity->getPollId(), $entity->getVoteOptionText())); $entity = parent::update($entity); return $this->find($entity->getId()); } public function insert(Entity $entity): Vote { $entity->setVoteOptionHash(Hash::getOptionHash($entity->getPollId(), $entity->getVoteOptionText())); - $entity->setVoteOptionHashBin(Hash::getOptionHashBin($entity->getPollId(), $entity->getVoteOptionText())); $entity = parent::insert($entity); return $this->find($entity->getId()); } @@ -173,7 +171,7 @@ public function removeOrphanedVotes(): int { 'votes', Option::TABLE, 'options', - 'votes.poll_id = options.poll_id AND votes.vote_option_hash_bin = options.poll_option_hash_bin' + 'votes.poll_id = options.poll_id AND votes.vote_option_hash = options.poll_option_hash' ); $qb->where('options.poll_id IS NULL'); @@ -259,7 +257,7 @@ protected function joinOption( $joinAlias, $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), - $qb->expr()->eq($joinAlias . '.poll_option_hash_bin', $fromAlias . '.vote_option_hash_bin'), + $qb->expr()->eq($joinAlias . '.poll_option_hash', $fromAlias . '.vote_option_hash'), $qb->expr()->eq($joinAlias . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)), ) ); diff --git a/lib/Helper/Hash.php b/lib/Helper/Hash.php index f7aedf99fa..ff5695d717 100644 --- a/lib/Helper/Hash.php +++ b/lib/Helper/Hash.php @@ -44,6 +44,7 @@ public static function getOptionHash(int $pollId, string $optionText): string { * @param int $pollId * @param string $optionText * @return string binary 16-byte MD5 hash + * @psalm-api */ public static function getOptionHashBin(int $pollId, string $optionText): string { return hash('md5', $pollId . $optionText, true); diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index 78dff40fcb..21ead0dff9 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -72,7 +72,7 @@ abstract class TableSchema { ], Option::TABLE => [ 'polls_options' => ['columns' => ['poll_id', 'deleted']], - 'polls_options_hash' => ['columns' => ['poll_id', 'poll_option_hash_bin', 'deleted']], + 'polls_options_hash' => ['columns' => ['poll_id', 'poll_option_hash', 'deleted']], 'polls_options_owner' => ['columns' => ['poll_id', 'owner']], ], Share::TABLE => [ @@ -83,7 +83,7 @@ abstract class TableSchema { Vote::TABLE => [ 'polls_votes_answers' => ['columns' => ['poll_id', 'user_id']], 'polls_votes_user' => ['columns' => ['poll_id', 'vote_answer', 'user_id']], - 'polls_votes_hash' => ['columns' => ['poll_id', 'vote_option_hash_bin', 'deleted']], + 'polls_votes_hash' => ['columns' => ['poll_id', 'vote_option_hash', 'deleted']], ], ]; @@ -96,7 +96,7 @@ abstract class TableSchema { public const UNIQUE_INDICES = [ Option::TABLE => [ 'UNIQ_options' => ['columns' => ['poll_id', 'poll_option_hash', 'timestamp']], - 'UNIQ_options_bin' => ['columns' => ['poll_id', 'poll_option_hash_bin', 'timestamp']], + 'UNIQ_options_bin' => ['columns' => ['poll_id', 'poll_option_hash', 'timestamp']], ], Log::TABLE => [ 'UNIQ_unprocessed' => ['columns' => ['processed', 'poll_id', 'user_id', 'message_id']], @@ -110,7 +110,7 @@ abstract class TableSchema { ], Vote::TABLE => [ 'UNIQ_votes' => ['columns' => ['poll_id', 'user_id', 'vote_option_hash']], - 'UNIQ_votes_bin' => ['columns' => ['poll_id', 'user_id', 'vote_option_hash_bin']], + 'UNIQ_votes_bin' => ['columns' => ['poll_id', 'user_id', 'vote_option_hash']], ], Preferences::TABLE => [ 'UNIQ_preferences' => ['columns' => ['user_id']], @@ -207,6 +207,12 @@ abstract class TableSchema { 'message', // dropped in 1.07, orphaned // 'processed', // dropped in 8.1, orphaned ], + Option::TABLE => [ + 'poll_option_hash_bin', + ], + Vote::TABLE => [ + 'vote_option_hash_bin', + ], ]; /** @@ -259,7 +265,6 @@ abstract class TableSchema { 'poll_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'poll_option_text' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 1024]], 'poll_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 32]], - 'poll_option_hash_bin' => ['type' => Types::BINARY, 'options' => ['notnull' => false, 'default' => '', 'length' => 16]], 'timestamp' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'duration' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'order' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], @@ -275,7 +280,6 @@ abstract class TableSchema { 'vote_option_id' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], 'vote_option_text' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 1024]], 'vote_option_hash' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => '', 'length' => 32]], - 'vote_option_hash_bin' => ['type' => Types::BINARY, 'options' => ['notnull' => false, 'default' => '', 'length' => 16]], 'vote_answer' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 64]], 'deleted' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], ], diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php index 3a649cfb1a..184d70b009 100644 --- a/lib/Service/OptionService.php +++ b/lib/Service/OptionService.php @@ -123,7 +123,6 @@ public function add(int $pollId, SimpleOption $simpleOption, bool $voteYes = fal try { // Insert the new option - /** @var Option $newOption */ $newOption = $this->optionMapper->insert($newOption); } catch (Exception $e) { // TODO: Change exception catch to actual exception @@ -134,7 +133,7 @@ public function add(int $pollId, SimpleOption $simpleOption, bool $voteYes = fal // Option already exists, so we need to update the existing one // and remove deleted setting - $option = $this->optionMapper->findByPollAndText($pollId, $newOption->getPollOptionHashBin(), true); + $option = $this->optionMapper->findByPollAndText($pollId, $newOption->getPollOptionHash(), true); $option->setDeleted(0); $newOption = $this->optionMapper->update($option); From 0b369de42cdfdbaa42a19c03a641db75b6ff20b8 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 13:55:17 +0200 Subject: [PATCH 25/26] cs fix Signed-off-by: dartcafe --- lib/Db/Option.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Db/Option.php b/lib/Db/Option.php index 805dca2af5..4e379c2dce 100644 --- a/lib/Db/Option.php +++ b/lib/Db/Option.php @@ -12,7 +12,6 @@ use DateTime; use DateTimeImmutable; use DateTimeZone; -use Doctrine\DBAL\Types\Types; use JsonSerializable; use OCA\Polls\Exceptions\InsufficientAttributesException; use OCA\Polls\Helper\Hash; From 6405c49007edb36e3cc83a5ad0826d89c041af3f Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 14 Aug 2025 21:27:41 +0200 Subject: [PATCH 26/26] updated readme Signed-off-by: dartcafe --- README.md | 69 ++++++++----------------------------------------------- 1 file changed, 10 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 16701a6078..7116409ed5 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ ![lint](https://github.com/nextcloud/polls/actions/workflows/lint.yml/badge.svg) [![Dependabot status](https://img.shields.io/badge/Dependabot-enabled-brightgreen.svg?longCache=true&style=flat-square&logo=dependabot)](https://dependabot.com) -[![Software License](https://img.shields.io/badge/license-AGPL-brightgreen.svg?style=flat-square)](COPYING) [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/polls)](https://api.reuse.software/info/github.com/nextcloud/polls) +[![Software License](https://img.shields.io/badge/license-AGPL-brightgreen.svg?style=flat-square)](COPYING) +[![Nextcloud21+](https://img.shields.io/badge/Nextcloud%20Version-21%2B-0082C9?logo=nextcloud)](https://nextcloud.com) # Free meeting schedule tool - :next_track_button: Easy poll creation - :hammer_and_wrench: Highly customizable - :envelope: Make your poll confidential by hiding the results until you want them to be discovered - - :dark_sunglasses: Obfuscate participants' names from other participants + - :dark_sunglasses: Obfuscate participants' names from other participants or set strong anonymous mode - :timer_clock: Set an automatic expiry date - :heavy_plus_sign: Allow participants to add more options - :white_check_mark: Limit votes per option or user @@ -34,23 +35,14 @@ - Contacts - Activity -## Installation / Update -This app is supposed to work on Nextcloud version 21+. +Find Polls in the [Nextcloud app store](https://apps.nextcloud.com/apps/polls). -### Install latest release -You can download and install the latest release from the [Nextcloud app store](https://apps.nextcloud.com/apps/polls). +## Installation / Update +See [wiki](https://github.com/nextcloud/polls/wiki/Installation-help). ## Available occ commands -| Command | Description | -| - | - | -| `polls:db:clean-migrations` | Remove obsolete migrations, which are no more needed | -| `polls:db:purge` | Drop Polls' tables and remove migration and settings records | -| `polls:db:rebuild` | Rebuild Polls' database including indices | -| `polls:index:create` | Create all necessary indices and foreign key constraints | -| `polls:index:remove` | Remove all indices | -| `polls:poll:transfer-ownership ` | Transfer poll ownership from to | -| `polls:share:add [--user USER] [--group GROUP] [--email EMAIL] [--] ` | Add user/group/email with to shares | -| `polls:share:remove [--user USER] [--group GROUP] [--email EMAIL] [--] ` | Remove user/group/email with from shares | +See [wiki](https://github.com/nextcloud/polls/wiki/OCC-commands). + ## Support - Report a bug or request a feature: https://github.com/nextcloud/polls/issues - Community support: https://help.nextcloud.com/c/apps/polls/ @@ -60,49 +52,8 @@ Manage your polls and create new ones | Many configuration options | Share your :-:|:-:|:-:|:-: ![Manage Polls](screenshots/overview.png) | ![Vote](screenshots/edit-poll.png) | ![Edit poll](screenshots/share.png) | ![Share poll](screenshots/vote.png) -## For the 7.x release branch (v7.x) please switch to the [master-7 branch](https://github.com/nextcloud/polls/tree/master-7) -### Install from git -If you want to run the latest development version from git source, you need to clone the repo to your apps folder: - -``` -git clone https://github.com/nextcloud/polls.git -``` - -* Install dev environment with ```make setup-dev``` or -* install runtime environment with ```make setup-build``` -* Compile javascript with ```npm run build``` -* Run a complete build with ```make appstore``` (Find the output in the build directory) -* call `occ app:enable polls` to enable Polls - -### Installation variants - -### First time install -Nextcloud executes -* unexecuted `migration classes` (not listed in the `*_migrations` table) and the -* `install` repair step. - -### After a version update (changed version attribute in appinfo/info.xml) -Nextcloud executes -* `pre-migration` repair steps, -* unexecuted `migration classes` (not listed in the `*_migrations` table) and the -* `post-migration` repair steps - -### Enabling already installed but disabled app without version change -Nextcloud executes -* `pre-migration` repair steps, -* unexecuted `migration classes` (not listed in the `*_migrations` table) and the -* `post-migration` repair steps and the -* `install` repair step - -❗ As a compromise at the moment we allow the index creation to be ran twice when enabling the app via app store or `occ`, to ensure all indexes are created properly for every install/update/enabling path. - -## Removing Polls from instance -Call `occ polls:db:purge` to remove Polls completely. -* removes all Polls related tables -* removes all Polls related migration records -* removes all Polls related app config records (this also disables Polls) - -This does not remove Polls' files (call `occ app:remove polls` to remove it complete afterwards) but it resets Polls into an 'uninstalled' state. Enabling the app is then equivalent to a first time install and calls the migration and the install repair step (see above). +### Installation help +See [wiki](https://github.com/nextcloud/polls/wiki/Installation-help). ## Contribution Guidelines Please read the [Code of Conduct](https://nextcloud.com/community/code-of-conduct/). This document offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other.