From 63fb22fcf89f648416a6db97ba7d29f946b2f61b Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 09:28:41 +0100 Subject: [PATCH 1/6] chore: removed deprecated CSV class --- src/Backend/Core/Engine/Csv.php | 98 --------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 src/Backend/Core/Engine/Csv.php diff --git a/src/Backend/Core/Engine/Csv.php b/src/Backend/Core/Engine/Csv.php deleted file mode 100644 index 4a54319a49..0000000000 --- a/src/Backend/Core/Engine/Csv.php +++ /dev/null @@ -1,98 +0,0 @@ -output()')] - public static function outputCSV( - string $filename, - array $array, - ?array $columns = null, - ?array $excludeColumns = null - ) { - $headers = $columns; - $data = $array; - - // remove data that should be excluded - if (!empty($excludeColumns)) { - $headers = array_filter( - $columns, - fn($column) => !in_array($column, $excludeColumns) - ); - - foreach ($array as $rowNumber => $row) { - $data[$rowNumber] = array_filter( - $row, - fn($key) => !in_array($key, $excludeColumns), - ARRAY_FILTER_USE_KEY - ); - } - } - - $spreadSheet = new Spreadsheet(); - $sheet = $spreadSheet->getActiveSheet(); - - // add data - $sheet->fromArray($headers, null, 'A1'); - $sheet->fromArray($data, null, 'A2'); - - $writer = new CsvWriter($spreadSheet); - $writer->setDelimiter(Authentication::getUser()->getSetting('csv_split_character')); - $writer->setEnclosure('"'); - $writer->setLineEnding(self::getLineEnding()); - - $response = new StreamedResponse( - function () use ($writer): void { - $writer->save('php://output'); - } - ); - - // set headers - $charset = BackendModel::getContainer()->getParameter('kernel.charset'); - $response->headers->set('Content-type', 'application/csv; charset=' . $charset); - $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); - $response->headers->set('Cache-Control', 'max-age=0'); - $response->headers->set('Pragma', 'no-cache'); - - throw new RedirectException( - 'Return the csv data', - $response - ); - } - - #[\Deprecated(message: 'remove this in Fork 6, you should not rely on this.')] - private static function getLineEnding(): string - { - $lineEnding = Authentication::getUser()->getSetting('csv_line_ending'); - - // reformat - if ($lineEnding === '\n') { - return "\n"; - } - if ($lineEnding === '\r\n') { - return "\r\n"; - } - - return $lineEnding; - } -} From fd284cb0367dfa2b03bae3fa30fc69c61f556849 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 12:43:52 +0100 Subject: [PATCH 2/6] chore: Cleanup CSV writer code --- src/ForkCMS/Utility/Csv/Writer.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/ForkCMS/Utility/Csv/Writer.php b/src/ForkCMS/Utility/Csv/Writer.php index 85a87ff626..cd68bb5017 100644 --- a/src/ForkCMS/Utility/Csv/Writer.php +++ b/src/ForkCMS/Utility/Csv/Writer.php @@ -2,17 +2,14 @@ namespace ForkCMS\Utility\Csv; -use Backend\Core\Engine\Authentication; use Backend\Core\Engine\User; -use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Csv; use Symfony\Component\HttpFoundation\StreamedResponse; -use ZipStream\Stream; -class Writer +readonly class Writer { - public function __construct(private readonly string $charset) + public function __construct(private string $charset) { } @@ -43,13 +40,13 @@ private function getUserOptions(User $user): array private function getWriter(Spreadsheet $spreadsheet, array $options = []): Csv { - $writer = IOFactory::createWriter($spreadsheet, 'Csv'); + $writer = new Csv($spreadsheet); if (!empty($options)) { foreach ($options as $option => $value) { $methodName = 'set' . $option; if (method_exists($writer, $methodName)) { - call_user_func([$writer, $methodName], $value); + $writer->$methodName($value); } } } @@ -86,11 +83,6 @@ public function getResponse(Spreadsheet $spreadsheet, string $filename, array $o public function getResponseForUser(Spreadsheet $spreadsheet, string $filename, User $user): StreamedResponse { - $options = array_merge($this->getDefaultOptions(), $this->getUserOptions($user)); - - return $this->getStreamedResponse( - $this->getWriter($spreadsheet, $options), - $filename - ); + return $this->getResponse($spreadsheet, $filename, $this->getUserOptions($user)); } } From 1c9e8b82e94b0fd21605c43254291e82cb110b00 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 12:47:19 +0100 Subject: [PATCH 3/6] chore: improve phpdoc --- src/Backend/Modules/Profiles/Actions/ExportTemplate.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Backend/Modules/Profiles/Actions/ExportTemplate.php b/src/Backend/Modules/Profiles/Actions/ExportTemplate.php index 888e0f69db..7779f275ae 100644 --- a/src/Backend/Modules/Profiles/Actions/ExportTemplate.php +++ b/src/Backend/Modules/Profiles/Actions/ExportTemplate.php @@ -9,7 +9,8 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; /** - * This is the add-action, it will display a form to add a new profile. + * This is the export template-action + * it will download a template that can be used for import. */ class ExportTemplate extends BackendBaseActionAdd { From e7e6e49225719e0871dd090bdfcae98dd088ca22 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 12:51:42 +0100 Subject: [PATCH 4/6] chore: remove deprecated/unused methdo --- src/Backend/Modules/Profiles/Engine/Model.php | 91 ------------------- 1 file changed, 91 deletions(-) diff --git a/src/Backend/Modules/Profiles/Engine/Model.php b/src/Backend/Modules/Profiles/Engine/Model.php index 91c205ab23..4ec1aff8f3 100644 --- a/src/Backend/Modules/Profiles/Engine/Model.php +++ b/src/Backend/Modules/Profiles/Engine/Model.php @@ -542,97 +542,6 @@ public static function getUser(int $id): string return $html; } - /** - * Import CSV data - * - * - * @param array $data The array from the .csv file - * @param int|null $groupId $groupId Adding these profiles to a group - * @param bool $overwriteExisting $overwriteExisting - * - * @throws BackendException - * - * @return array array('count' => array('exists' => 0, 'inserted' => 0)); - * @internal param $bool [optional] $overwriteExisting If set to true, this will overwrite existing profiles - */ - #[\Deprecated(message: 'remove this in Fork 6, use Backend\Modules\Profiles\Engine::importFromArray')] - public static function importCsv(array $data, ?int $groupId = null, bool $overwriteExisting = false): array - { - // init statistics - $statistics = ['count' => ['exists' => 0, 'inserted' => 0]]; - - // loop data - foreach ($data as $item) { - // field checking - if (!isset($item['email']) || !isset($item['display_name']) || !isset($item['password'])) { - throw new BackendException( - 'The .csv file should have the following columns; "email", "password" and "display_name".' - ); - } - - // define exists - $exists = self::existsByEmail($item['email']); - - // do not overwrite existing profiles - if ($exists && !$overwriteExisting) { - // adding to exists - $statistics['count']['exists'] += 1; - - // skip this item - continue; - } - - // build item - $values = [ - 'email' => $item['email'], - 'registered_on' => BackendModel::getUTCDate(), - 'display_name' => $item['display_name'], - 'url' => self::getUrl($item['display_name']), - ]; - - // does not exist - if (!$exists) { - // import - $id = self::insert($values); - - // update counter - $statistics['count']['inserted'] += 1; - } else { - // already exists get profile - $profile = self::getByEmail($item['email']); - $id = $profile['id']; - - // exists - $statistics['count']['exists'] += 1; - } - - // new password filled in? - if ($item['password']) { - // build password - $values['password'] = self::encryptPassword($item['password']); - } - - // update values - self::update($id, $values); - - // we have a group id - if ($groupId !== null) { - // init values - $values = []; - - // build item - $values['profile_id'] = $id; - $values['group_id'] = $groupId; - $values['starts_on'] = BackendModel::getUTCDate(); - - // insert values - self::insertProfileGroup($values); - } - } - - return $statistics; - } - /** * Import multiple profiles based on a given array * Each row in the array should contain the fields: From 2209190f43fc3fb94acf0bb096bc486997e45477 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 13:28:42 +0100 Subject: [PATCH 5/6] feat: use ForkCMS\Utility\Csv\Reader --- .../Modules/Profiles/Actions/Import.php | 44 +++++-------- src/ForkCMS/Utility/Csv/Reader.php | 63 ++++++++++++++++++- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/Backend/Modules/Profiles/Actions/Import.php b/src/Backend/Modules/Profiles/Actions/Import.php index 3f0719e7b6..703a284476 100644 --- a/src/Backend/Modules/Profiles/Actions/Import.php +++ b/src/Backend/Modules/Profiles/Actions/Import.php @@ -2,17 +2,16 @@ namespace Backend\Modules\Profiles\Actions; +use Backend\Core\Engine\Authentication; use Backend\Core\Engine\Base\ActionAdd as BackendBaseActionAdd; use Backend\Core\Engine\Form as BackendForm; use Backend\Core\Language\Language as BL; use Backend\Core\Engine\Model as BackendModel; use Backend\Modules\Profiles\Engine\Model as BackendProfilesModel; use ForkCMS\Utility\Csv\Reader; -use ForkCMS\Utility\PhpSpreadsheet\Reader\Filter\ColumnsFilter; -use PhpOffice\PhpSpreadsheet\IOFactory; /** - * This is the add-action, it will display a form to add a new profile. + * This is the import-action, it will display to import a CSV file with profiles to create. */ class Import extends BackendBaseActionAdd { @@ -59,8 +58,15 @@ private function validateForm(): void ) { $indexes = $this->get(Reader::class)->findColumnIndexes( $fileFile->getTempFileName(), - ['email', 'display_name', 'password'] + [ + 'email', + 'display_name', + 'password' + ], + Authentication::getUser() ); + + // check if all required columns are present if (in_array(null, $indexes, true)) { $fileFile->addError(BL::getError('InvalidCSV')); } @@ -73,7 +79,11 @@ private function validateForm(): void // import the profiles $overwrite = $this->form->getField('overwrite_existing')->isChecked(); - $csvData = $this->convertFileToArray($fileFile->getTempFileName(), array_flip($indexes)); + $csvData = $this->get(Reader::class)->convertFileToArray( + $fileFile->getTempFileName(), + array_flip($indexes), + Authentication::getUser() + ); $statistics = BackendProfilesModel::importFromArray( $csvData, @@ -92,28 +102,4 @@ private function validateForm(): void // everything is saved, so redirect to the overview $this->redirect($redirectUrl); } - - private function convertFileToArray(string $path, array $mapping): array - { - $dataToImport = []; - - $reader = IOFactory::createReader('Csv'); - $reader->setReadDataOnly(true); - $reader->setReadFilter(new ColumnsFilter(array_keys($mapping))); - $spreadSheet = $reader->load($path); - - foreach ($spreadSheet->getActiveSheet()->getRowIterator() as $row) { - // skip the first row as it contains the headers - if ($row->getRowIndex() === 1) { - continue; - } - - $dataToImport[] = $this->get(Reader::class)->convertRowIntoMappedArray( - $row, - $mapping - ); - } - - return $dataToImport; - } } diff --git a/src/ForkCMS/Utility/Csv/Reader.php b/src/ForkCMS/Utility/Csv/Reader.php index bbea2b6ab6..27c5be3c30 100644 --- a/src/ForkCMS/Utility/Csv/Reader.php +++ b/src/ForkCMS/Utility/Csv/Reader.php @@ -2,15 +2,48 @@ namespace ForkCMS\Utility\Csv; +use Backend\Core\Engine\User; use ForkCMS\Utility\PhpSpreadsheet\Reader\Filter\ChunkReadFilter; -use PhpOffice\PhpSpreadsheet\IOFactory; +use ForkCMS\Utility\PhpSpreadsheet\Reader\Filter\ColumnsFilter; +use PhpOffice\PhpSpreadsheet\Reader\Csv; use PhpOffice\PhpSpreadsheet\Worksheet\Row; class Reader { - public function findColumnIndexes(string $path, array $columns): array + private function getUserOptions(User $user): array { - $reader = IOFactory::createReader('Csv'); + $options['Delimiter'] = $user->getSetting('csv_split_character'); + + $lineEnding = $user->getSetting('csv_line_ending'); + if ($lineEnding === '\n') { + $options['LineEnding'] = "\n"; + } + if ($lineEnding === '\r\n') { + $options['LineEnding'] = "\r\n"; + } + + return $options; + } + + private function getReader(array $options): Csv + { + $reader = new Csv(); + + if (!empty($options)) { + foreach ($options as $option => $value) { + $methodName = 'set' . $option; + if (method_exists($reader, $methodName)) { + $reader->$methodName($value); + } + } + } + + return $reader; + } + + public function findColumnIndexes(string $path, array $columns, User $user): array + { + $reader = $this->getReader($this->getUserOptions($user)); $reader->setReadDataOnly(true); $reader->setReadFilter(new ChunkReadFilter(1, 1)); $spreadSheet = $reader->load($path); @@ -28,6 +61,30 @@ public function findColumnIndexes(string $path, array $columns): array return $indexes; } + public function convertFileToArray(string $path, array $mapping, User $user): array + { + $data = []; + + $reader = $this->getReader($this->getUserOptions($user)); + $reader->setReadDataOnly(true); + $reader->setReadFilter(new ColumnsFilter(array_keys($mapping))); + $spreadSheet = $reader->load($path); + + foreach ($spreadSheet->getActiveSheet()->getRowIterator() as $row) { + // skip the first row as it contains the headers + if ($row->getRowIndex() === 1) { + continue; + } + + $data[] = $this->convertRowIntoMappedArray( + $row, + $mapping + ); + } + + return $data; + } + public function convertRowIntoMappedArray(Row $row, array $mapping): array { $data = array_fill_keys( From 8af002694e864984b1c1a7dd0be3884b33d98ee1 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Wed, 25 Feb 2026 13:29:09 +0100 Subject: [PATCH 6/6] chore: update phpoffice/phpspreadsheet --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b96b0395fd..b77b0b9bf4 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "symfony/translation": "^5.4", "symfony/intl": "^5.4", "tijsverkoyen/css-to-inline-styles": "^2.0", - "phpoffice/phpspreadsheet": "^1.12", + "phpoffice/phpspreadsheet": "^5.4", "guzzlehttp/guzzle": "^7.10", "doctrine/annotations": "^1.14", "sentry/sentry-symfony": "^5.8",