Skip to content

Commit 06a1c8a

Browse files
committed
feat: add import function (#1425)
Signed-off-by: TimedIn <git@timedin.net>
1 parent 8b6d9a9 commit 06a1c8a

9 files changed

Lines changed: 685 additions & 184 deletions

File tree

lib/Controller/ApiController.php

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use OCP\AppFramework\OCS\OCSForbiddenException;
4545
use OCP\AppFramework\OCS\OCSNotFoundException;
4646
use OCP\AppFramework\OCSController;
47+
use OCP\App\IAppManager;
4748
use OCP\BackgroundJob\IJobList;
4849
use OCP\Files\Folder;
4950
use OCP\Files\IMimeTypeDetector;
@@ -86,6 +87,7 @@ public function __construct(
8687
private readonly SubmissionService $submissionService,
8788
private readonly IL10N $l10n,
8889
private readonly LoggerInterface $logger,
90+
private readonly IAppManager $appManager,
8991
private readonly IUserManager $userManager,
9092
private readonly IRootFolder $rootFolder,
9193
private readonly UploadedFileMapper $uploadedFileMapper,
@@ -148,23 +150,39 @@ public function getForms(string $type = 'owned'): DataResponse {
148150
* Return a copy of the form if the parameter $fromId is set
149151
*
150152
* @param ?int $fromId (optional) Id of the form that should be cloned
153+
* @param ?bool $import (optional) If it should import the form from post body
154+
* @param ?array<string, mixed> $formData (optional) The formdata to import
151155
* @return DataResponse<Http::STATUS_CREATED, FormsForm, array{}>
152156
* @throws OCSForbiddenException The user is not allowed to create forms
157+
* @throws OCSBadRequestException Cannot use both fromId and import parameters
158+
* @throws OCSBadRequestException Invalid form data: missing questions
159+
* @throws OCSBadRequestException Invalid form data: unknown properties
160+
* @throws OCSBadRequestException Invalid question data: missing id
161+
* @throws OCSBadRequestException Invalid question data: unknown properties
162+
* @throws OCSBadRequestException Invalid question data: invalid type
163+
* @throws OCSBadRequestException Invalid question data: datetime type no longer supported
164+
* @throws OCSBadRequestException Invalid question data: invalid extraSettings
165+
* @throws OCSBadRequestException Invalid option data: unknown properties
153166
*
154167
* 201: the created form
155168
*/
156169
#[CORS()]
157170
#[NoAdminRequired()]
158171
#[BruteForceProtection(action: 'form')]
159172
#[ApiRoute(verb: 'POST', url: '/api/v3/forms')]
160-
public function newForm(?int $fromId = null): DataResponse {
173+
public function newForm(?int $fromId = null, ?bool $import = false, ?array $formData = []): DataResponse {
161174
// Check if user is allowed
162175
if (!$this->configService->canCreateForms()) {
163176
$this->logger->debug('This user is not allowed to create Forms.');
164177
throw new OCSForbiddenException('This user is not allowed to create Forms.');
165178
}
166179

167-
if ($fromId === null) {
180+
// Validate mutually exclusive parameters
181+
if ($fromId !== null && $import === true) {
182+
throw new OCSBadRequestException('Cannot use both fromId and import parameters');
183+
}
184+
185+
if ($fromId === null && $import === false) {
168186
// Create Form
169187
$form = new Form();
170188
$form->setOwnerId($this->currentUser->getUID());
@@ -183,10 +201,37 @@ public function newForm(?int $fromId = null): DataResponse {
183201

184202
$this->formMapper->insert($form);
185203
} else {
186-
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
204+
// Fill variables from json or database
205+
if ($import) {
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+
} else {
226+
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
187227

188-
// Read old form, (un)set new form specific data, extend title
189-
$formData = $oldForm->read();
228+
// Read old form, (un)set new form specific data, extend title
229+
$formData = $oldForm->read();
230+
// Get Questions, set new formId, reinsert
231+
$questions = $this->questionMapper->findByForm($oldForm->getId());
232+
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
233+
}
234+
// Remove unused data
190235
unset($formData['id']);
191236
unset($formData['created']);
192237
unset($formData['lastUpdated']);
@@ -199,7 +244,9 @@ public function newForm(?int $fromId = null): DataResponse {
199244
$formData['ownerId'] = $this->currentUser->getUID();
200245
$formData['hash'] = $this->formsService->generateFormHash();
201246
// TRANSLATORS Appendix to the form Title of a duplicated/copied form.
202-
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
247+
if (!$import) {
248+
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
249+
}
203250
$formData['access'] = [
204251
'permitAllUsers' => false,
205252
'showToAllUsers' => false,
@@ -213,26 +260,70 @@ public function newForm(?int $fromId = null): DataResponse {
213260
$form = Form::fromParams($formData);
214261
$this->formMapper->insert($form);
215262

216-
// Get Questions, set new formId, reinsert
217-
$questions = $this->questionMapper->findByForm($oldForm->getId());
218-
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
219-
220263
foreach ($questions as $oldQuestion) {
221-
$questionData = $oldQuestion->read();
264+
if ($import) {
265+
if (!isset($oldQuestion['id'])) {
266+
throw new OCSBadRequestException('Invalid question data: missing id');
267+
}
268+
269+
// Validate question property whitelist
270+
$allowedQuestionProperties = ['id', 'order', 'type', 'isRequired', 'text', 'name', 'description', 'extraSettings', 'options'];
271+
$invalidQuestionKeys = array_diff(array_keys($oldQuestion), $allowedQuestionProperties);
272+
if (!empty($invalidQuestionKeys)) {
273+
throw new OCSBadRequestException('Invalid question data: unknown properties: ' . implode(', ', $invalidQuestionKeys));
274+
}
275+
276+
// Validate question type
277+
$type = $oldQuestion['type'] ?? null;
278+
if ($type === null || array_search($type, Constants::ANSWER_TYPES) === false) {
279+
throw new OCSBadRequestException('Invalid question data: invalid type');
280+
}
281+
282+
// Block datetime questions
283+
if ($type === 'datetime') {
284+
throw new OCSBadRequestException('Invalid question data: datetime type no longer supported');
285+
}
286+
287+
// Validate extraSettings
288+
if (!empty($oldQuestion['extraSettings'] ?? [])) {
289+
if (!$this->formsService->areExtraSettingsValid($oldQuestion['extraSettings'], $type)) {
290+
throw new OCSBadRequestException('Invalid question data: invalid extraSettings');
291+
}
292+
}
293+
294+
$questionData = $oldQuestion;
295+
$oldQuestionId = $oldQuestion['id'];
296+
$options = $oldQuestion['options'] ?? [];
297+
} else {
298+
$questionData = $oldQuestion->read();
299+
$oldQuestionId = $oldQuestion->getId();
300+
// Get Options, set new QuestionId, reinsert
301+
$options = $this->optionMapper->findByQuestion($oldQuestionId);
302+
}
222303

223304
unset($questionData['id']);
305+
unset($questionData['options']);
306+
unset($questionData['accept']);
307+
224308
$questionData['formId'] = $form->getId();
225309
$newQuestion = Question::fromParams($questionData);
226310
$this->questionMapper->insert($newQuestion);
227311

228-
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestion->getId()) {
312+
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestionId) {
229313
$form->setConfirmationEmailQuestionId($newQuestion->getId());
230314
}
231315

232-
// Get Options, set new QuestionId, reinsert
233-
$options = $this->optionMapper->findByQuestion($oldQuestion->getId());
234316
foreach ($options as $oldOption) {
235-
$optionData = $oldOption->read();
317+
$optionData = $import ? $oldOption : $oldOption->read();
318+
319+
if ($import) {
320+
// Validate option property whitelist
321+
$allowedOptionProperties = ['text', 'order', 'optionType'];
322+
$invalidOptionKeys = array_diff(array_keys($optionData), $allowedOptionProperties);
323+
if (!empty($invalidOptionKeys)) {
324+
throw new OCSBadRequestException('Invalid option data: unknown properties: ' . implode(', ', $invalidOptionKeys));
325+
}
326+
}
236327

237328
unset($optionData['id']);
238329
$optionData['questionId'] = $newQuestion->getId();
@@ -251,7 +342,8 @@ public function newForm(?int $fromId = null): DataResponse {
251342
* Read all information to edit a Form (form, questions, options, except submissions/answers)
252343
*
253344
* @param int $formId Id of the form
254-
* @return DataResponse<Http::STATUS_OK, FormsForm, array{}>
345+
* @param ?bool $download if the form should be downloaded
346+
* @return DataResponse<Http::STATUS_OK, FormsForm, array{}>|DataDownloadResponse<Http::STATUS_OK, 'application/json', array{}>
255347
* @throws OCSBadRequestException Could not find form
256348
* @throws OCSForbiddenException User has no permissions to get this form
257349
*
@@ -261,9 +353,38 @@ public function newForm(?int $fromId = null): DataResponse {
261353
#[NoAdminRequired()]
262354
#[BruteForceProtection(action: 'form')]
263355
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}')]
264-
public function getForm(int $formId): DataResponse {
356+
public function getForm(int $formId, ?bool $download): DataResponse|DataDownloadResponse {
265357
$form = $this->formsService->getFormIfAllowed($formId, Constants::PERMISSION_SUBMIT);
266358

359+
if ($download) {
360+
$formData = $this->formsService->getPublicForm($form);
361+
unset($formData['hash']);
362+
unset($formData['created']);
363+
unset($formData['lastUpdated']);
364+
unset($formData['lockedBy']);
365+
unset($formData['lockedUntil']);
366+
unset($formData['permissions']);
367+
unset($formData['canSubmit']);
368+
unset($formData['isMaxSubmissionsReached']);
369+
unset($formData['submissionCount']);
370+
unset($formData['filePath']);
371+
unset($formData['state']);
372+
unset($formData['id']);
373+
374+
foreach($formData['questions'] as &$question) {
375+
unset($question['formId']);
376+
unset($question['accept']);
377+
foreach($question['options'] as &$option) {
378+
unset($option['questionId']);
379+
unset($option['id']);
380+
}
381+
}
382+
383+
$downloadFile = ["form"=>$formData,"appVersion"=>$this->appManager->getAppVersion('forms')];
384+
return new DataDownloadResponse(
385+
json_encode($downloadFile), $form->getTitle(), 'application/json');
386+
}
387+
267388
return new DataResponse($this->formsService->getForm($form));
268389
}
269390

@@ -694,8 +815,10 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
694815
throw new OCSBadRequestException('Invalid extraSettings, will not update.');
695816
}
696817

697-
if ($form->getConfirmationEmailQuestionId() === $question->getId()
698-
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)) {
818+
if (
819+
$form->getConfirmationEmailQuestionId() === $question->getId()
820+
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)
821+
) {
699822
$form->setConfirmationEmailQuestionId(null);
700823
}
701824

openapi.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,21 @@
873873
"nullable": true,
874874
"default": null,
875875
"description": "(optional) Id of the form that should be cloned"
876+
},
877+
"import": {
878+
"type": "boolean",
879+
"nullable": true,
880+
"default": false,
881+
"description": "(optional) If it should import the form from post body"
882+
},
883+
"formData": {
884+
"type": "object",
885+
"nullable": true,
886+
"default": {},
887+
"description": "(optional) The formdata to import",
888+
"additionalProperties": {
889+
"type": "object"
890+
}
876891
}
877892
}
878893
}

0 commit comments

Comments
 (0)