From 74502e60a3a909e3d049e1624d19818a3648116d Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Fri, 9 Oct 2026 16:09:07 +0200 Subject: [PATCH] fe_comment: recipients are the FE users of the feedback group - The selectable recipients are no longer a hand-maintained setting but every active frontend user of the feedback group (feUserGroup) with a valid e-mail address; the id is the fe_users uid, so a stored selection survives users joining or leaving. The `recipients` setting is marked unused - Each recipient in the state payload carries label, email and selected (the stored choice), for a checkbox list with "select all" above the submit button - submit accepts the checked ids directly ({"recipients": [uids]}), validates them against the group, stores and uses them, and reports the addresses used as sentTo; no selection falls back to mailTo - migrations/check_fecomment_user.php: all summaries and items of one user incl. deleted ones, for diagnosis --- migrations/check_fecomment_user.php | 35 +++++++++++ .../Middleware/FeedbackApiMiddleware.php | 24 +++++-- .../Classes/Service/FeedbackState.php | 63 +++++++++++++++++-- packages/fe_comment/ext_conf_template.txt | 2 +- public/_frontend/index.html | 4 +- 5 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 migrations/check_fecomment_user.php diff --git a/migrations/check_fecomment_user.php b/migrations/check_fecomment_user.php new file mode 100644 index 0000000..58b464d --- /dev/null +++ b/migrations/check_fecomment_user.php @@ -0,0 +1,35 @@ +\n"); + exit(1); +} +$settings = require __DIR__ . '/../config/system/settings.php'; +$db = $settings['DB']['Connections']['Default']; +$pdo = new PDO('mysql:host=' . $db['host'] . ';dbname=' . $db['dbname'] . ';charset=utf8mb4', $db['user'], $db['password']); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$summaries = $pdo->prepare( + "SELECT uid, status, deleted, FROM_UNIXTIME(crdate) AS created, FROM_UNIXTIME(tstamp) AS changed, + FROM_UNIXTIME(NULLIF(sent_at,0)) AS sent, recipients, CHAR_LENGTH(general_notes) AS notes_len + FROM tx_fecomment_summary WHERE fe_user = ? ORDER BY uid" +); +$summaries->execute([$feUser]); +$items = $pdo->prepare( + "SELECT uid, deleted, FROM_UNIXTIME(crdate) AS created, FROM_UNIXTIME(tstamp) AS changed, url, + CHAR_LENGTH(note) AS note_len + FROM tx_fecomment_feedback WHERE summary = ? ORDER BY sorting" +); +foreach ($summaries->fetchAll(PDO::FETCH_ASSOC) as $s) { + echo "Summary {$s['uid']} | status={$s['status']} deleted={$s['deleted']} | erstellt {$s['created']} | geaendert {$s['changed']}" + . ($s['sent'] ? " | gesendet {$s['sent']}" : '') . " | recipients=[{$s['recipients']}] | general_notes " . (int)$s['notes_len'] . " Zeichen\n"; + $items->execute([(int)$s['uid']]); + foreach ($items->fetchAll(PDO::FETCH_ASSOC) as $i) { + echo " Item {$i['uid']} | deleted={$i['deleted']} | erstellt {$i['created']} | geaendert {$i['changed']} | note " . (int)$i['note_len'] . " Zeichen | {$i['url']}\n"; + } +} diff --git a/packages/fe_comment/Classes/Middleware/FeedbackApiMiddleware.php b/packages/fe_comment/Classes/Middleware/FeedbackApiMiddleware.php index 4b841cd..e751d94 100644 --- a/packages/fe_comment/Classes/Middleware/FeedbackApiMiddleware.php +++ b/packages/fe_comment/Classes/Middleware/FeedbackApiMiddleware.php @@ -79,7 +79,7 @@ final class FeedbackApiMiddleware implements MiddlewareInterface $route === 'upload' => $this->upload($request, $user), (bool)preg_match('#^attachment/(\d+)/delete$#', $route, $m) => $this->deleteAttachment($user, (int)$m[1]), $route === 'summary' => $this->updateSummary($request, $user), - $route === 'submit' => $this->submit($user), + $route === 'submit' => $this->submit($request, $user), default => new JsonResponse(['error' => 'unknown route'], 404), }; } catch (\Throwable $e) { @@ -237,7 +237,7 @@ final class FeedbackApiMiddleware implements MiddlewareInterface } /** @param array{uid:int,username:string,name:string,email:string} $user */ - private function submit(array $user): ResponseInterface + private function submit(ServerRequestInterface $request, array $user): ResponseInterface { $summary = $this->openSummary($user['uid'], false); if ($summary === null) { @@ -249,6 +249,19 @@ final class FeedbackApiMiddleware implements MiddlewareInterface } $recipientMap = $this->recipientMap(); + + // The checkbox selection may travel with the submit itself + // ({"recipients": [ids]}) - validated against the configured list, + // stored on the summary, then used. Without it the stored selection + // applies; an empty selection falls back to mailTo. + $body = $this->jsonBody($request); + if (array_key_exists('recipients', $body) && is_array($body['recipients'])) { + $chosenIds = array_values(array_intersect(array_map('intval', $body['recipients']), array_keys($recipientMap))); + $summary['recipients'] = implode(',', $chosenIds); + GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_SUMMARY) + ->update(self::T_SUMMARY, ['recipients' => $summary['recipients'], 'tstamp' => time()], ['uid' => (int)$summary['uid']]); + } + $chosen = GeneralUtility::intExplode(',', (string)$summary['recipients'], true); $to = []; foreach ($chosen as $id) { @@ -307,7 +320,8 @@ final class FeedbackApiMiddleware implements MiddlewareInterface GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_SUMMARY) ->update(self::T_SUMMARY, ['status' => 'sent', 'sent_at' => time(), 'tstamp' => time()], ['uid' => (int)$summary['uid']]); - return new JsonResponse(['ok' => true, 'sent' => count($payload['items'])]); + // sentTo: the addresses actually used, for the confirmation message + return new JsonResponse(['ok' => true, 'sent' => count($payload['items']), 'sentTo' => array_values($to)]); } // ---- helpers ------------------------------------------------------------ @@ -379,10 +393,10 @@ final class FeedbackApiMiddleware implements MiddlewareInterface ->update(self::T_FEEDBACK, ['attachments' => $count, 'tstamp' => time()], ['uid' => $feedbackUid]); } - /** @return array id => recipient */ + /** @return array fe_users uid => recipient (FE users of the feedback group) */ private function recipientMap(): array { - return $this->settings->recipients(); + return $this->stateService()->recipientMap(); } /** @return array */ diff --git a/packages/fe_comment/Classes/Service/FeedbackState.php b/packages/fe_comment/Classes/Service/FeedbackState.php index 18423ce..f989e39 100644 --- a/packages/fe_comment/Classes/Service/FeedbackState.php +++ b/packages/fe_comment/Classes/Service/FeedbackState.php @@ -76,7 +76,7 @@ final class FeedbackState return [ 'allowed' => true, 'user' => ['name' => $user['name'], 'email' => $user['email']], - 'recipients' => $this->recipientOptions(), + 'recipients' => $this->recipientOptions($summaryPayload['recipients'] ?? []), 'overviewUrl' => $this->overviewUrl(), 'summary' => $summaryPayload, // the submit button may be active (the endpoint rejects an empty summary) @@ -91,7 +91,7 @@ final class FeedbackState * bodies except `upload` (multipart: file + feedback). */ private const ACTIONS = [ - 'submit' => ['method' => 'POST', 'url' => '/api/fe-comment/submit'], + 'submit' => ['method' => 'POST', 'url' => '/api/fe-comment/submit', 'body' => ['recipients' => 'int[] (optional, the checked recipient ids)']], 'updateSummary' => ['method' => 'POST', 'url' => '/api/fe-comment/summary', 'body' => ['generalNotes' => 'html', 'recipients' => 'int[]']], 'createItem' => ['method' => 'POST', 'url' => '/api/fe-comment/feedback', 'body' => ['url' => 'string', 'note' => 'html']], 'updateItem' => ['method' => 'POST', 'url' => '/api/fe-comment/feedback/{uid}', 'body' => ['url' => 'string', 'note' => 'html']], @@ -168,12 +168,63 @@ final class FeedbackState ], $rows); } - /** @return array */ - private function recipientOptions(): array + /** + * Who a summary can be sent to: every active FE user of the feedback + * group (extension setting feUserGroup) with an e-mail address - the + * same group that may use the tool (decision Olli 2026-10-09). Keyed by + * fe_users uid, which doubles as the id the front end sends back, so a + * selection stays valid while users join or leave. Group 0 = nobody + * (never all frontend users). + * + * @return array fe_users uid => recipient + */ + public function recipientMap(): array + { + $group = $this->settings->feUserGroup(); + if ($group <= 0) { + return []; + } + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_users'); + $rows = $queryBuilder->select('uid', 'username', 'name', 'email')->from('fe_users') + ->where( + $queryBuilder->expr()->inSet('usergroup', (string)$group), + $queryBuilder->expr()->neq('email', $queryBuilder->createNamedParameter('')) + ) + ->orderBy('name')->addOrderBy('username') + ->executeQuery()->fetchAllAssociative(); + $map = []; + foreach ($rows as $row) { + $email = trim((string)$row['email']); + if (!GeneralUtility::validEmail($email)) { + continue; + } + $map[(int)$row['uid']] = [ + 'label' => trim((string)$row['name']) ?: (string)$row['username'], + 'email' => $email, + ]; + } + return $map; + } + + /** + * The recipients for the checkbox list above the submit button. + * `selected` mirrors the summary's stored choice, so checkboxes survive + * a reload. Addresses are shown on purpose: only logged-in members of + * the feedback group ever see this payload. + * + * @param int[] $selectedIds + * @return array + */ + private function recipientOptions(array $selectedIds): array { $options = []; - foreach ($this->settings->recipients() as $id => $recipient) { - $options[] = ['id' => $id, 'label' => $recipient['label']]; + foreach ($this->recipientMap() as $id => $recipient) { + $options[] = [ + 'id' => $id, + 'label' => $recipient['label'], + 'email' => $recipient['email'], + 'selected' => in_array($id, $selectedIds, true), + ]; } return $options; } diff --git a/packages/fe_comment/ext_conf_template.txt b/packages/fe_comment/ext_conf_template.txt index 768f176..77db7b1 100644 --- a/packages/fe_comment/ext_conf_template.txt +++ b/packages/fe_comment/ext_conf_template.txt @@ -19,7 +19,7 @@ jsonOutput = 1 # cat=access//10; type=string; label=FE user group (uid): Only logged-in frontend users in this group see and use the widget. 0 = any logged-in user. feUserGroup = 0 -# cat=mail//50; type=string; label=Selectable recipients: who the feedback can be sent to, as "Label " entries separated by semicolons. Empty = only the Mail to address above. +# cat=mail//50; type=string; label=Selectable recipients (UNUSED since 2026-10-09): recipients are now the frontend users of the FE user group above that have an e-mail address. Kept only so existing configurations stay valid. recipients = # cat=basic//20; type=string; label=Storage pid: page/folder uid the feedback records are stored on (backend visibility). 0 = root. diff --git a/public/_frontend/index.html b/public/_frontend/index.html index 87241ed..281ba76 100644 --- a/public/_frontend/index.html +++ b/public/_frontend/index.html @@ -23,8 +23,8 @@ VITEC - - + +