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
203 changes: 177 additions & 26 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 @@ public function __construct(
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 @@ public function __construct(
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 @@ public function getForms(string $type = 'owned'): DataResponse {
* 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');
}

if ($fromId === null && $import !== true) {
// Create Form
$form = new Form();
$form->setOwnerId($this->currentUser->getUID());
Expand All @@ -182,33 +202,104 @@ public function newForm(?int $fromId = null): DataResponse {
$form->setIsAnonymous(false);

$this->formMapper->insert($form);
} elseif ($import === true) {
if (!isset($formData['questions']) || !\is_array($formData['questions'])) {
throw new OCSBadRequestException('Invalid form data: missing questions');
}
$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));
}

$formData = $this->setDefaults($formData);

$form = Form::fromParams($formData);
$this->formMapper->insert($form);

foreach ($questions as $oldQuestion) {
if (!isset($oldQuestion['id'])) {
throw new OCSBadRequestException('Invalid question data: missing id');
}

// 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'] ?? [];

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 === $oldQuestionId) {
$form->setConfirmationEmailQuestionId($newQuestion->getId());
}

foreach ($options as $oldOption) {
$optionData = $oldOption;

// 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();
$newOption = Option::fromParams($optionData);
$this->optionMapper->insert($newOption);
}
}
$this->formMapper->update($form);

} else {
$oldForm = $this->formsService->getFormIfAllowed($fromId, Constants::PERMISSION_EDIT);

// Read old form, (un)set new form specific data, extend title
$formData = $oldForm->read();
unset($formData['id']);
unset($formData['created']);
unset($formData['lastUpdated']);
unset($formData['state']);
unset($formData['fileId']);
unset($formData['fileFormat']);
unset($formData['lockedBy']);
unset($formData['lockedUntil']);
unset($formData['confirmationEmailQuestionId']);
$formData['ownerId'] = $this->currentUser->getUID();
$formData['hash'] = $this->formsService->generateFormHash();
$formData = $this->setDefaults($formData);
// TRANSLATORS Appendix to the form Title of a duplicated/copied form.
$formData['title'] .= ' - ' . $this->l10n->t('Copy');
$formData['access'] = [
'permitAllUsers' => false,
'showToAllUsers' => false,
];
$formData['submitMultiple'] = false;
$formData['allowEditSubmissions'] = false;
$formData['showExpiration'] = false;
$formData['expires'] = 0;
$formData['isAnonymous'] = false;

$form = Form::fromParams($formData);
$this->formMapper->insert($form);
Expand Down Expand Up @@ -247,11 +338,38 @@ public function newForm(?int $fromId = null): DataResponse {
return new DataResponse($this->formsService->getForm($form), Http::STATUS_CREATED);
}

private function setDefaults(array $formData) {
// Remove unused data
unset($formData['id']);
unset($formData['created']);
unset($formData['lastUpdated']);
unset($formData['state']);
unset($formData['fileId']);
unset($formData['fileFormat']);
unset($formData['lockedBy']);
unset($formData['lockedUntil']);
unset($formData['confirmationEmailQuestionId']);
$formData['ownerId'] = $this->currentUser->getUID();
$formData['hash'] = $this->formsService->generateFormHash();

$formData['access'] = [
'permitAllUsers' => false,
'showToAllUsers' => false,
];
$formData['submitMultiple'] = false;
$formData['allowEditSubmissions'] = false;
$formData['showExpiration'] = false;
$formData['expires'] = 0;
$formData['isAnonymous'] = false;
return $formData;
}

/**
* 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 $export if the form should get exported
* @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 +379,40 @@ public function newForm(?int $fromId = null): DataResponse {
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}')]
public function getForm(int $formId): DataResponse {
public function getForm(int $formId, ?bool $export): DataResponse|DataDownloadResponse {
$form = $this->formsService->getFormIfAllowed($formId, Constants::PERMISSION_SUBMIT);

if ($export) {
$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']);
}
}

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

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

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

Expand Down Expand Up @@ -694,8 +843,10 @@ public function updateQuestion(int $formId, int $questionId, array $keyValuePair
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
Loading