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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ Returns the full-depth object of the requested form (without submissions).
"isAnonymous": false,
"submitMultiple": true,
"allowEditSubmissions": false,
"notifyOwnerOnSubmission": false,
"attachSubmissionPdf": false,
"notificationRecipients": ["team@example.com"],
"showExpiration": false,
"canSubmit": true,
"state": 0,
Expand Down Expand Up @@ -275,6 +278,9 @@ Update a single or multiple properties of a form-object. Concerns **only** the F
- To transfer the ownership of a form to another user, you must only send a single _keyValuePair_ containing the key `ownerId` and the user id of the new owner.
- To link a file for submissions, the _keyValuePairs_ need to contain the keys `path` and `fileFormat`
- To unlink a file for submissions, the _keyValuePairs_ need to contain the keys `fileId` and `fileFormat` need to contain the value `null`
- `notifyOwnerOnSubmission` must be a boolean.
- `attachSubmissionPdf` must be a boolean.
- `notificationRecipients` must be an array of valid email addresses.
- Response: **Status-Code OK**, as well as the id of the updated form.

```
Expand Down
8 changes: 7 additions & 1 deletion docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ This document describes the Object-Structure, that is used within the Forms App
| description | String | max. 8192 ch. | The Form description |
| ownerId | String | | The nextcloud userId of the form owner |
| 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) |
| notifyOwnerOnSubmission | Boolean | | If the form owner should receive an email notification with a response summary for each new submission |
| attachSubmissionPdf | Boolean | | If notification emails should include a PDF attachment with the submitted responses |
| notificationRecipients | Array of Strings | | Additional email recipients to notify for each new submission, independent of `notifyOwnerOnSubmission` |
| created | unix timestamp | | When the form has been created |
| access | [Access-Object](#access-object) | | Describing access-settings of the form |
| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ |
Expand Down Expand Up @@ -66,7 +69,10 @@ This document describes the Object-Structure, that is used within the Forms App
"shares": []
"submissions": [],
"submissionCount": 0,
"submissionMessage": "string"
"submissionMessage": "string",
"notifyOwnerOnSubmission": false,
"attachSubmissionPdf": false,
"notificationRecipients": ["team@example.com"]
}
```

Expand Down
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

use OCA\Analytics\Datasource\DatasourceEvent;
use OCA\Forms\Capabilities;
use OCA\Forms\Events\FormSubmittedEvent;
use OCA\Forms\FormsMigrator;
use OCA\Forms\Listener\AnalyticsDatasourceListener;
use OCA\Forms\Listener\OwnerNotificationListener;
use OCA\Forms\Listener\UserDeletedListener;
use OCA\Forms\Middleware\ThrottleFormAccessMiddleware;
use OCA\Forms\Search\SearchProvider;
Expand Down Expand Up @@ -43,6 +45,7 @@ public function register(IRegistrationContext $context): void {

$context->registerCapability(Capabilities::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(FormSubmittedEvent::class, OwnerNotificationListener::class);
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
$context->registerMiddleware(ThrottleFormAccessMiddleware::class);
$context->registerSearchProvider(SearchProvider::class);
Expand Down
65 changes: 63 additions & 2 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\UploadedFile;
use OCA\Forms\Db\UploadedFileMapper;
use OCA\Forms\Events\FormSubmittedEvent;
use OCA\Forms\Exception\NoSuchFormException;
use OCA\Forms\ResponseDefinitions;
use OCA\Forms\Service\ConfigService;
Expand Down Expand Up @@ -52,6 +53,7 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Mail\IMailer;

use Psr\Log\LoggerInterface;

Expand All @@ -68,6 +70,8 @@
* @psalm-import-type FormsUploadedFile from ResponseDefinitions
*/
class ApiController extends OCSController {
private const MAX_NOTIFICATION_RECIPIENTS = 20;

private ?IUser $currentUser;

public function __construct(
Expand All @@ -90,6 +94,7 @@ public function __construct(
private UploadedFileMapper $uploadedFileMapper,
private IMimeTypeDetector $mimeTypeDetector,
private IJobList $jobList,
private IMailer $mailer,
) {
parent::__construct($appName, $request);
$this->currentUser = $userSession->getUser();
Expand Down Expand Up @@ -178,6 +183,10 @@ public function newForm(?int $fromId = null): DataResponse {
$form->setShowExpiration(false);
$form->setExpires(0);
$form->setIsAnonymous(false);
$form->setNotifyOwnerOnSubmission(false);
$form->setState(Constants::FORM_STATE_ACTIVE);
$form->setAttachSubmissionPdf(false);
$form->setNotificationRecipients([]);

$this->formMapper->insert($form);
} else {
Expand Down Expand Up @@ -206,6 +215,10 @@ public function newForm(?int $fromId = null): DataResponse {
$formData['showExpiration'] = false;
$formData['expires'] = 0;
$formData['isAnonymous'] = false;
$formData['notifyOwnerOnSubmission'] = false;
$formData['state'] = Constants::FORM_STATE_ACTIVE;
$formData['attachSubmissionPdf'] = false;
$formData['notificationRecipients'] = [];

$form = Form::fromParams($formData);
$this->formMapper->insert($form);
Expand Down Expand Up @@ -316,6 +329,18 @@ public function updateForm(int $formId, array $keyValuePairs): DataResponse {
// Do not allow changing showToAllUsers or permitAllUsers if disabled
$this->checkAccessUpdate($keyValuePairs);

if (isset($keyValuePairs['notifyOwnerOnSubmission']) && !is_bool($keyValuePairs['notifyOwnerOnSubmission'])) {
throw new OCSBadRequestException('notifyOwnerOnSubmission must be a boolean');
}

if (isset($keyValuePairs['attachSubmissionPdf']) && !is_bool($keyValuePairs['attachSubmissionPdf'])) {
throw new OCSBadRequestException('attachSubmissionPdf must be a boolean');
}

if (array_key_exists('notificationRecipients', $keyValuePairs)) {
$keyValuePairs['notificationRecipients'] = $this->normalizeNotificationRecipients($keyValuePairs['notificationRecipients']);
}

// Process file linking
if (isset($keyValuePairs['path']) && isset($keyValuePairs['fileFormat'])) {
$file = $this->submissionService->writeFileToCloud($form, $keyValuePairs['path'], $keyValuePairs['fileFormat']);
Expand Down Expand Up @@ -1405,7 +1430,7 @@ public function newSubmission(int $formId, array $answers, string $shareHash = '
$this->formMapper->update($form);

//Create Activity
$this->formsService->notifyNewSubmission($form, $submission);
$this->formsService->notifyNewSubmission($form, $submission, FormSubmittedEvent::TRIGGER_CREATED);

if ($form->getFileId() !== null) {
$this->jobList->add(SyncSubmissionsWithLinkedFileJob::class, ['form_id' => $form->getId()]);
Expand Down Expand Up @@ -1487,7 +1512,7 @@ public function updateSubmission(int $formId, int $submissionId, array $answers)
}

//Create Activity
$this->formsService->notifyNewSubmission($form, $submission);
$this->formsService->notifyNewSubmission($form, $submission, FormSubmittedEvent::TRIGGER_UPDATED);

return new DataResponse($submissionId);
}
Expand Down Expand Up @@ -1827,6 +1852,42 @@ private function checkAccessUpdate(array $keyValuePairs): void {
}
}

/**
* @param mixed $notificationRecipients
* @return list<string>
*/
private function normalizeNotificationRecipients(mixed $notificationRecipients): array {
if (!is_array($notificationRecipients)) {
throw new OCSBadRequestException('notificationRecipients must be an array');
}

$normalizedRecipients = [];
foreach ($notificationRecipients as $recipient) {
if (!is_string($recipient)) {
throw new OCSBadRequestException('notificationRecipients must be an array of strings');
}

$trimmedRecipient = trim($recipient);
if ($trimmedRecipient === '') {
continue;
}

if (!$this->mailer->validateMailAddress($trimmedRecipient)) {
throw new OCSBadRequestException('notificationRecipients contains an invalid email address');
}

$recipientKey = strtolower($trimmedRecipient);
if (!isset($normalizedRecipients[$recipientKey])) {
$normalizedRecipients[$recipientKey] = $trimmedRecipient;
}
}

if (count($normalizedRecipients) > self::MAX_NOTIFICATION_RECIPIENTS) {
throw new OCSBadRequestException('Too many notificationRecipients');
}

return array_values($normalizedRecipients);
}
/**
* Checks if the current user is allowed to archive/unarchive the form
*/
Expand Down
48 changes: 48 additions & 0 deletions lib/Db/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
* @method void setLastUpdated(int $value)
* @method string|null getSubmissionMessage()
* @method void setSubmissionMessage(string|null $value)
* @method bool getNotifyOwnerOnSubmission()
* @method void setNotifyOwnerOnSubmission(bool $value)
* @method bool getAttachSubmissionPdf()
* @method void setAttachSubmissionPdf(bool $value)
* @method string|null getNotificationRecipientsJson()
* @method void setNotificationRecipientsJson(?string $value)
* @method int getState()
* @psalm-method 0|1|2 getState()
* @method void setState(int|null $value)
Expand All @@ -69,6 +75,9 @@ class Form extends Entity {
protected $allowEditSubmissions;
protected $showExpiration;
protected $submissionMessage;
protected $notifyOwnerOnSubmission;
protected $attachSubmissionPdf;
protected $notificationRecipientsJson;
protected $lastUpdated;
protected $state;
protected $lockedBy;
Expand All @@ -85,6 +94,8 @@ public function __construct() {
$this->addType('submitMultiple', 'boolean');
$this->addType('allowEditSubmissions', 'boolean');
$this->addType('showExpiration', 'boolean');
$this->addType('notifyOwnerOnSubmission', 'boolean');
$this->addType('attachSubmissionPdf', 'boolean');
$this->addType('lastUpdated', 'integer');
$this->addType('state', 'integer');
$this->addType('lockedBy', 'string');
Expand Down Expand Up @@ -142,6 +153,37 @@ public function setAccess(array $access): void {
$this->setAccessEnum($value);
}

/**
* @return list<string>
*/
public function getNotificationRecipients(): array {
$encodedRecipients = $this->getNotificationRecipientsJson();
if ($encodedRecipients === null || $encodedRecipients === '') {
return [];
}

$decodedRecipients = json_decode($encodedRecipients, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($decodedRecipients)) {
return [];
}

return array_values(array_filter(array_map(static fn (mixed $recipient): string => trim((string)$recipient), $decodedRecipients), static fn (string $recipient): bool => $recipient !== ''));
}

/**
* @param list<string> $recipients
*/
public function setNotificationRecipients(array $recipients): void {
$normalizedRecipients = array_values(array_filter(array_map(static fn (string $recipient): string => trim($recipient), $recipients), static fn (string $recipient): bool => $recipient !== ''));

if ($normalizedRecipients === []) {
$this->setNotificationRecipientsJson(null);
return;
}

$this->setNotificationRecipientsJson(json_encode($normalizedRecipients, JSON_THROW_ON_ERROR));
}

/**
* @return array{
* id: int,
Expand All @@ -160,6 +202,9 @@ public function setAccess(array $access): void {
* showExpiration: bool,
* lastUpdated: int,
* submissionMessage: ?string,
* notifyOwnerOnSubmission: bool,
* attachSubmissionPdf: bool,
* notificationRecipients: list<string>,
* state: 0|1|2,
* lockedBy: ?string,
* lockedUntil: ?int,
Expand All @@ -184,6 +229,9 @@ public function read() {
'showExpiration' => (bool)$this->getShowExpiration(),
'lastUpdated' => (int)$this->getLastUpdated(),
'submissionMessage' => $this->getSubmissionMessage(),
'notifyOwnerOnSubmission' => (bool)$this->getNotifyOwnerOnSubmission(),
'attachSubmissionPdf' => (bool)$this->getAttachSubmissionPdf(),
'notificationRecipients' => $this->getNotificationRecipients(),
'state' => $this->getState(),
'lockedBy' => $this->getLockedBy(),
'lockedUntil' => $this->getLockedUntil(),
Expand Down
18 changes: 18 additions & 0 deletions lib/Events/FormSubmittedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,35 @@
use OCA\Forms\Db\Submission;

class FormSubmittedEvent extends AbstractFormEvent {
public const TRIGGER_CREATED = 'created';
public const TRIGGER_UPDATED = 'updated';
public const TRIGGER_VERIFIED = 'verified';

public function __construct(
Form $form,
private Submission $submission,
private string $trigger = self::TRIGGER_CREATED,
) {
parent::__construct($form);
}

public function getSubmission(): Submission {
return $this->submission;
}

public function getTrigger(): string {
return $this->trigger;
}

public function isNewSubmission(): bool {
return $this->trigger === self::TRIGGER_CREATED;
}

public function getWebhookSerializable(): array {
return [
'form' => $this->form->read(),
'submission' => $this->submission->read(),
'trigger' => $this->trigger,
];
}
}
4 changes: 3 additions & 1 deletion lib/FormsMigrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
$form->setSubmitMultiple($formData['submitMultiple']);
$form->setAllowEditSubmissions($formData['allowEditSubmissions']);
$form->setShowExpiration($formData['showExpiration']);
$form->setMaxSubmissions($formData['maxSubmissions'] ?? null);
$form->setNotifyOwnerOnSubmission($formData['notifyOwnerOnSubmission'] ?? false);
$form->setAttachSubmissionPdf($formData['attachSubmissionPdf'] ?? false);
$form->setNotificationRecipients($formData['notificationRecipients'] ?? []);

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

Expand Down
Loading