Skip to content

Commit 70ccce5

Browse files
committed
feat: importing forms
Signed-off-by: TimedIn <git@timedin.net>
1 parent d7123e2 commit 70ccce5

7 files changed

Lines changed: 920 additions & 1867 deletions

File tree

lib/Controller/ApiController.php

Lines changed: 139 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -152,23 +152,39 @@ public function getForms(string $type = 'owned'): DataResponse {
152152
* Return a copy of the form if the parameter $fromId is set
153153
*
154154
* @param ?int $fromId (optional) Id of the form that should be cloned
155+
* @param ?bool $import (optional) If it should import the form from post body
156+
* @param ?array<string, mixed> $formData (optional) The formdata to import
155157
* @return DataResponse<Http::STATUS_CREATED, FormsForm, array{}>
156158
* @throws OCSForbiddenException The user is not allowed to create forms
159+
* @throws OCSBadRequestException Cannot use both fromId and import parameters
160+
* @throws OCSBadRequestException Invalid form data: missing questions
161+
* @throws OCSBadRequestException Invalid form data: unknown properties
162+
* @throws OCSBadRequestException Invalid question data: missing id
163+
* @throws OCSBadRequestException Invalid question data: unknown properties
164+
* @throws OCSBadRequestException Invalid question data: invalid type
165+
* @throws OCSBadRequestException Invalid question data: datetime type no longer supported
166+
* @throws OCSBadRequestException Invalid question data: invalid extraSettings
167+
* @throws OCSBadRequestException Invalid option data: unknown properties
157168
*
158169
* 201: the created form
159170
*/
160171
#[CORS()]
161172
#[NoAdminRequired()]
162173
#[BruteForceProtection(action: 'form')]
163174
#[ApiRoute(verb: 'POST', url: '/api/v3/forms')]
164-
public function newForm(?int $fromId = null): DataResponse {
175+
public function newForm(?int $fromId = null, ?bool $import = false, ?array $formData = []): DataResponse {
165176
// Check if user is allowed
166177
if (!$this->configService->canCreateForms()) {
167178
$this->logger->debug('This user is not allowed to create Forms.');
168179
throw new OCSForbiddenException('This user is not allowed to create Forms.');
169180
}
170181

171-
if ($fromId === null) {
182+
// Validate mutually exclusive parameters
183+
if ($fromId !== null && $import === true) {
184+
throw new OCSBadRequestException('Cannot use both fromId and import parameters');
185+
}
186+
187+
if ($fromId === null && $import !== true) {
172188
// Create Form
173189
$form = new Form();
174190
$form->setOwnerId($this->currentUser->getUID());
@@ -186,33 +202,104 @@ public function newForm(?int $fromId = null): DataResponse {
186202
$form->setIsAnonymous(false);
187203

188204
$this->formMapper->insert($form);
205+
} elseif ($import === true) {
206+
if (!isset($formData['questions']) || !\is_array($formData['questions'])) {
207+
throw new OCSBadRequestException('Invalid form data: missing questions');
208+
}
209+
$questions = $formData['questions'];
210+
$oldConfirmationEmailQuestionId = $formData['confirmationEmailQuestionId'] ?? null;
211+
unset($formData['questions']);
212+
213+
// Validate form data whitelist
214+
$allowedFormProperties = [
215+
'title', 'description', 'access', 'expires', 'isAnonymous',
216+
'submitMultiple', 'allowEditSubmissions', 'showExpiration',
217+
'submissionMessage', 'maxSubmissions', 'confirmationEmailEnabled',
218+
'confirmationEmailSubject', 'confirmationEmailBody', 'confirmationEmailQuestionId',
219+
'allowComments',
220+
];
221+
$invalidKeys = array_diff(array_keys($formData), $allowedFormProperties);
222+
if (!empty($invalidKeys)) {
223+
throw new OCSBadRequestException('Invalid form data: unknown properties: ' . implode(', ', $invalidKeys));
224+
}
225+
226+
$formData = $this->setDefaults($formData);
227+
228+
$form = Form::fromParams($formData);
229+
$this->formMapper->insert($form);
230+
231+
foreach ($questions as $oldQuestion) {
232+
if (!isset($oldQuestion['id'])) {
233+
throw new OCSBadRequestException('Invalid question data: missing id');
234+
}
235+
236+
// Validate question property whitelist
237+
$allowedQuestionProperties = ['id', 'order', 'type', 'isRequired', 'text', 'name', 'description', 'extraSettings', 'options'];
238+
$invalidQuestionKeys = array_diff(array_keys($oldQuestion), $allowedQuestionProperties);
239+
if (!empty($invalidQuestionKeys)) {
240+
throw new OCSBadRequestException('Invalid question data: unknown properties: ' . implode(', ', $invalidQuestionKeys));
241+
}
242+
243+
// Validate question type
244+
$type = $oldQuestion['type'] ?? null;
245+
if ($type === null || array_search($type, Constants::ANSWER_TYPES) === false) {
246+
throw new OCSBadRequestException('Invalid question data: invalid type');
247+
}
248+
249+
// Block datetime questions
250+
if ($type === 'datetime') {
251+
throw new OCSBadRequestException('Invalid question data: datetime type no longer supported');
252+
}
253+
254+
// Validate extraSettings
255+
if (!empty($oldQuestion['extraSettings'] ?? [])) {
256+
if (!$this->formsService->areExtraSettingsValid($oldQuestion['extraSettings'], $type)) {
257+
throw new OCSBadRequestException('Invalid question data: invalid extraSettings');
258+
}
259+
}
260+
261+
$questionData = $oldQuestion;
262+
$oldQuestionId = $oldQuestion['id'];
263+
$options = $oldQuestion['options'] ?? [];
264+
265+
unset($questionData['id']);
266+
unset($questionData['options']);
267+
unset($questionData['accept']);
268+
269+
$questionData['formId'] = $form->getId();
270+
$newQuestion = Question::fromParams($questionData);
271+
$this->questionMapper->insert($newQuestion);
272+
273+
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestionId) {
274+
$form->setConfirmationEmailQuestionId($newQuestion->getId());
275+
}
276+
277+
foreach ($options as $oldOption) {
278+
$optionData = $oldOption;
279+
280+
// Validate option property whitelist
281+
$allowedOptionProperties = ['text', 'order', 'optionType'];
282+
$invalidOptionKeys = array_diff(array_keys($optionData), $allowedOptionProperties);
283+
if (!empty($invalidOptionKeys)) {
284+
throw new OCSBadRequestException('Invalid option data: unknown properties: ' . implode(', ', $invalidOptionKeys));
285+
}
286+
287+
unset($optionData['id']);
288+
$optionData['questionId'] = $newQuestion->getId();
289+
$newOption = Option::fromParams($optionData);
290+
$this->optionMapper->insert($newOption);
291+
}
292+
}
293+
$this->formMapper->update($form);
294+
189295
} else {
190296
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
191297

192298
// Read old form, (un)set new form specific data, extend title
193299
$formData = $oldForm->read();
194-
unset($formData['id']);
195-
unset($formData['created']);
196-
unset($formData['lastUpdated']);
197-
unset($formData['state']);
198-
unset($formData['fileId']);
199-
unset($formData['fileFormat']);
200-
unset($formData['lockedBy']);
201-
unset($formData['lockedUntil']);
202-
unset($formData['confirmationEmailQuestionId']);
203-
$formData['ownerId'] = $this->currentUser->getUID();
204-
$formData['hash'] = $this->formsService->generateFormHash();
300+
$formData = $this->setDefaults($formData);
205301
// TRANSLATORS Appendix to the form Title of a duplicated/copied form.
206302
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
207-
$formData['access'] = [
208-
'permitAllUsers' => false,
209-
'showToAllUsers' => false,
210-
];
211-
$formData['submitMultiple'] = false;
212-
$formData['allowEditSubmissions'] = false;
213-
$formData['showExpiration'] = false;
214-
$formData['expires'] = 0;
215-
$formData['isAnonymous'] = false;
216303

217304
$form = Form::fromParams($formData);
218305
$this->formMapper->insert($form);
@@ -251,6 +338,32 @@ public function newForm(?int $fromId = null): DataResponse {
251338
return new DataResponse($this->formsService->getForm($form), Http::STATUS_CREATED);
252339
}
253340

341+
private function setDefaults(array $formData) {
342+
// Remove unused data
343+
unset($formData['id']);
344+
unset($formData['created']);
345+
unset($formData['lastUpdated']);
346+
unset($formData['state']);
347+
unset($formData['fileId']);
348+
unset($formData['fileFormat']);
349+
unset($formData['lockedBy']);
350+
unset($formData['lockedUntil']);
351+
unset($formData['confirmationEmailQuestionId']);
352+
$formData['ownerId'] = $this->currentUser->getUID();
353+
$formData['hash'] = $this->formsService->generateFormHash();
354+
355+
$formData['access'] = [
356+
'permitAllUsers' => false,
357+
'showToAllUsers' => false,
358+
];
359+
$formData['submitMultiple'] = false;
360+
$formData['allowEditSubmissions'] = false;
361+
$formData['showExpiration'] = false;
362+
$formData['expires'] = 0;
363+
$formData['isAnonymous'] = false;
364+
return $formData;
365+
}
366+
254367
/**
255368
* Read all information to edit a Form (form, questions, options, except submissions/answers)
256369
*
@@ -730,8 +843,10 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
730843
throw new OCSBadRequestException('Invalid extraSettings, will not update.');
731844
}
732845

733-
if ($form->getConfirmationEmailQuestionId() === $question->getId()
734-
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)) {
846+
if (
847+
$form->getConfirmationEmailQuestionId() === $question->getId()
848+
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)
849+
) {
735850
$form->setConfirmationEmailQuestionId(null);
736851
}
737852

openapi.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,21 @@
885885
"nullable": true,
886886
"default": null,
887887
"description": "(optional) Id of the form that should be cloned"
888+
},
889+
"import": {
890+
"type": "boolean",
891+
"nullable": true,
892+
"default": false,
893+
"description": "(optional) If it should import the form from post body"
894+
},
895+
"formData": {
896+
"type": "object",
897+
"nullable": true,
898+
"default": {},
899+
"description": "(optional) The formdata to import",
900+
"additionalProperties": {
901+
"type": "object"
902+
}
888903
}
889904
}
890905
}
@@ -962,6 +977,34 @@
962977
}
963978
}
964979
},
980+
"400": {
981+
"description": "Invalid option data: unknown properties",
982+
"content": {
983+
"application/json": {
984+
"schema": {
985+
"type": "object",
986+
"required": [
987+
"ocs"
988+
],
989+
"properties": {
990+
"ocs": {
991+
"type": "object",
992+
"required": [
993+
"meta",
994+
"data"
995+
],
996+
"properties": {
997+
"meta": {
998+
"$ref": "#/components/schemas/OCSMeta"
999+
},
1000+
"data": {}
1001+
}
1002+
}
1003+
}
1004+
}
1005+
}
1006+
}
1007+
},
9651008
"401": {
9661009
"description": "Current user is not logged in",
9671010
"content": {

0 commit comments

Comments
 (0)