diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index 980bc6e0d..ba6f3b14f 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -31,6 +31,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\Mail\IMailer; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Csv; diff --git a/tests/Unit/Service/SubmissionServiceTest.php b/tests/Unit/Service/SubmissionServiceTest.php index 58d8b3b40..2454fbce5 100644 --- a/tests/Unit/Service/SubmissionServiceTest.php +++ b/tests/Unit/Service/SubmissionServiceTest.php @@ -33,6 +33,8 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\Mail\IMailer; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -565,6 +567,53 @@ public function testGetSubmissionsDataThrowsExceptionOnInvalidFormat() { $this->submissionService->getSubmissionsData($form, 'invalid'); } + public static function dataSetCellValueType(): array { + return [ + // Radio (and similar choice) answers are always strings, even when the option + // labels are pure numbers. For spreadsheet formats they must be stored as numbers + // so they can be aggregated (e.g. averaged) in the spreadsheet application. + 'radio-numeric-label-xlsx' => ['xlsx', '3', DataType::TYPE_NUMERIC, 3], + 'radio-numeric-label-ods' => ['ods', '5', DataType::TYPE_NUMERIC, 5], + 'numeric-zero' => ['xlsx', '0', DataType::TYPE_NUMERIC, 0], + 'numeric-decimal' => ['xlsx', '3.5', DataType::TYPE_NUMERIC, 3.5], + // Values starting with a formula-trigger character keep the string typing so they + // are never interpreted as formulas. + 'formula' => ['xlsx', '=SUM(A1:A2)', DataType::TYPE_STRING, '=SUM(A1:A2)'], + 'negative-number' => ['xlsx', '-5', DataType::TYPE_STRING, '-5'], + 'plus-prefixed' => ['xlsx', '+5', DataType::TYPE_STRING, '+5'], + 'at-prefixed' => ['xlsx', '@foo', DataType::TYPE_STRING, '@foo'], + // Strings whose canonical numeric form would differ must stay text to avoid data + // loss (leading-zero phone numbers, scientific notation, trailing decimal zeros, …). + 'leading-zero' => ['xlsx', '0123456789', DataType::TYPE_STRING, '0123456789'], + 'scientific' => ['xlsx', '1e5', DataType::TYPE_STRING, '1e5'], + 'trailing-decimal-zero' => ['xlsx', '3.50', DataType::TYPE_STRING, '3.50'], + 'text' => ['xlsx', 'Option A', DataType::TYPE_STRING, 'Option A'], + // CSV is always escaped and kept as string, even for pure numbers. + 'csv-number' => ['csv', '3', DataType::TYPE_STRING, '3'], + 'csv-formula' => ['csv', '=danger', DataType::TYPE_STRING, "'=danger"], + ]; + } + + /** + * @dataProvider dataSetCellValueType + */ + public function testSetCellValueDataType(string $fileFormat, string $value, string $expectedType, mixed $expectedValue): void { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + TestCase::invokePrivate($this->submissionService, 'setCellValue', [$worksheet, 1, 1, $value, $fileFormat]); + + $cell = $worksheet->getCell([1, 1]); + + $this->assertSame($expectedType, $cell->getDataType()); + if ($expectedType === DataType::TYPE_NUMERIC) { + $this->assertEquals($expectedValue, $cell->getValue()); + } else { + // assertSame guards against a numeric-looking string being silently coerced. + $this->assertSame($expectedValue, $cell->getValue()); + } + } + /** * Setting up a very simple default CsvTest */