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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
98 changes: 0 additions & 98 deletions src/Backend/Core/Engine/Csv.php

This file was deleted.

3 changes: 2 additions & 1 deletion src/Backend/Modules/Profiles/Actions/ExportTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
44 changes: 15 additions & 29 deletions src/Backend/Modules/Profiles/Actions/Import.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment has a grammatical error. "display to import" should be "display a form to import" for proper English grammar.

Suggested change
* This is the import-action, it will display to import a CSV file with profiles to create.
* This is the import-action, it will display a form to import a CSV file with profiles to create.

Copilot uses AI. Check for mistakes.
*/
class Import extends BackendBaseActionAdd
{
Expand Down Expand Up @@ -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'));
}
Expand All @@ -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,
Expand All @@ -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;
}
}
91 changes: 0 additions & 91 deletions src/Backend/Modules/Profiles/Engine/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
63 changes: 60 additions & 3 deletions src/ForkCMS/Utility/Csv/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +13 to +25
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $options array is used without being initialized. If neither line ending condition is met (lines 18-23), the method would return an undefined variable. Initialize $options as an empty array at the beginning of the method: $options = [];

Copilot uses AI. Check for mistakes.
}

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);
Expand All @@ -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(
Expand Down
Loading
Loading