Skip to content

Commit fe58a06

Browse files
author
Robert
committed
feat(forms): add external notification recipients
Signed-off-by: Robert <robert@local>
1 parent 0a0539f commit fe58a06

File tree

17 files changed

+346
-12
lines changed

17 files changed

+346
-12
lines changed

docs/API_v3.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Returns the full-depth object of the requested form (without submissions).
171171
"submitMultiple": true,
172172
"allowEditSubmissions": false,
173173
"notifyOwnerOnSubmission": false,
174+
"notificationRecipients": ["team@example.com"],
174175
"showExpiration": false,
175176
"canSubmit": true,
176177
"state": 0,
@@ -277,6 +278,7 @@ Update a single or multiple properties of a form-object. Concerns **only** the F
277278
- To link a file for submissions, the _keyValuePairs_ need to contain the keys `path` and `fileFormat`
278279
- To unlink a file for submissions, the _keyValuePairs_ need to contain the keys `fileId` and `fileFormat` need to contain the value `null`
279280
- `notifyOwnerOnSubmission` must be a boolean.
281+
- `notificationRecipients` must be an array of valid email addresses.
280282
- Response: **Status-Code OK**, as well as the id of the updated form.
281283

282284
```

docs/DataStructure.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This document describes the Object-Structure, that is used within the Forms App
2222
| ownerId | String | | The nextcloud userId of the form owner |
2323
| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) |
2424
| notifyOwnerOnSubmission | Boolean | | If the form owner should receive an email notification with a response summary for each new submission |
25+
| notificationRecipients | Array of Strings | | Additional email recipients to notify for each new submission, independent of `notifyOwnerOnSubmission` |
2526
| created | unix timestamp | | When the form has been created |
2627
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
2728
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
@@ -68,7 +69,8 @@ This document describes the Object-Structure, that is used within the Forms App
6869
"submissions": [],
6970
"submissionCount": 0,
7071
"submissionMessage": "string",
71-
"notifyOwnerOnSubmission": false
72+
"notifyOwnerOnSubmission": false,
73+
"notificationRecipients": ["team@example.com"]
7274
}
7375
```
7476

lib/Controller/ApiController.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use OCP\IUser;
5454
use OCP\IUserManager;
5555
use OCP\IUserSession;
56+
use OCP\Mail\IMailer;
5657

5758
use Psr\Log\LoggerInterface;
5859

@@ -69,6 +70,8 @@
6970
* @psalm-import-type FormsUploadedFile from ResponseDefinitions
7071
*/
7172
class ApiController extends OCSController {
73+
private const MAX_NOTIFICATION_RECIPIENTS = 20;
74+
7275
private ?IUser $currentUser;
7376

7477
public function __construct(
@@ -91,6 +94,7 @@ public function __construct(
9194
private UploadedFileMapper $uploadedFileMapper,
9295
private IMimeTypeDetector $mimeTypeDetector,
9396
private IJobList $jobList,
97+
private IMailer $mailer,
9498
) {
9599
parent::__construct($appName, $request);
96100
$this->currentUser = $userSession->getUser();
@@ -181,6 +185,7 @@ public function newForm(?int $fromId = null): DataResponse {
181185
$form->setIsAnonymous(false);
182186
$form->setNotifyOwnerOnSubmission(false);
183187
$form->setState(Constants::FORM_STATE_ACTIVE);
188+
$form->setNotificationRecipients([]);
184189

185190
$this->formMapper->insert($form);
186191
} else {
@@ -211,6 +216,7 @@ public function newForm(?int $fromId = null): DataResponse {
211216
$formData['isAnonymous'] = false;
212217
$formData['notifyOwnerOnSubmission'] = false;
213218
$formData['state'] = Constants::FORM_STATE_ACTIVE;
219+
$formData['notificationRecipients'] = [];
214220

215221
$form = Form::fromParams($formData);
216222
$this->formMapper->insert($form);
@@ -325,6 +331,10 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
325331
throw new OCSBadRequestException('notifyOwnerOnSubmission must be a boolean');
326332
}
327333

334+
if (array_key_exists('notificationRecipients', $keyValuePairs)) {
335+
$keyValuePairs['notificationRecipients'] = $this->normalizeNotificationRecipients($keyValuePairs['notificationRecipients']);
336+
}
337+
328338
// Process file linking
329339
if (isset($keyValuePairs['path']) && isset($keyValuePairs['fileFormat'])) {
330340
$file = $this->submissionService->writeFileToCloud($form, $keyValuePairs['path'], $keyValuePairs['fileFormat']);
@@ -1836,6 +1846,42 @@ private function checkAccessUpdate(array $keyValuePairs): void {
18361846
}
18371847
}
18381848

1849+
/**
1850+
* @param mixed $notificationRecipients
1851+
* @return list<string>
1852+
*/
1853+
private function normalizeNotificationRecipients(mixed $notificationRecipients): array {
1854+
if (!is_array($notificationRecipients)) {
1855+
throw new OCSBadRequestException('notificationRecipients must be an array');
1856+
}
1857+
1858+
$normalizedRecipients = [];
1859+
foreach ($notificationRecipients as $recipient) {
1860+
if (!is_string($recipient)) {
1861+
throw new OCSBadRequestException('notificationRecipients must be an array of strings');
1862+
}
1863+
1864+
$trimmedRecipient = trim($recipient);
1865+
if ($trimmedRecipient === '') {
1866+
continue;
1867+
}
1868+
1869+
if (!$this->mailer->validateMailAddress($trimmedRecipient)) {
1870+
throw new OCSBadRequestException('notificationRecipients contains an invalid email address');
1871+
}
1872+
1873+
$recipientKey = strtolower($trimmedRecipient);
1874+
if (!isset($normalizedRecipients[$recipientKey])) {
1875+
$normalizedRecipients[$recipientKey] = $trimmedRecipient;
1876+
}
1877+
}
1878+
1879+
if (count($normalizedRecipients) > self::MAX_NOTIFICATION_RECIPIENTS) {
1880+
throw new OCSBadRequestException('Too many notificationRecipients');
1881+
}
1882+
1883+
return array_values($normalizedRecipients);
1884+
}
18391885
/**
18401886
* Checks if the current user is allowed to archive/unarchive the form
18411887
*/

lib/Db/Form.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
* @method void setSubmissionMessage(string|null $value)
4646
* @method bool getNotifyOwnerOnSubmission()
4747
* @method void setNotifyOwnerOnSubmission(bool $value)
48+
* @method string|null getNotificationRecipientsJson()
49+
* @method void setNotificationRecipientsJson(?string $value)
4850
* @method int getState()
4951
* @psalm-method 0|1|2 getState()
5052
* @method void setState(int|null $value)
@@ -72,6 +74,7 @@ class Form extends Entity {
7274
protected $showExpiration;
7375
protected $submissionMessage;
7476
protected $notifyOwnerOnSubmission;
77+
protected $notificationRecipientsJson;
7578
protected $lastUpdated;
7679
protected $state;
7780
protected $lockedBy;
@@ -146,6 +149,37 @@ public function setAccess(array $access): void {
146149
$this->setAccessEnum($value);
147150
}
148151

152+
/**
153+
* @return list<string>
154+
*/
155+
public function getNotificationRecipients(): array {
156+
$encodedRecipients = $this->getNotificationRecipientsJson();
157+
if ($encodedRecipients === null || $encodedRecipients === '') {
158+
return [];
159+
}
160+
161+
$decodedRecipients = json_decode($encodedRecipients, true, 512, JSON_THROW_ON_ERROR);
162+
if (!is_array($decodedRecipients)) {
163+
return [];
164+
}
165+
166+
return array_values(array_filter(array_map(static fn (mixed $recipient): string => trim((string)$recipient), $decodedRecipients), static fn (string $recipient): bool => $recipient !== ''));
167+
}
168+
169+
/**
170+
* @param list<string> $recipients
171+
*/
172+
public function setNotificationRecipients(array $recipients): void {
173+
$normalizedRecipients = array_values(array_filter(array_map(static fn (string $recipient): string => trim($recipient), $recipients), static fn (string $recipient): bool => $recipient !== ''));
174+
175+
if ($normalizedRecipients === []) {
176+
$this->setNotificationRecipientsJson(null);
177+
return;
178+
}
179+
180+
$this->setNotificationRecipientsJson(json_encode($normalizedRecipients, JSON_THROW_ON_ERROR));
181+
}
182+
149183
/**
150184
* @return array{
151185
* id: int,
@@ -165,6 +199,7 @@ public function setAccess(array $access): void {
165199
* lastUpdated: int,
166200
* submissionMessage: ?string,
167201
* notifyOwnerOnSubmission: bool,
202+
* notificationRecipients: list<string>,
168203
* state: 0|1|2,
169204
* lockedBy: ?string,
170205
* lockedUntil: ?int,
@@ -190,6 +225,7 @@ public function read() {
190225
'lastUpdated' => (int)$this->getLastUpdated(),
191226
'submissionMessage' => $this->getSubmissionMessage(),
192227
'notifyOwnerOnSubmission' => (bool)$this->getNotifyOwnerOnSubmission(),
228+
'notificationRecipients' => $this->getNotificationRecipients(),
193229
'state' => $this->getState(),
194230
'lockedBy' => $this->getLockedBy(),
195231
'lockedUntil' => $this->getLockedUntil(),

lib/FormsMigrator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
149149
$form->setAllowEditSubmissions($formData['allowEditSubmissions']);
150150
$form->setShowExpiration($formData['showExpiration']);
151151
$form->setNotifyOwnerOnSubmission($formData['notifyOwnerOnSubmission'] ?? false);
152+
$form->setNotificationRecipients($formData['notificationRecipients'] ?? []);
152153

153154
$this->formMapper->insert($form);
154155

lib/Listener/OwnerNotificationListener.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,28 @@ public function handle(Event $event): void {
4242
}
4343

4444
$form = $event->getForm();
45-
if (!$form->getNotifyOwnerOnSubmission()) {
46-
return;
45+
$submission = $event->getSubmission();
46+
$recipients = $form->getNotificationRecipients();
47+
48+
if ($form->getNotifyOwnerOnSubmission()) {
49+
$owner = $this->userManager->get($form->getOwnerId());
50+
$ownerMail = $owner?->getEMailAddress();
51+
if (is_string($ownerMail) && trim($ownerMail) !== '') {
52+
$recipients[] = trim($ownerMail);
53+
}
4754
}
4855

49-
$submission = $event->getSubmission();
50-
$owner = $this->userManager->get($form->getOwnerId());
51-
$ownerMail = trim((string)$owner?->getEMailAddress());
52-
if ($ownerMail === '') {
56+
$normalizedRecipients = [];
57+
foreach ($recipients as $recipient) {
58+
$trimmedRecipient = trim($recipient);
59+
if ($trimmedRecipient === '') {
60+
continue;
61+
}
62+
63+
$normalizedRecipients[strtolower($trimmedRecipient)] = $trimmedRecipient;
64+
}
65+
66+
if ($normalizedRecipients === []) {
5367
return;
5468
}
5569

@@ -87,7 +101,7 @@ public function handle(Event $event): void {
87101
$this->ownerNotificationMailService->send(
88102
$form,
89103
$submission,
90-
[$ownerMail],
104+
array_values($normalizedRecipients),
91105
$answerSummaries,
92106
);
93107
}

lib/Migration/Version050300Date20260228170000.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
3535
]);
3636
}
3737

38+
if (!$formsTable->hasColumn('notification_recipients_json')) {
39+
$formsTable->addColumn('notification_recipients_json', Types::TEXT, [
40+
'notnull' => false,
41+
'default' => null,
42+
]);
43+
}
44+
3845
return $schema;
3946
}
4047
}

lib/ResponseDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
* submissionCount?: int,
143143
* submissionMessage: ?string,
144144
* notifyOwnerOnSubmission: bool,
145+
* notificationRecipients: list<string>,
145146
* }
146147
*
147148
* @psalm-type FormsUploadedFile = array{

lib/Service/FormsService.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ public function getPublicForm(Form $form): array {
285285
unset($formData['filePath']);
286286
unset($formData['fileFormat']);
287287
unset($formData['notifyOwnerOnSubmission']);
288+
unset($formData['notificationRecipients']);
288289

289290
return $formData;
290291
}

openapi.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@
120120
"maxSubmissions",
121121
"shares",
122122
"submissionMessage",
123-
"notifyOwnerOnSubmission"
123+
"notifyOwnerOnSubmission",
124+
"notificationRecipients"
124125
],
125126
"properties": {
126127
"id": {
@@ -236,6 +237,12 @@
236237
},
237238
"notifyOwnerOnSubmission": {
238239
"type": "boolean"
240+
},
241+
"notificationRecipients": {
242+
"type": "array",
243+
"items": {
244+
"type": "string"
245+
}
239246
}
240247
}
241248
},

0 commit comments

Comments
 (0)