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
165 changes: 146 additions & 19 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
use OCA\Forms\Db\UploadedFile;
use OCA\Forms\Db\UploadedFileMapper;
use OCA\Forms\Exception\NoSuchFormException;
use OCA\Forms\Helper\FilePathHelper;
use OCA\Forms\ResponseDefinitions;
use OCA\Forms\Service\ConfigService;
use OCA\Forms\Service\ConfirmationEmailService;
use OCA\Forms\Service\FormsService;
use OCA\Forms\Service\SubmissionService;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http;
Expand Down Expand Up @@ -76,6 +78,7 @@
IRequest $request,
IUserSession $userSession,
private readonly AnswerMapper $answerMapper,
private readonly FilePathHelper $filePathHelper,
private readonly FormMapper $formMapper,
private readonly OptionMapper $optionMapper,
private readonly QuestionMapper $questionMapper,
Expand All @@ -86,6 +89,7 @@
private readonly SubmissionService $submissionService,
private readonly IL10N $l10n,
private readonly LoggerInterface $logger,
private readonly IAppManager $appManager,
private readonly IUserManager $userManager,
private readonly IRootFolder $rootFolder,
private readonly UploadedFileMapper $uploadedFileMapper,
Expand Down Expand Up @@ -148,23 +152,39 @@
* Return a copy of the form if the parameter $fromId is set
*
* @param ?int $fromId (optional) Id of the form that should be cloned
* @param ?bool $import (optional) If it should import the form from post body
* @param ?array<string, mixed> $formData (optional) The formdata to import
* @return DataResponse<Http::STATUS_CREATED, FormsForm, array{}>
* @throws OCSForbiddenException The user is not allowed to create forms
* @throws OCSBadRequestException Cannot use both fromId and import parameters
* @throws OCSBadRequestException Invalid form data: missing questions
* @throws OCSBadRequestException Invalid form data: unknown properties
* @throws OCSBadRequestException Invalid question data: missing id
* @throws OCSBadRequestException Invalid question data: unknown properties
* @throws OCSBadRequestException Invalid question data: invalid type
* @throws OCSBadRequestException Invalid question data: datetime type no longer supported
* @throws OCSBadRequestException Invalid question data: invalid extraSettings
* @throws OCSBadRequestException Invalid option data: unknown properties
*
* 201: the created form
*/
#[CORS()]
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'POST', url: '/api/v3/forms')]
public function newForm(?int $fromId = null): DataResponse {
public function newForm(?int $fromId = null, ?bool $import = false, ?array $formData = []): DataResponse {
// Check if user is allowed
if (!$this->configService->canCreateForms()) {
$this->logger->debug('This user is not allowed to create Forms.');
throw new OCSForbiddenException('This user is not allowed to create Forms.');
}

if ($fromId === null) {
// Validate mutually exclusive parameters
if ($fromId !== null && $import === true) {
throw new OCSBadRequestException('Cannot use both fromId and import parameters');
Comment thread
timedin-de marked this conversation as resolved.
}

if ($fromId === null && $import === false) {
// Create Form
$form = new Form();
$form->setOwnerId($this->currentUser->getUID());
Expand All @@ -183,10 +203,37 @@

$this->formMapper->insert($form);
} else {
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);
// Fill variables from json or database
if ($import) {
if (!isset($formData['questions']) || !\is_array($formData['questions'])) {
throw new OCSBadRequestException('Invalid form data: missing questions');
Comment thread
timedin-de marked this conversation as resolved.
}
$questions = $formData['questions'];
$oldConfirmationEmailQuestionId = $formData['confirmationEmailQuestionId'] ?? null;
unset($formData['questions']);

// Validate form data whitelist
$allowedFormProperties = [
'title', 'description', 'access', 'expires', 'isAnonymous',
'submitMultiple', 'allowEditSubmissions', 'showExpiration',
'submissionMessage', 'maxSubmissions', 'confirmationEmailEnabled',
'confirmationEmailSubject', 'confirmationEmailBody', 'confirmationEmailQuestionId',
'allowComments',
];
$invalidKeys = array_diff(array_keys($formData), $allowedFormProperties);
if (!empty($invalidKeys)) {
throw new OCSBadRequestException('Invalid form data: unknown properties: ' . implode(', ', $invalidKeys));
}
} else {
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);

// Read old form, (un)set new form specific data, extend title
$formData = $oldForm->read();
// Read old form, (un)set new form specific data, extend title
$formData = $oldForm->read();
// Get Questions, set new formId, reinsert
$questions = $this->questionMapper->findByForm($oldForm->getId());
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();
}
// Remove unused data
unset($formData['id']);
unset($formData['created']);
unset($formData['lastUpdated']);
Expand All @@ -199,7 +246,9 @@
$formData['ownerId'] = $this->currentUser->getUID();
$formData['hash'] = $this->formsService->generateFormHash();
// TRANSLATORS Appendix to the form Title of a duplicated/copied form.
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
if (!$import) {
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
}
$formData['access'] = [
'permitAllUsers' => false,
'showToAllUsers' => false,
Expand All @@ -213,26 +262,70 @@
$form = Form::fromParams($formData);
$this->formMapper->insert($form);

// Get Questions, set new formId, reinsert
$questions = $this->questionMapper->findByForm($oldForm->getId());
$oldConfirmationEmailQuestionId = $oldForm->getConfirmationEmailQuestionId();

foreach ($questions as $oldQuestion) {
$questionData = $oldQuestion->read();
if ($import) {
if (!isset($oldQuestion['id'])) {

Check failure on line 267 in lib/Controller/ApiController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable34

UndefinedMagicMethod

lib/Controller/ApiController.php:267:17: UndefinedMagicMethod: Magic method OCA\Forms\Db\Question::offsetget does not exist (see https://psalm.dev/219)

Check failure on line 267 in lib/Controller/ApiController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

UndefinedMagicMethod

lib/Controller/ApiController.php:267:17: UndefinedMagicMethod: Magic method OCA\Forms\Db\Question::offsetget does not exist (see https://psalm.dev/219)

Check failure on line 267 in lib/Controller/ApiController.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

UndefinedMagicMethod

lib/Controller/ApiController.php:267:17: UndefinedMagicMethod: Magic method OCA\Forms\Db\Question::offsetget does not exist (see https://psalm.dev/219)
throw new OCSBadRequestException('Invalid question data: missing id');
Comment thread
timedin-de marked this conversation as resolved.
}

// Validate question property whitelist
$allowedQuestionProperties = ['id', 'order', 'type', 'isRequired', 'text', 'name', 'description', 'extraSettings', 'options'];
$invalidQuestionKeys = array_diff(array_keys($oldQuestion), $allowedQuestionProperties);
if (!empty($invalidQuestionKeys)) {
throw new OCSBadRequestException('Invalid question data: unknown properties: ' . implode(', ', $invalidQuestionKeys));
}

// Validate question type
$type = $oldQuestion['type'] ?? null;
if ($type === null || array_search($type, Constants::ANSWER_TYPES) === false) {
throw new OCSBadRequestException('Invalid question data: invalid type');
}

// Block datetime questions
if ($type === 'datetime') {
throw new OCSBadRequestException('Invalid question data: datetime type no longer supported');
}

// Validate extraSettings
if (!empty($oldQuestion['extraSettings'] ?? [])) {
if (!$this->formsService->areExtraSettingsValid($oldQuestion['extraSettings'], $type)) {
throw new OCSBadRequestException('Invalid question data: invalid extraSettings');
}
}

$questionData = $oldQuestion;
$oldQuestionId = $oldQuestion['id'];
$options = $oldQuestion['options'] ?? [];
} else {
$questionData = $oldQuestion->read();
$oldQuestionId = $oldQuestion->getId();
// Get Options, set new QuestionId, reinsert
$options = $this->optionMapper->findByQuestion($oldQuestionId);
}

unset($questionData['id']);
unset($questionData['options']);
unset($questionData['accept']);

$questionData['formId'] = $form->getId();
$newQuestion = Question::fromParams($questionData);
$this->questionMapper->insert($newQuestion);

if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestion->getId()) {
if (isset($oldConfirmationEmailQuestionId) && $oldConfirmationEmailQuestionId === $oldQuestionId) {
$form->setConfirmationEmailQuestionId($newQuestion->getId());
}

// Get Options, set new QuestionId, reinsert
$options = $this->optionMapper->findByQuestion($oldQuestion->getId());
foreach ($options as $oldOption) {
$optionData = $oldOption->read();
$optionData = $import ? $oldOption : $oldOption->read();

if ($import) {
// Validate option property whitelist
$allowedOptionProperties = ['text', 'order', 'optionType'];
$invalidOptionKeys = array_diff(array_keys($optionData), $allowedOptionProperties);
if (!empty($invalidOptionKeys)) {
throw new OCSBadRequestException('Invalid option data: unknown properties: ' . implode(', ', $invalidOptionKeys));
}
}

unset($optionData['id']);
$optionData['questionId'] = $newQuestion->getId();
Expand All @@ -251,7 +344,8 @@
* Read all information to edit a Form (form, questions, options, except submissions/answers)
*
* @param int $formId Id of the form
* @return DataResponse<Http::STATUS_OK, FormsForm, array{}>
* @param ?bool $download if the form should be downloaded
* @return DataResponse<Http::STATUS_OK, FormsForm, array{}>|DataDownloadResponse<Http::STATUS_OK, 'application/json', array{}>
* @throws OCSBadRequestException Could not find form
* @throws OCSForbiddenException User has no permissions to get this form
*
Expand All @@ -261,9 +355,40 @@
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}')]
public function getForm(int $formId): DataResponse {
public function getForm(int $formId, ?bool $download): DataResponse|DataDownloadResponse {
$form = $this->formsService->getFormIfAllowed($formId, Constants::PERMISSION_SUBMIT);

if ($download) {
$formData = $this->formsService->getPublicForm($form);
unset($formData['hash']);
unset($formData['created']);
unset($formData['lastUpdated']);
unset($formData['lockedBy']);
unset($formData['lockedUntil']);
unset($formData['permissions']);
unset($formData['canSubmit']);
unset($formData['isMaxSubmissionsReached']);
unset($formData['submissionCount']);
unset($formData['filePath']);
unset($formData['state']);
unset($formData['id']);

foreach ($formData['questions'] as &$question) {
unset($question['formId']);
unset($question['accept']);
foreach ($question['options'] as &$option) {
unset($option['questionId']);
unset($option['id']);
}
}

$downloadFile = ['form' => $formData,'appVersion' => $this->appManager->getAppVersion('forms')];
$fileName = $this->filePathHelper->normalizeFileName($form->getTitle() . '.json');

return new DataDownloadResponse(
json_encode($downloadFile), $fileName, 'application/json');
}

return new DataResponse($this->formsService->getForm($form));
}

Expand Down Expand Up @@ -694,8 +819,10 @@
throw new OCSBadRequestException('Invalid extraSettings, will not update.');
}

if ($form->getConfirmationEmailQuestionId() === $question->getId()
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)) {
if (
$form->getConfirmationEmailQuestionId() === $question->getId()
&& !$question->isEmailType($keyValuePairs['type'] ?? null, $keyValuePairs['extraSettings'] ?? null)
) {
$form->setConfirmationEmailQuestionId(null);
}

Expand Down
88 changes: 74 additions & 14 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,21 @@
"nullable": true,
"default": null,
"description": "(optional) Id of the form that should be cloned"
},
"import": {
"type": "boolean",
"nullable": true,
"default": false,
"description": "(optional) If it should import the form from post body"
},
"formData": {
"type": "object",
"nullable": true,
"default": {},
"description": "(optional) The formdata to import",
"additionalProperties": {
"type": "object"
}
}
}
}
Expand Down Expand Up @@ -950,6 +965,34 @@
}
}
},
"400": {
"description": "Invalid option data: unknown properties",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
Expand Down Expand Up @@ -1005,6 +1048,15 @@
"format": "int64"
}
},
{
"name": "download",
"in": "query",
"description": "if the form should be downloaded",
"schema": {
"type": "boolean",
"nullable": true
}
},
{
"name": "OCS-APIRequest",
"in": "header",
Expand All @@ -1022,27 +1074,35 @@
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"anyOf": [
{
"type": "object",
"required": [
"meta",
"data"
"ocs"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Form"
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Form"
}
}
}
}
},
{
"type": "string",
"format": "binary"
}
}
]
}
}
}
Expand Down
Loading
Loading