diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index 278fb2828..507140995 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -585,8 +585,28 @@ private function escapeCSV(string $value): string { return $value; } + /** + * Whether the submit view shows the question to respondents: it needs a title, choice, dropdown and ranking questions need options, and grid questions need rows and columns + * + * @param array $question the question with its options + */ + private static function isShownToRespondents(array $question): bool { + if (preg_match('/^[\s\p{Z}\x{FEFF}]*$/u', (string)($question['text'] ?? '')) === 1) { + return false; + } + + $optionTypes = array_map(static fn (mixed $option): mixed => is_array($option) ? ($option['optionType'] ?? null) : null, $question['options'] ?? []); + return match ($question['type'] ?? null) { + Constants::ANSWER_TYPE_MULTIPLE, Constants::ANSWER_TYPE_MULTIPLEUNIQUE, Constants::ANSWER_TYPE_DROPDOWN, Constants::ANSWER_TYPE_RANKING => $optionTypes !== [], + Constants::ANSWER_TYPE_GRID => in_array(Option::OPTION_TYPE_ROW, $optionTypes, true) && in_array(Option::OPTION_TYPE_COLUMN, $optionTypes, true), + default => true, + }; + } + /** * Validate all answers against the questions + * Required questions the submit view does not show to respondents need no answer + * * @param array $questions Array of the questions of the form * @param array $answers Array of the submitted answers * @param string $formOwnerId Owner of the form @@ -601,6 +621,7 @@ public function validateSubmission(array $questions, array $answers, string $for // Check if all required questions have an answer if ($question['isRequired'] + && self::isShownToRespondents($question) && (!$questionAnswered || !array_filter($answers[$questionId], static function (string|array $value): bool { if (is_array($value)) { @@ -674,7 +695,7 @@ public function validateSubmission(array $questions, array $answers, string $for if ($question['type'] === Constants::ANSWER_TYPE_LINEARSCALE) { $optionsLowest = $question['extraSettings']['optionsLowest'] ?? 1; $optionsHighest = $question['extraSettings']['optionsHighest'] ?? 5; - if (!ctype_digit((string)$answer) || intval($answer) < $optionsLowest || intval($answer) > $optionsHighest) { + if (!is_scalar($answer) || is_bool($answer) || !ctype_digit((string)$answer) || intval($answer) < $optionsLowest || intval($answer) > $optionsHighest) { throw new \InvalidArgumentException(sprintf('The answer for question "%s" must be an integer between %d and %d.', $question['text'], $optionsLowest, $optionsHighest)); } } diff --git a/tests/Unit/Service/SubmissionServiceTest.php b/tests/Unit/Service/SubmissionServiceTest.php index e3d19a7e4..1abe556ba 100644 --- a/tests/Unit/Service/SubmissionServiceTest.php +++ b/tests/Unit/Service/SubmissionServiceTest.php @@ -813,6 +813,46 @@ private function setUpCsvTest(array $questions, array $submissions, string $csvT // Data for validation of Submissions public static function dataValidateSubmission() { return [ + 'required-without-title-is-not-shown' => [ + [ + ['id' => 1, 'type' => 'short', 'text' => ' ', 'isRequired' => true] + ], + [], + null, + ], + 'required-with-unicode-space-title-is-not-shown' => [ + [ + ['id' => 1, 'type' => 'short', 'text' => "\u{00A0}\u{3000}\u{FEFF}", 'isRequired' => true] + ], + [], + null, + ], + 'required-choice-without-options-is-not-shown' => [ + [ + ['id' => 1, 'type' => 'multiple_unique', 'text' => 'q1', 'isRequired' => true, 'options' => []] + ], + [], + null, + ], + 'required-grid-without-columns-is-not-shown' => [ + [ + ['id' => 1, 'type' => 'grid', 'text' => 'q1', 'isRequired' => true, 'options' => [ + ['id' => 3, 'optionType' => 'row'] + ]] + ], + [], + null, + ], + 'required-grid-with-rows-and-columns' => [ + [ + ['id' => 1, 'type' => 'grid', 'text' => 'q1', 'isRequired' => true, 'options' => [ + ['id' => 3, 'optionType' => 'row'], + ['id' => 4, 'optionType' => 'column'] + ]] + ], + [], + 'Question "q1" is required.', + ], 'required-not-answered' => [ // Questions [ @@ -1185,6 +1225,24 @@ public static function dataValidateSubmission() { // Expected Result 'The answer for question "q1" must be an integer between 0 and 5.', ], + 'boolean-linearscale-answer' => [ + [ + ['id' => 1, 'type' => 'linearscale', 'text' => 'q1', 'isRequired' => false, 'extraSettings' => ['optionsLowest' => 0]] + ], + [ + '1' => [true] + ], + 'The answer for question "q1" must be an integer between 0 and 5.', + ], + 'list-linearscale-answer' => [ + [ + ['id' => 1, 'type' => 'linearscale', 'text' => 'q1', 'isRequired' => false, 'extraSettings' => ['optionsLowest' => 0]] + ], + [ + '1' => [['3']] + ], + 'The answer for question "q1" must be an integer between 0 and 5.', + ], 'invalid-color-question' => [ // Questions [