From fe28b9749d6dfaadbe68f1aa058f5487837c5537 Mon Sep 17 00:00:00 2001 From: Daniel Huf <1814195+dhuf@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:11:10 +0200 Subject: [PATCH 01/12] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index fa7bf4d..962eb2d 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,8 @@ } }, "require": { - "typo3/cms-core": "^11.5", - "phpoffice/phpspreadsheet": "^1.29.7" + "typo3/cms-core": "^13.4", + "phpoffice/phpspreadsheet": "^1.30" }, "autoload": { "psr-4": { From 807237a25ae4ce89ac3fedee2f989a5e346eeaa3 Mon Sep 17 00:00:00 2001 From: Daniel Huf <1814195+dhuf@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:13:43 +0200 Subject: [PATCH 02/12] Update phpspreadsheet to v4 as powermail uses it --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 962eb2d..8dcee60 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "require": { "typo3/cms-core": "^13.4", - "phpoffice/phpspreadsheet": "^1.30" + "phpoffice/phpspreadsheet": "^4" }, "autoload": { "psr-4": { From 90fc00836730a5faadd48acc012f5c6b42a5ad51 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 18:17:38 +0200 Subject: [PATCH 03/12] feat: Run rector --- Classes/Controller/UserimportController.php | 35 ++++++------------ Classes/Domain/Model/ImportJob.php | 36 +++++-------------- .../UploadedFileReferenceConverter.php | 18 +++------- Classes/Service/SpreadsheetService.php | 4 +-- Classes/Service/TcaService.php | 17 +++------ Classes/Service/UserImportService.php | 14 ++------ Classes/ViewHelpers/Form/UploadViewHelper.php | 10 +++--- Configuration/Backend/Modules.php | 20 +++++++++++ Configuration/TCA/Overrides/sys_template.php | 3 ++ .../tx_userimport_domain_model_importjob.php | 29 +++++++-------- .../Controller/UserimportControllerTest.php | 2 +- ext_tables.php | 22 ------------ 12 files changed, 73 insertions(+), 137 deletions(-) create mode 100644 Configuration/Backend/Modules.php create mode 100644 Configuration/TCA/Overrides/sys_template.php diff --git a/Classes/Controller/UserimportController.php b/Classes/Controller/UserimportController.php index 8b3a110..eda342c 100644 --- a/Classes/Controller/UserimportController.php +++ b/Classes/Controller/UserimportController.php @@ -36,31 +36,28 @@ class UserimportController extends ActionController /** * @var ImportJobRepository */ - protected $importJobRepository = null; + protected $importJobRepository; /** * @var PersistenceManagerInterface */ - protected $persistenceManager = null; + protected $persistenceManager; /** * @var SpreadsheetService */ - protected $spreadsheetService = null; + protected $spreadsheetService; /** * @var UserImportService */ - protected $userImportService = null; + protected $userImportService; /** * @var TcaService */ - protected $tcaService = null; + protected $tcaService; - /** - * @return void - */ public function mainAction(): ResponseInterface { $importJob = GeneralUtility::makeInstance(ImportJob::class); @@ -94,18 +91,18 @@ protected function initializeUploadAction() /** * @param ImportJob $importJob * + * @return \Psr\Http\Message\ResponseInterface + */ + /** * @return void */ public function uploadAction(ImportJob $importJob) { $this->importJobRepository->add($importJob); $this->persistenceManager->persistAll(); - $this->redirect('options', null, null, ['importJob' => $importJob]); + return $this->redirect('options', null, null, ['importJob' => $importJob]); } - /** - * @param ImportJob $importJob - */ public function optionsAction(ImportJob $importJob): ResponseInterface { $this->view->assign('importJob', $importJob); @@ -122,9 +119,6 @@ public function optionsAction(ImportJob $importJob): ResponseInterface return $this->htmlResponse(); } - /** - * @param ImportJob $importJob - */ public function fieldMappingAction(ImportJob $importJob): ResponseInterface { $this->view->assign('importJob', $importJob); @@ -168,10 +162,6 @@ public function fieldMappingAction(ImportJob $importJob): ResponseInterface return $this->htmlResponse(); } - /** - * @param ImportJob $importJob - * @param array $fieldMapping - */ public function importPreviewAction(ImportJob $importJob, array $fieldMapping): ResponseInterface { $this->view->assign('importJob', $importJob); @@ -187,9 +177,6 @@ public function importPreviewAction(ImportJob $importJob, array $fieldMapping): return $this->htmlResponse(); } - /** - * @param ImportJob $importJob - */ public function performImportAction(ImportJob $importJob): ResponseInterface { $rowsToImport = $this->spreadsheetService->generateDataFromImportJob($importJob); @@ -212,10 +199,8 @@ public function performImportAction(ImportJob $importJob): ResponseInterface /** * Deactivate errorFlashMessage - * - * @return bool|string */ - public function getErrorFlashMessage() + public function getErrorFlashMessage(): bool|string { return false; } diff --git a/Classes/Domain/Model/ImportJob.php b/Classes/Domain/Model/ImportJob.php index 8bb50f3..ad10b05 100644 --- a/Classes/Domain/Model/ImportJob.php +++ b/Classes/Domain/Model/ImportJob.php @@ -29,12 +29,12 @@ class ImportJob extends AbstractEntity /** * @var FileReference */ - protected $file = null; + protected $file; /** * @var string - * @TYPO3\CMS\Extbase\Annotation\ORM\Transient */ + #[TYPO3\CMS\Extbase\Annotation\ORM\Transient] protected $importOptions; /** @@ -53,29 +53,21 @@ public function getFile() /** * Sets the image * - * @param FileReference $file * - * @return void */ - public function setFile(FileReference $file) + public function setFile(FileReference $file): void { $this->file = $file; } - /** - * @return string - */ public function getImportOptions(): string { return $this->importOptions; } - /** - * @return array - */ public function getImportOptionsArray(): array { - return !empty($this->importOptions) ? unserialize($this->importOptions) : []; + return empty($this->importOptions) ? [] : unserialize($this->importOptions); } /** @@ -85,37 +77,25 @@ public function getImportOptionsArray(): array */ public function getImportOption($option) { - return array_key_exists($option, $this->getImportOptionsArray()) ? $this->getImportOptionsArray()[$option] : null; + return $this->getImportOptionsArray()[$option] ?? null; } - /** - * @param array $importOptions - */ - public function setImportOptions(array $importOptions) + public function setImportOptions(array $importOptions): void { $this->importOptions = serialize($importOptions); } - /** - * @return string - */ public function getFieldMapping(): string { return $this->fieldMapping; } - /** - * @return array - */ public function getFieldMappingArray(): array { - return !empty($this->fieldMapping) ? unserialize($this->fieldMapping) : []; + return empty($this->fieldMapping) ? [] : unserialize($this->fieldMapping); } - /** - * @param array $fieldMapping - */ - public function setFieldMapping(array $fieldMapping) + public function setFieldMapping(array $fieldMapping): void { $this->fieldMapping = serialize($fieldMapping); } diff --git a/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php b/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php index 3a380bc..fb12814 100644 --- a/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php +++ b/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php @@ -54,6 +54,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter { + public $objectManager; /** * Folder where the file upload should go to (including storage). */ @@ -94,7 +95,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter protected $resourceFactory; /** - * @var HashService + * @var \TYPO3\CMS\Core\Crypto\HashService */ protected $hashService; @@ -114,8 +115,6 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter * * @param string|int $source * @param string $targetType - * @param array $convertedChildProperties - * @param PropertyMappingConfigurationInterface $configuration * @throws \TYPO3\CMS\Extbase\Property\Exception * @return AbstractFileFolder * @api @@ -129,9 +128,8 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie if (strpos($resourcePointer, 'file:') === 0) { $fileUid = substr($resourcePointer, 5); return $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid)); - } else { - return $this->createFileReferenceFromFalFileReferenceObject($this->resourceFactory->getFileReferenceObject($resourcePointer), $resourcePointer); } + return $this->createFileReferenceFromFalFileReferenceObject($this->resourceFactory->getFileReferenceObject($resourcePointer), $resourcePointer); } catch (\InvalidArgumentException $e) { // Nothing to do. No file is uploaded and resource pointer is invalid. Discard! } @@ -165,7 +163,6 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie } /** - * @param FalFile $file * @param int $resourcePointer * @return FileReference */ @@ -185,8 +182,6 @@ protected function createFileReferenceFromFalFileObject(FalFile $file, $resource /** * Import a resource and respect configuration given for properties * - * @param array $uploadInfo - * @param PropertyMappingConfigurationInterface $configuration * @return \TYPO3\CMS\Extbase\Domain\Model\FileReference * @throws TypeConverterException * @throws ExistingTargetFileNameException @@ -226,13 +221,10 @@ protected function importUploadedResource(array $uploadInfo, PropertyMappingConf ? $this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer']) : null; - $fileReferenceModel = $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer); - - return $fileReferenceModel; + return $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer); } /** - * @param FalFileReference $falFileReference * @param int $resourcePointer * @return FileReference */ @@ -255,7 +247,7 @@ public function injectResourceFactory(ResourceFactory $resourceFactory): void $this->resourceFactory = $resourceFactory; } - public function injectHashService(HashService $hashService): void + public function injectHashService(\TYPO3\CMS\Core\Crypto\HashService $hashService): void { $this->hashService = $hashService; } diff --git a/Classes/Service/SpreadsheetService.php b/Classes/Service/SpreadsheetService.php index 596ce7f..c896c6f 100644 --- a/Classes/Service/SpreadsheetService.php +++ b/Classes/Service/SpreadsheetService.php @@ -118,9 +118,7 @@ public function getColumnLabelsAndExamples($fileName, $firstRowContainsFieldName /** * Generate data from import job data and configuration * - * @param ImportJob $importJob * @param bool $isPreview - * * @return array */ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = false) @@ -148,7 +146,7 @@ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = fal $row = empty($tcaDefaultsRow) ? [] : $tcaDefaultsRow; foreach ($fieldMapping as $columnIndex => $fieldName) { $value = $worksheet->getCellByColumnAndRow(Coordinate::columnIndexFromString($columnIndex), $rowIndex)->getValue(); - $row[$fieldName] = !empty($value) ? $value : ''; + $row[$fieldName] = empty($value) ? '' : $value; } if (!array_filter($row)) { diff --git a/Classes/Service/TcaService.php b/Classes/Service/TcaService.php index c5f2d2c..ac8967f 100644 --- a/Classes/Service/TcaService.php +++ b/Classes/Service/TcaService.php @@ -38,11 +38,8 @@ public function getFrontendUserFolders() ->from('pages') ->where( $queryBuilder->expr()->eq('doktype', 254), - $queryBuilder->expr()->eq('module', $queryBuilder->createNamedParameter('fe_users', \PDO::PARAM_STR)) - ) - ->addOrderBy('uid', 'DESC') - ->execute() - ->fetchAll(); + $queryBuilder->expr()->eq('module', $queryBuilder->createNamedParameter('fe_users', \TYPO3\CMS\Core\Database\Connection::PARAM_STR)) + )->addOrderBy('uid', 'DESC')->executeQuery()->fetchAllAssociative(); $folders = []; @@ -54,7 +51,7 @@ public function getFrontendUserFolders() } $folders[] = [ 'uid' => $page['uid'], - 'title' => !empty($title) ? $title : $page['title'] + 'title' => $title === '' || $title === '0' ? $page['title'] : $title ]; } @@ -70,12 +67,8 @@ public function getFrontendUserGroups() { /** @var QueryBuilder $queryBuilder */ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_groups'); - $result = $queryBuilder - ->select('uid', 'title') - ->from('fe_groups') - ->execute() - ->fetchAll(); - return $result; + return $queryBuilder + ->select('uid', 'title')->from('fe_groups')->executeQuery()->fetchAllAssociative(); } /** diff --git a/Classes/Service/UserImportService.php b/Classes/Service/UserImportService.php index 84d3dd0..19e9b65 100644 --- a/Classes/Service/UserImportService.php +++ b/Classes/Service/UserImportService.php @@ -25,8 +25,6 @@ class UserImportService implements SingletonInterface /** * Imports/updates all given rows as fe_user records respecting the options in the ImportJob * - * @param array $rowsToImport - * @param ImportJob $importJob * * @return array */ @@ -73,7 +71,6 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) 'disable' => 0 ] ); - if ($affectedRecords === 1) { $log[] = [ 'action' => 'update.success', @@ -86,11 +83,10 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) 'row' => $rowForLog ]; } - $updatedRecords += $affectedRecords; - continue; - } elseif ($existing > 1) { + } + if ($existing > 1) { // More than one record, fail $log[] = [ 'action' => 'update.moreThanOneRecordFound', @@ -100,9 +96,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) } // Must be newly imported $affectedRecords = $queryBuilder - ->insert('fe_users', null) - ->values($row) - ->execute(); + ->insert('fe_users')->values($row)->executeStatement(); if ($affectedRecords < 1) { // Error case @@ -117,8 +111,6 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) ]; } $insertedRecords += $affectedRecords; - - continue; } return [ diff --git a/Classes/ViewHelpers/Form/UploadViewHelper.php b/Classes/ViewHelpers/Form/UploadViewHelper.php index ba501f5..96da7e1 100644 --- a/Classes/ViewHelpers/Form/UploadViewHelper.php +++ b/Classes/ViewHelpers/Form/UploadViewHelper.php @@ -35,7 +35,7 @@ class UploadViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\UploadViewHelper { /** - * @var HashService + * @var \TYPO3\CMS\Core\Crypto\HashService */ protected $hashService; @@ -58,7 +58,7 @@ public function render() if ($resource !== null) { $resourcePointerIdAttribute = ''; if ($this->hasArgument('id')) { - $resourcePointerIdAttribute = ' id="' . htmlspecialchars($this->arguments['id']) . '-file-reference"'; + $resourcePointerIdAttribute = ' id="' . htmlspecialchars($this->additionalArguments['id']) . '-file-reference"'; } $resourcePointerValue = $resource->getUid(); if ($resourcePointerValue === null) { @@ -72,9 +72,7 @@ public function render() $output .= $this->renderChildren(); $this->templateVariableContainer->remove('resource'); } - - $output .= parent::render(); - return $output; + return $output . parent::render(); } /** @@ -100,7 +98,7 @@ protected function getUploadedResource() return $this->propertyMapper->convert($resource, 'TYPO3\\CMS\\Extbase\\Domain\\Model\\FileReference'); } - public function injectHashService(HashService $hashService): void + public function injectHashService(\TYPO3\CMS\Core\Crypto\HashService $hashService): void { $this->hashService = $hashService; } diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..02cd8e3 --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,20 @@ + [ + 'parent' => 'web', + 'access' => 'user', + 'labels' => 'LLL:EXT:userimport/Resources/Private/Language/locallang_userimport.xlf', + 'extensionName' => 'Userimport', + 'controllerActions' => [ + 'Visol\Userimport\Controller\UserimportController' => [ + 'main', + 'upload', + 'options', + 'fieldMapping', + 'importPreview', + 'performImport', + ], + ], + ], +]; diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000..9b5ae6e --- /dev/null +++ b/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,3 @@ + 'title', 'tstamp' => 'tstamp', 'crdate' => 'crdate', - 'cruser_id' => 'cruser_id', 'delete' => 'deleted', 'enablecolumns' => [ 'disabled' => 'hidden' @@ -38,22 +37,20 @@ 'file' => [ 'exclude' => true, 'label' => $ll . 'tx_userimport_domain_model_importjob.file', - 'config' => ExtensionManagementUtility::getFileFieldTCAConfig( - 'file', - [ - 'appearance' => [ - 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference' - ], - 'foreign_match_fields' => [ - 'fieldname' => 'file', - 'tablenames' => 'tx_userimport_domain_model_importjob', - 'table_local' => 'sys_file', - ], - 'minitems' => 1, - 'maxitems' => 1, + 'config' => [ + ### !!! Watch out for fieldName different from columnName + 'type' => 'file', + 'allowed' => 'xlsx,csv', + 'appearance' => [ + 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference' + ], + 'foreign_match_fields' => [ + 'fieldname' => 'file', + 'tablenames' => 'tx_userimport_domain_model_importjob', ], - 'xlsx,csv' - ), + 'minitems' => 1, + 'maxitems' => 1, + ], ], 'import_options' => [ 'exclude' => true, diff --git a/Tests/Unit/Controller/UserimportControllerTest.php b/Tests/Unit/Controller/UserimportControllerTest.php index 3dfc142..93e73d8 100644 --- a/Tests/Unit/Controller/UserimportControllerTest.php +++ b/Tests/Unit/Controller/UserimportControllerTest.php @@ -13,7 +13,7 @@ class UserimportControllerTest extends UnitTestCase /** * @var UserimportController */ - protected $subject = null; + protected $subject; protected function setUp() { diff --git a/ext_tables.php b/ext_tables.php index bb6ed29..aa37c9c 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -6,27 +6,5 @@ call_user_func( function () { - - if (TYPO3_MODE === 'BE') { - - ExtensionUtility::registerModule( - 'Userimport', - 'web', // Make module a submodule of 'web' - 'userimport', // Submodule key - '', // Position - [ - UserimportController::class => 'main,upload,options,fieldMapping,importPreview,performImport', - - ], - [ - 'access' => 'user,group', - 'navigationComponentId' => null, - 'icon' => 'EXT:userimport/Resources/Public/Icons/user_mod_userimport.svg', - 'labels' => 'LLL:EXT:userimport/Resources/Private/Language/locallang_userimport.xlf', - ] - ); - } - - ExtensionManagementUtility::addStaticFile('userimport', 'Configuration/TypoScript', 'Frontend User Import'); } ); From 8a6dfaa5a82372ea915a66819b0d7b45db35ce12 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 18:19:02 +0200 Subject: [PATCH 04/12] feat: Run fractor --- Configuration/TypoScript/constants.typoscript | 24 +++++++++---------- Configuration/TypoScript/setup.typoscript | 22 ++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Configuration/TypoScript/constants.typoscript b/Configuration/TypoScript/constants.typoscript index 5ba3012..967b6c1 100644 --- a/Configuration/TypoScript/constants.typoscript +++ b/Configuration/TypoScript/constants.typoscript @@ -1,15 +1,15 @@ module.tx_userimport_userimport { - view { - # cat=module.tx_userimport_userimport/file; type=string; label=Path to template root (BE) - templateRootPath = EXT:userimport/Resources/Private/Backend/Templates/ - # cat=module.tx_userimport_userimport/file; type=string; label=Path to template partials (BE) - partialRootPath = EXT:userimport/Resources/Private/Backend/Partials/ - # cat=module.tx_userimport_userimport/file; type=string; label=Path to template layouts (BE) - layoutRootPath = EXT:userimport/Resources/Private/Backend/Layouts/ - } - persistence { - # cat=module.tx_userimport_userimport//a; type=string; label=Default storage PID - storagePid = - } + view { + # cat=module.tx_userimport_userimport/file; type=string; label=Path to template root (BE) + templateRootPath = EXT:userimport/Resources/Private/Backend/Templates/ + # cat=module.tx_userimport_userimport/file; type=string; label=Path to template partials (BE) + partialRootPath = EXT:userimport/Resources/Private/Backend/Partials/ + # cat=module.tx_userimport_userimport/file; type=string; label=Path to template layouts (BE) + layoutRootPath = EXT:userimport/Resources/Private/Backend/Layouts/ + } + persistence { + # cat=module.tx_userimport_userimport//a; type=string; label=Default storage PID + storagePid = + } } diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 135c432..1ea8537 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -1,15 +1,15 @@ # Module configuration module.tx_userimport_web_userimportuserimport { - persistence { - storagePid = {$module.tx_userimport_userimport.persistence.storagePid} - } - view { - templateRootPaths.0 = EXT:userimport/Resources/Private/Backend/Templates/ - templateRootPaths.1 = {$module.tx_userimport_userimport.view.templateRootPath} - partialRootPaths.0 = EXT:userimport/Resources/Private/Backend/Partials/ - partialRootPaths.1 = {$module.tx_userimport_userimport.view.partialRootPath} - layoutRootPaths.0 = EXT:userimport/Resources/Private/Backend/Layouts/ - layoutRootPaths.1 = {$module.tx_userimport_userimport.view.layoutRootPath} - } + persistence { + storagePid = {$module.tx_userimport_userimport.persistence.storagePid} + } + view { + templateRootPaths.0 = EXT:userimport/Resources/Private/Backend/Templates/ + templateRootPaths.1 = {$module.tx_userimport_userimport.view.templateRootPath} + partialRootPaths.0 = EXT:userimport/Resources/Private/Backend/Partials/ + partialRootPaths.1 = {$module.tx_userimport_userimport.view.partialRootPath} + layoutRootPaths.0 = EXT:userimport/Resources/Private/Backend/Layouts/ + layoutRootPaths.1 = {$module.tx_userimport_userimport.view.layoutRootPath} + } } From 76112b57d48137466429fa6dd3b44130f2938173 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 18:33:22 +0200 Subject: [PATCH 05/12] feat: Run PHP_CS --- Classes/Controller/UserimportController.php | 29 +++--- Classes/Domain/Model/ImportJob.php | 8 +- .../Domain/Repository/ImportJobRepository.php | 3 +- .../UploadedFileReferenceConverter.php | 91 ++++++++++--------- Classes/Service/SpreadsheetService.php | 18 ++-- Classes/Service/TcaService.php | 30 +++--- Classes/Service/UserImportService.php | 22 ++--- Classes/Utility/LogUtility.php | 67 +++++++------- Configuration/Backend/Modules.php | 2 +- .../tx_userimport_domain_model_importjob.php | 14 +-- ext_emconf.php | 1 + ext_localconf.php | 4 +- ext_tables.php | 4 +- 13 files changed, 138 insertions(+), 155 deletions(-) diff --git a/Classes/Controller/UserimportController.php b/Classes/Controller/UserimportController.php index eda342c..c15f28c 100644 --- a/Classes/Controller/UserimportController.php +++ b/Classes/Controller/UserimportController.php @@ -13,26 +13,25 @@ * ***/ +use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; -use Visol\Userimport\Domain\Repository\ImportJobRepository; use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; -use Visol\Userimport\Service\SpreadsheetService; -use Visol\Userimport\Service\UserImportService; -use Visol\Userimport\Service\TcaService; -use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration; use Visol\Userimport\Domain\Model\ImportJob; +use Visol\Userimport\Domain\Repository\ImportJobRepository; use Visol\Userimport\Mvc\Property\TypeConverter\UploadedFileReferenceConverter; +use Visol\Userimport\Service\SpreadsheetService; +use Visol\Userimport\Service\TcaService; +use Visol\Userimport\Service\UserImportService; /** * UserimportController */ class UserimportController extends ActionController { - /** * @var ImportJobRepository */ @@ -78,7 +77,7 @@ protected function initializeUploadAction() /** @var PropertyMappingConfiguration $propertyMappingConfiguration */ $propertyMappingConfiguration = $this->arguments['importJob']->getPropertyMappingConfiguration(); $uploadConfiguration = [ - UploadedFileReferenceConverter::CONFIGURATION_ALLOWED_FILE_EXTENSIONS => 'xlsx,csv' + UploadedFileReferenceConverter::CONFIGURATION_ALLOWED_FILE_EXTENSIONS => 'xlsx,csv', ]; $propertyMappingConfiguration->allowProperties('file'); $propertyMappingConfiguration->forProperty('file') @@ -89,12 +88,7 @@ protected function initializeUploadAction() } /** - * @param ImportJob $importJob - * - * @return \Psr\Http\Message\ResponseInterface - */ - /** - * @return void + * @return ResponseInterface */ public function uploadAction(ImportJob $importJob) { @@ -131,7 +125,7 @@ public function fieldMappingAction(ImportJob $importJob): ResponseInterface ImportJob::IMPORT_OPTION_GENERATE_PASSWORD, ImportJob::IMPORT_OPTION_USER_GROUPS, ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS, - ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD + ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD, ]; $fieldOptionsArray = []; foreach ($fieldOptionArguments as $argumentName) { @@ -153,11 +147,11 @@ public function fieldMappingAction(ImportJob $importJob): ResponseInterface ); // If username is not generated from e-mail, the field must be mapped - $usernameMustBeMapped = !(bool)$importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME); + $usernameMustBeMapped = !(bool) $importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME); $this->view->assign('usernameMustBeMapped', $usernameMustBeMapped); // If username is generated from e-mail, the field e-mail must be mapped - $emailMustBeMapped = (bool)$importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME); + $emailMustBeMapped = (bool) $importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME); $this->view->assign('emailMustBeMapped', $emailMustBeMapped); return $this->htmlResponse(); } @@ -184,7 +178,6 @@ public function performImportAction(ImportJob $importJob): ResponseInterface $result = $this->userImportService->performImport($importJob, $rowsToImport); - $this->view->assign('updatedRecords', $result['updatedRecords']); $this->view->assign('insertedRecords', $result['insertedRecords']); $this->view->assign('log', $result['log']); diff --git a/Classes/Domain/Model/ImportJob.php b/Classes/Domain/Model/ImportJob.php index ad10b05..a1fbd56 100644 --- a/Classes/Domain/Model/ImportJob.php +++ b/Classes/Domain/Model/ImportJob.php @@ -2,9 +2,9 @@ namespace Visol\Userimport\Domain\Model; -use TYPO3\CMS\Extbase\Annotation as Extbase; -use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; + /*** * * This file is part of the "Frontend User Import" Extension for TYPO3 CMS. @@ -17,7 +17,6 @@ ***/ class ImportJob extends AbstractEntity { - const IMPORT_OPTION_TARGET_FOLDER = 'targetFolder'; const IMPORT_OPTION_FIRST_ROW_CONTAINS_FIELD_NAMES = 'firstRowContainsFieldNames'; const IMPORT_OPTION_USE_EMAIL_AS_USERNAME = 'useEmailAsUsername'; @@ -52,8 +51,6 @@ public function getFile() /** * Sets the image - * - * */ public function setFile(FileReference $file): void { @@ -72,7 +69,6 @@ public function getImportOptionsArray(): array /** * @param string $option - * * @return mixed */ public function getImportOption($option) diff --git a/Classes/Domain/Repository/ImportJobRepository.php b/Classes/Domain/Repository/ImportJobRepository.php index d3bae8f..8d69366 100644 --- a/Classes/Domain/Repository/ImportJobRepository.php +++ b/Classes/Domain/Repository/ImportJobRepository.php @@ -1,7 +1,9 @@ hashService->validateAndStripHmac($source['submittedFile']['resourcePointer']); @@ -130,18 +133,18 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie return $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid)); } return $this->createFileReferenceFromFalFileReferenceObject($this->resourceFactory->getFileReferenceObject($resourcePointer), $resourcePointer); - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { // Nothing to do. No file is uploaded and resource pointer is invalid. Discard! } } return null; } - if ($source['error'] !== \UPLOAD_ERR_OK) { + if ($source['error'] !== UPLOAD_ERR_OK) { switch ($source['error']) { - case \UPLOAD_ERR_INI_SIZE: - case \UPLOAD_ERR_FORM_SIZE: - case \UPLOAD_ERR_PARTIAL: + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + case UPLOAD_ERR_PARTIAL: return new Error('Error Code: ' . $source['error'], 1264440823); default: return new Error('An error occurred while uploading. Please try again or contact the administrator if the problem remains', 1340193849); @@ -163,7 +166,7 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie } /** - * @param int $resourcePointer + * @param null|int $resourcePointer * @return FileReference */ protected function createFileReferenceFromFalFileObject(FalFile $file, $resourcePointer = null) @@ -182,7 +185,7 @@ protected function createFileReferenceFromFalFileObject(FalFile $file, $resource /** * Import a resource and respect configuration given for properties * - * @return \TYPO3\CMS\Extbase\Domain\Model\FileReference + * @return FileReference * @throws TypeConverterException * @throws ExistingTargetFileNameException */ @@ -192,7 +195,7 @@ protected function importUploadedResource(array $uploadInfo, PropertyMappingConf throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1399312430); } - $allowedFileExtensions = $configuration->getConfigurationValue(UploadedFileReferenceConverter::class, self::CONFIGURATION_ALLOWED_FILE_EXTENSIONS); + $allowedFileExtensions = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_ALLOWED_FILE_EXTENSIONS); if ($allowedFileExtensions !== null) { $filePathInfo = PathUtility::pathinfo($uploadInfo['name']); @@ -210,12 +213,12 @@ protected function importUploadedResource(array $uploadInfo, PropertyMappingConf throw new \Exception('You must configure an upload folder in the Extension Manager configuration.', 1643747930); } - $uploadFolderId = $configuration->getConfigurationValue(UploadedFileReferenceConverter::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $uploadStorageFolder; + $uploadFolderId = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $uploadStorageFolder; $defaultConflictMode = DuplicationBehavior::RENAME; - $conflictMode = $configuration->getConfigurationValue(UploadedFileReferenceConverter::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $defaultConflictMode; + $conflictMode = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $defaultConflictMode; $uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId); - $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode); + $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode); $resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) && strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === false ? $this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer']) @@ -225,13 +228,13 @@ protected function importUploadedResource(array $uploadInfo, PropertyMappingConf } /** - * @param int $resourcePointer + * @param null|int $resourcePointer * @return FileReference */ protected function createFileReferenceFromFalFileReferenceObject(FalFileReference $falFileReference, $resourcePointer = null) { if ($resourcePointer === null) { - /** @var $fileReference FileReference */ + /** @var FileReference $fileReference */ $fileReference = $this->objectManager->get(FileReference::class); } else { $fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, FileReference::class, false); @@ -247,7 +250,7 @@ public function injectResourceFactory(ResourceFactory $resourceFactory): void $this->resourceFactory = $resourceFactory; } - public function injectHashService(\TYPO3\CMS\Core\Crypto\HashService $hashService): void + public function injectHashService(HashService $hashService): void { $this->hashService = $hashService; } diff --git a/Classes/Service/SpreadsheetService.php b/Classes/Service/SpreadsheetService.php index c896c6f..581f111 100644 --- a/Classes/Service/SpreadsheetService.php +++ b/Classes/Service/SpreadsheetService.php @@ -12,12 +12,13 @@ * (c) 2018 Lorenz Ulrich , visol digitale Dienstleistungen GmbH * ***/ -use TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility; -use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; + use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility; use TYPO3\CMS\Core\Crypto\Random; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; @@ -26,14 +27,12 @@ class SpreadsheetService implements SingletonInterface { - /** * Return the content of the spreadsheet's first worksheet * * @param string $fileName - * @param int $numberOfRowsToReturn + * @param null|int $numberOfRowsToReturn * @param bool $skipFirstRow - * * @return array */ public function getContent($fileName, $numberOfRowsToReturn = null, $skipFirstRow = false) @@ -72,7 +71,6 @@ public function getContent($fileName, $numberOfRowsToReturn = null, $skipFirstRo * @param $fileName * @param bool $firstRowContainsFieldNames * @param int $numberOfExamples - * * @return array */ public function getColumnLabelsAndExamples($fileName, $firstRowContainsFieldNames = false, $numberOfExamples = 5) @@ -156,7 +154,7 @@ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = fal $rows[$i] = $row; // Process import options - if ((bool)$importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME)) { + if ((bool) $importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME)) { $rows[$i]['username'] = $rows[$i]['email']; } @@ -194,7 +192,7 @@ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = fal $rows[$i]['password'] = $saltedPassword; // PID - $rows[$i]['pid'] = (int)$importJob->getImportOption(ImportJob::IMPORT_OPTION_TARGET_FOLDER); + $rows[$i]['pid'] = (int) $importJob->getImportOption(ImportJob::IMPORT_OPTION_TARGET_FOLDER); // crtime/tstamp $rows[$i]['crdate'] = time(); @@ -203,7 +201,7 @@ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = fal // User groups if (!empty($importJob->getImportOption(ImportJob::IMPORT_OPTION_USER_GROUPS))) { $rows[$i]['usergroup'] = implode(',', $importJob->getImportOption(ImportJob::IMPORT_OPTION_USER_GROUPS)); - }; + } } $i++; @@ -217,7 +215,6 @@ public function generateDataFromImportJob(ImportJob $importJob, $isPreview = fal * Respects the TSConfig applying to the given page * * @param $targetFolderUid - * * @return array */ protected function getTcaDefaultsRow($targetFolderUid) @@ -246,7 +243,6 @@ protected function getTcaDefaultsRow($targetFolderUid) /** * @param string $fileName - * * @return Spreadsheet */ protected function getSpreadsheet($fileName) diff --git a/Classes/Service/TcaService.php b/Classes/Service/TcaService.php index ac8967f..17b1b07 100644 --- a/Classes/Service/TcaService.php +++ b/Classes/Service/TcaService.php @@ -2,6 +2,14 @@ namespace Visol\Userimport\Service; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Database\QueryGenerator; +use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; + /*** * * This file is part of the "Frontend User Import" Extension for TYPO3 CMS. @@ -13,16 +21,8 @@ * ***/ -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Database\QueryGenerator; -use TYPO3\CMS\Core\SingletonInterface; -use TYPO3\CMS\Core\Utility\GeneralUtility; - class TcaService implements SingletonInterface { - /** * Return all pages of type folder containing frontend users * @@ -38,7 +38,7 @@ public function getFrontendUserFolders() ->from('pages') ->where( $queryBuilder->expr()->eq('doktype', 254), - $queryBuilder->expr()->eq('module', $queryBuilder->createNamedParameter('fe_users', \TYPO3\CMS\Core\Database\Connection::PARAM_STR)) + $queryBuilder->expr()->eq('module', $queryBuilder->createNamedParameter('fe_users', Connection::PARAM_STR)) )->addOrderBy('uid', 'DESC')->executeQuery()->fetchAllAssociative(); $folders = []; @@ -51,7 +51,7 @@ public function getFrontendUserFolders() } $folders[] = [ 'uid' => $page['uid'], - 'title' => $title === '' || $title === '0' ? $page['title'] : $title + 'title' => $title === '' || $title === '0' ? $page['title'] : $title, ]; } @@ -82,16 +82,16 @@ public function getFrontendUserTableUniqueFieldNames() return [ [ 'value' => 'name', - 'label' => 'name' + 'label' => 'name', ], [ 'value' => 'username', - 'label' => 'username' + 'label' => 'username', ], [ 'value' => 'email', - 'label' => 'email' - ] + 'label' => 'email', + ], ]; } @@ -117,7 +117,7 @@ public function getFrontendUserTableFieldNames() } $fieldArray[] = [ 'label' => $fieldName, - 'value' => $fieldName + 'value' => $fieldName, ]; } return $fieldArray; diff --git a/Classes/Service/UserImportService.php b/Classes/Service/UserImportService.php index 19e9b65..bb4cc90 100644 --- a/Classes/Service/UserImportService.php +++ b/Classes/Service/UserImportService.php @@ -21,11 +21,9 @@ ***/ class UserImportService implements SingletonInterface { - /** * Imports/updates all given rows as fe_user records respecting the options in the ImportJob * - * * @return array */ public function performImport(ImportJob $importJob, array $rowsToImport = []) @@ -35,7 +33,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) /** @var QueryBuilder $queryBuilder */ $queryBuilder = $connectionPool->getQueryBuilderForTable('fe_users'); - $updateExisting = (bool)$importJob->getImportOption(ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS); + $updateExisting = (bool) $importJob->getImportOption(ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS); $updatedRecords = 0; $insertedRecords = 0; @@ -48,7 +46,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) $rowForLog = LogUtility::formatRowForImportLog($row); if ($updateExisting) { - $targetFolder = (int)$importJob->getImportOption(ImportJob::IMPORT_OPTION_TARGET_FOLDER); + $targetFolder = (int) $importJob->getImportOption(ImportJob::IMPORT_OPTION_TARGET_FOLDER); $updateExistingUniqueField = $importJob->getImportOption(ImportJob::IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD); $existing = $feUsersConnection->count( 'uid', @@ -57,7 +55,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) $updateExistingUniqueField => $row[$updateExistingUniqueField], 'pid' => $targetFolder, 'deleted' => 0, - 'disable' => 0 + 'disable' => 0, ] ); if ($existing === 1) { @@ -68,19 +66,19 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) $updateExistingUniqueField => $row[$updateExistingUniqueField], 'pid' => $targetFolder, 'deleted' => 0, - 'disable' => 0 + 'disable' => 0, ] ); if ($affectedRecords === 1) { $log[] = [ 'action' => 'update.success', - 'row' => $rowForLog + 'row' => $rowForLog, ]; } else { // Error case $log[] = [ 'action' => 'update.fail', - 'row' => $rowForLog + 'row' => $rowForLog, ]; } $updatedRecords += $affectedRecords; @@ -90,7 +88,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) // More than one record, fail $log[] = [ 'action' => 'update.moreThanOneRecordFound', - 'row' => $rowForLog + 'row' => $rowForLog, ]; } } @@ -102,12 +100,12 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) // Error case $log[] = [ 'action' => 'insert.fail', - 'row' => $rowForLog + 'row' => $rowForLog, ]; } else { $log[] = [ 'action' => 'insert.success', - 'row' => $rowForLog + 'row' => $rowForLog, ]; } $insertedRecords += $affectedRecords; @@ -116,7 +114,7 @@ public function performImport(ImportJob $importJob, array $rowsToImport = []) return [ 'insertedRecords' => $insertedRecords, 'updatedRecords' => $updatedRecords, - 'log' => $log + 'log' => $log, ]; } } diff --git a/Classes/Utility/LogUtility.php b/Classes/Utility/LogUtility.php index 4650300..c02fc75 100644 --- a/Classes/Utility/LogUtility.php +++ b/Classes/Utility/LogUtility.php @@ -1,35 +1,32 @@ -, visol digitale Dienstleistungen GmbH - * - ***/ - - -class LogUtility -{ - - /** - * @param $row - * - * @return string - */ - public static function formatRowForImportLog($row) - { - // Unset values not to be displayed in the log - unset($row['password']); - unset($row['crdate']); - unset($row['tstamp']); - unset($row['usergroup']); - - return implode(" | ", $row); - } -} +, visol digitale Dienstleistungen GmbH + * + ***/ + +class LogUtility +{ + /** + * @param $row + * @return string + */ + public static function formatRowForImportLog($row) + { + // Unset values not to be displayed in the log + unset($row['password']); + unset($row['crdate']); + unset($row['tstamp']); + unset($row['usergroup']); + + return implode(" | ", $row); + } +} diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php index 02cd8e3..9c77a9a 100644 --- a/Configuration/Backend/Modules.php +++ b/Configuration/Backend/Modules.php @@ -7,7 +7,7 @@ 'labels' => 'LLL:EXT:userimport/Resources/Private/Language/locallang_userimport.xlf', 'extensionName' => 'Userimport', 'controllerActions' => [ - 'Visol\Userimport\Controller\UserimportController' => [ + \Visol\Userimport\Controller\UserimportController::class => [ 'main', 'upload', 'options', diff --git a/Configuration/TCA/tx_userimport_domain_model_importjob.php b/Configuration/TCA/tx_userimport_domain_model_importjob.php index b622f98..3b39b99 100644 --- a/Configuration/TCA/tx_userimport_domain_model_importjob.php +++ b/Configuration/TCA/tx_userimport_domain_model_importjob.php @@ -1,7 +1,7 @@ 'crdate', 'delete' => 'deleted', 'enablecolumns' => [ - 'disabled' => 'hidden' + 'disabled' => 'hidden', ], 'searchFields' => 'title,style,cached_votes,cached_rank,image,votes,', 'iconfile' => 'EXT:userimport/Resources/Public/Icons/tx_userimport_domain_model_importjob.gif', @@ -38,11 +38,11 @@ 'exclude' => true, 'label' => $ll . 'tx_userimport_domain_model_importjob.file', 'config' => [ - ### !!! Watch out for fieldName different from columnName + // !!! Watch out for fieldName different from columnName 'type' => 'file', 'allowed' => 'xlsx,csv', 'appearance' => [ - 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference' + 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference', ], 'foreign_match_fields' => [ 'fieldname' => 'file', @@ -59,7 +59,7 @@ 'type' => 'text', 'cols' => 60, 'rows' => 5, - ] + ], ], 'field_mapping' => [ 'exclude' => true, @@ -68,7 +68,7 @@ 'type' => 'text', 'cols' => 60, 'rows' => 5, - ] + ], ], ], ]; diff --git a/ext_emconf.php b/ext_emconf.php index fe11527..c8c66f4 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,4 +1,5 @@ 'Frontend User Import', 'description' => 'Imports Excel or CSV data as Frontend Users', diff --git a/ext_localconf.php b/ext_localconf.php index 3ce9959..23c7e0b 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,12 +1,12 @@ Date: Fri, 15 Aug 2025 18:34:33 +0200 Subject: [PATCH 06/12] fix: Replace viewhelper and make the BE work --- Classes/ViewHelpers/Form/UploadViewHelper.php | 110 ------------------ .../Backend/Templates/Userimport/Main.html | 4 +- ext_localconf.php | 2 +- 3 files changed, 2 insertions(+), 114 deletions(-) delete mode 100644 Classes/ViewHelpers/Form/UploadViewHelper.php diff --git a/Classes/ViewHelpers/Form/UploadViewHelper.php b/Classes/ViewHelpers/Form/UploadViewHelper.php deleted file mode 100644 index 96da7e1..0000000 --- a/Classes/ViewHelpers/Form/UploadViewHelper.php +++ /dev/null @@ -1,110 +0,0 @@ - - * All rights reserved - * - * This script is part of the TYPO3 project. The TYPO3 project is - * free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * The GNU General Public License can be found at - * http://www.gnu.org/copyleft/gpl.html. - * A copy is found in the text file GPL.txt and important notices to the license - * from the author is found in LICENSE.txt distributed with these scripts. - * - * - * This script is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * This copyright notice MUST APPEAR in all copies of the script! - ***************************************************************/ -/** - * Class UploadViewHelper - */ -class UploadViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\UploadViewHelper -{ - /** - * @var \TYPO3\CMS\Core\Crypto\HashService - */ - protected $hashService; - - /** - * @var PropertyMapper - */ - protected $propertyMapper; - - /** - * Render the upload field including possible resource pointer - * - * @return string - * @api - */ - public function render() - { - $output = ''; - - $resource = $this->getUploadedResource(); - if ($resource !== null) { - $resourcePointerIdAttribute = ''; - if ($this->hasArgument('id')) { - $resourcePointerIdAttribute = ' id="' . htmlspecialchars($this->additionalArguments['id']) . '-file-reference"'; - } - $resourcePointerValue = $resource->getUid(); - if ($resourcePointerValue === null) { - // Newly created file reference which is not persisted yet. - // Use the file UID instead, but prefix it with "file:" to communicate this to the type converter - $resourcePointerValue = 'file:' . $resource->getOriginalResource()->getOriginalFile()->getUid(); - } - $output .= ''; - - $this->templateVariableContainer->add('resource', $resource); - $output .= $this->renderChildren(); - $this->templateVariableContainer->remove('resource'); - } - return $output . parent::render(); - } - - /** - * Return a previously uploaded resource. - * Return NULL if errors occurred during property mapping for this property. - * - * @return FileReference - */ - protected function getUploadedResource() - { - if ($this->getMappingResultsForProperty()->hasErrors()) { - return null; - } - if (is_callable([$this, 'getValueAttribute'])) { - $resource = $this->getValueAttribute(); - } else { - // @deprecated since 7.6 will be removed once 6.2 support is removed - $resource = $this->getValue(false); - } - if ($resource instanceof FileReference) { - return $resource; - } - return $this->propertyMapper->convert($resource, 'TYPO3\\CMS\\Extbase\\Domain\\Model\\FileReference'); - } - - public function injectHashService(\TYPO3\CMS\Core\Crypto\HashService $hashService): void - { - $this->hashService = $hashService; - } - - public function injectPropertyMapper(PropertyMapper $propertyMapper): void - { - $this->propertyMapper = $propertyMapper; - } -} diff --git a/Resources/Private/Backend/Templates/Userimport/Main.html b/Resources/Private/Backend/Templates/Userimport/Main.html index 43ff6f1..f1a193c 100644 --- a/Resources/Private/Backend/Templates/Userimport/Main.html +++ b/Resources/Private/Backend/Templates/Userimport/Main.html @@ -1,5 +1,3 @@ -{namespace userimport=Visol\Userimport\ViewHelpers} - @@ -11,7 +9,7 @@

{f:translate(key: 'uploadStorageFolder.label')}

- + {f:translate(key: 'uploadFile.help')}
diff --git a/ext_localconf.php b/ext_localconf.php index 23c7e0b..26f3365 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -7,7 +7,7 @@ defined('TYPO3') || die('Access denied.'); (function () { - ExtensionUtility::registerTypeConverter(UploadedFileReferenceConverter::class); + // ExtensionUtility::registerTypeConverter(UploadedFileReferenceConverter::class); ExtensionManagementUtility::addTypoScriptSetup( '@import \'EXT:userimport/Configuration/TypoScript/setup.typoscript\'' From a87b56aa32d38d766cafcd214be0f69bf0845159 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 22:25:56 +0200 Subject: [PATCH 07/12] fix: work on v13 --- Classes/Controller/UserimportController.php | 133 ++++----- Classes/Domain/Model/ImportJob.php | 15 +- .../UploadedFileReferenceConverter.php | 262 ------------------ Classes/Service/SpreadsheetService.php | 6 +- Classes/Service/TcaService.php | 19 +- Configuration/Backend/Modules.php | 4 +- .../Private/Backend/Layouts/Default.html | 35 --- Resources/Private/Layouts/Default.html | 16 ++ .../Partials/ClientSideValidationResults.html | 0 .../{Backend => }/Partials/FieldMapping.html | 0 .../{Backend => }/Partials/ImportOptions.html | 0 .../{Backend => }/Partials/ImportPreview.html | 0 .../Partials/SpreadsheetPreview.html | 0 .../Partials/ValidationResults.html | 0 .../Default}/Userimport/FieldMapping.html | 0 .../Default}/Userimport/ImportPreview.html | 0 .../Default}/Userimport/Main.html | 0 .../Default}/Userimport/Options.html | 0 .../Default}/Userimport/PerformImport.html | 0 .../Controller/UserimportControllerTest.php | 5 +- ext_localconf.php | 4 - ext_tables.php | 8 - 22 files changed, 95 insertions(+), 412 deletions(-) delete mode 100644 Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php delete mode 100644 Resources/Private/Backend/Layouts/Default.html create mode 100644 Resources/Private/Layouts/Default.html rename Resources/Private/{Backend => }/Partials/ClientSideValidationResults.html (100%) rename Resources/Private/{Backend => }/Partials/FieldMapping.html (100%) rename Resources/Private/{Backend => }/Partials/ImportOptions.html (100%) rename Resources/Private/{Backend => }/Partials/ImportPreview.html (100%) rename Resources/Private/{Backend => }/Partials/SpreadsheetPreview.html (100%) rename Resources/Private/{Backend => }/Partials/ValidationResults.html (100%) rename Resources/Private/{Backend/Templates => Templates/Default}/Userimport/FieldMapping.html (100%) rename Resources/Private/{Backend/Templates => Templates/Default}/Userimport/ImportPreview.html (100%) rename Resources/Private/{Backend/Templates => Templates/Default}/Userimport/Main.html (100%) rename Resources/Private/{Backend/Templates => Templates/Default}/Userimport/Options.html (100%) rename Resources/Private/{Backend/Templates => Templates/Default}/Userimport/PerformImport.html (100%) delete mode 100644 ext_tables.php diff --git a/Classes/Controller/UserimportController.php b/Classes/Controller/UserimportController.php index c15f28c..0dfafb3 100644 --- a/Classes/Controller/UserimportController.php +++ b/Classes/Controller/UserimportController.php @@ -1,7 +1,5 @@ view = $this->moduleTemplateFactory->create($this->request); - $extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class); - $moduleConfiguration = $extensionConfiguration->get('userimport'); + $moduleConfiguration = $this->extensionConfiguration->get('userimport'); - if (!empty($moduleConfiguration['uploadStorageFolder'])) { + if ($moduleConfiguration['uploadStorageFolder'] !== '') { $this->view->assign('uploadStorageFolder', $moduleConfiguration['uploadStorageFolder']); } - $this->view->assign('importJob', $importJob); - return $this->htmlResponse(); + $this->view->assign('importJob', $this->importJob); + return $this->view->renderResponse('Userimport/Main'); } protected function initializeUploadAction() { - /** @var PropertyMappingConfiguration $propertyMappingConfiguration */ - $propertyMappingConfiguration = $this->arguments['importJob']->getPropertyMappingConfiguration(); - $uploadConfiguration = [ - UploadedFileReferenceConverter::CONFIGURATION_ALLOWED_FILE_EXTENSIONS => 'xlsx,csv', - ]; - $propertyMappingConfiguration->allowProperties('file'); - $propertyMappingConfiguration->forProperty('file') - ->setTypeConverterOptions( - UploadedFileReferenceConverter::class, - $uploadConfiguration - ); + // As Validators can contain state, do not inject them + $mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class); + $mimeTypeValidator->setOptions([ + 'allowedMimeTypes' => ['text/csv', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], + 'ignoreFileExtensionCheck' => false, + 'notAllowedMessage' => 'Not allowed file type', + 'invalidExtensionMessage' => 'Invalid file extension', + ]); + + $moduleConfiguration = $this->extensionConfiguration->get('userimport'); + + $fileHandlingServiceConfiguration = $this->arguments->getArgument('importJob')->getFileHandlingServiceConfiguration(); + $fileHandlingServiceConfiguration->addFileUploadConfiguration( + (new FileUploadConfiguration('file')) + ->setRequired() + ->addValidator($mimeTypeValidator) + ->setMaxFiles(1) + ->setUploadFolder($moduleConfiguration['uploadStorageFolder']), + ); + + // Extbase's property mapping is not handling FileUploads, so it must not operate on this property. + // When using the FileUpload attribute/annotation, this internally does the same. This is covered + // by the `addFileUploadConfiguration()` functionality. + $this->arguments->getArgument('importJob')->getPropertyMappingConfiguration()->skipProperties('file'); } /** @@ -99,6 +97,7 @@ public function uploadAction(ImportJob $importJob) public function optionsAction(ImportJob $importJob): ResponseInterface { + $this->view = $this->moduleTemplateFactory->create($this->request); $this->view->assign('importJob', $importJob); if ($importJob->getFile() instanceof FileReference) { @@ -110,11 +109,12 @@ public function optionsAction(ImportJob $importJob): ResponseInterface $this->view->assign('frontendUserFolders', $this->tcaService->getFrontendUserFolders()); $this->view->assign('frontendUserGroups', $this->tcaService->getFrontendUserGroups()); $this->view->assign('frontendUserTableFieldNames', $this->tcaService->getFrontendUserTableUniqueFieldNames()); - return $this->htmlResponse(); + return $this->view->renderResponse('Userimport/Options'); } public function fieldMappingAction(ImportJob $importJob): ResponseInterface { + $this->view = $this->moduleTemplateFactory->create($this->request); $this->view->assign('importJob', $importJob); // Update ImportJob with options @@ -153,11 +153,12 @@ public function fieldMappingAction(ImportJob $importJob): ResponseInterface // If username is generated from e-mail, the field e-mail must be mapped $emailMustBeMapped = (bool) $importJob->getImportOption(ImportJob::IMPORT_OPTION_USE_EMAIL_AS_USERNAME); $this->view->assign('emailMustBeMapped', $emailMustBeMapped); - return $this->htmlResponse(); + return $this->view->renderResponse('Userimport/FieldMapping'); } public function importPreviewAction(ImportJob $importJob, array $fieldMapping): ResponseInterface { + $this->view = $this->moduleTemplateFactory->create($this->request); $this->view->assign('importJob', $importJob); // Update ImportJob with field mapping @@ -168,11 +169,12 @@ public function importPreviewAction(ImportJob $importJob, array $fieldMapping): $previewData = $this->spreadsheetService->generateDataFromImportJob($importJob, true); $this->view->assign('previewDataHeader', array_keys($previewData[0])); $this->view->assign('previewData', $previewData); - return $this->htmlResponse(); + return $this->view->renderResponse('Userimport/ImportPreview'); } public function performImportAction(ImportJob $importJob): ResponseInterface { + $this->view = $this->moduleTemplateFactory->create($this->request); $rowsToImport = $this->spreadsheetService->generateDataFromImportJob($importJob); $this->view->assign('rowsInSource', count($rowsToImport)); @@ -187,7 +189,7 @@ public function performImportAction(ImportJob $importJob): ResponseInterface // Remove import job $this->importJobRepository->remove($importJob); $this->persistenceManager->persistAll(); - return $this->htmlResponse(); + return $this->view->renderResponse('Userimport/PerformImport'); } /** @@ -197,29 +199,4 @@ public function getErrorFlashMessage(): bool|string { return false; } - - public function injectImportJobRepository(ImportJobRepository $importJobRepository): void - { - $this->importJobRepository = $importJobRepository; - } - - public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void - { - $this->persistenceManager = $persistenceManager; - } - - public function injectSpreadsheetService(SpreadsheetService $spreadsheetService): void - { - $this->spreadsheetService = $spreadsheetService; - } - - public function injectUserImportService(UserImportService $userImportService): void - { - $this->userImportService = $userImportService; - } - - public function injectTcaService(TcaService $tcaService): void - { - $this->tcaService = $tcaService; - } } diff --git a/Classes/Domain/Model/ImportJob.php b/Classes/Domain/Model/ImportJob.php index a1fbd56..5f38703 100644 --- a/Classes/Domain/Model/ImportJob.php +++ b/Classes/Domain/Model/ImportJob.php @@ -25,10 +25,7 @@ class ImportJob extends AbstractEntity const IMPORT_OPTION_UPDATE_EXISTING_USERS = 'updateExistingUsers'; const IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD = 'updateExistingUsersUniqueField'; - /** - * @var FileReference - */ - protected $file; + protected ?FileReference $file = null; /** * @var string @@ -41,18 +38,12 @@ class ImportJob extends AbstractEntity */ protected $fieldMapping; - /** - * @return FileReference $file - */ - public function getFile() + public function getFile(): ?FileReference { return $this->file; } - /** - * Sets the image - */ - public function setFile(FileReference $file): void + public function setFile(?FileReference $file): void { $this->file = $file; } diff --git a/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php b/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php deleted file mode 100644 index 7cc6aaf..0000000 --- a/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php +++ /dev/null @@ -1,262 +0,0 @@ - - */ - protected $sourceTypes = ['array']; - - /** - * @var string - */ - protected $targetType = FileReference::class; - - /** - * Take precedence over the available FileReferenceConverter - * - * @var int - */ - protected $priority = 30; - - /** - * @var ResourceFactory - */ - protected $resourceFactory; - - /** - * @var HashService - */ - protected $hashService; - - /** - * @var PersistenceManager - */ - protected $persistenceManager; - - /** - * @var FileInterface[] - */ - protected $convertedResources = []; - - /** - * Actually convert from $source to $targetType, taking into account the fully - * built $convertedChildProperties and $configuration. - * - * @param string|int $source - * @param string $targetType - * @throws Exception - * @return AbstractFileFolder - */ - public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) - { - if (!isset($source['error']) || $source['error'] === UPLOAD_ERR_NO_FILE) { - if (isset($source['submittedFile']['resourcePointer'])) { - try { - $resourcePointer = $this->hashService->validateAndStripHmac($source['submittedFile']['resourcePointer']); - if (strpos($resourcePointer, 'file:') === 0) { - $fileUid = substr($resourcePointer, 5); - return $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid)); - } - return $this->createFileReferenceFromFalFileReferenceObject($this->resourceFactory->getFileReferenceObject($resourcePointer), $resourcePointer); - } catch (InvalidArgumentException $e) { - // Nothing to do. No file is uploaded and resource pointer is invalid. Discard! - } - } - return null; - } - - if ($source['error'] !== UPLOAD_ERR_OK) { - switch ($source['error']) { - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - case UPLOAD_ERR_PARTIAL: - return new Error('Error Code: ' . $source['error'], 1264440823); - default: - return new Error('An error occurred while uploading. Please try again or contact the administrator if the problem remains', 1340193849); - } - } - - if (isset($this->convertedResources[$source['tmp_name']])) { - return $this->convertedResources[$source['tmp_name']]; - } - - try { - $resource = $this->importUploadedResource($source, $configuration); - } catch (\Exception $e) { - return new Error($e->getMessage(), $e->getCode()); - } - - $this->convertedResources[$source['tmp_name']] = $resource; - return $resource; - } - - /** - * @param null|int $resourcePointer - * @return FileReference - */ - protected function createFileReferenceFromFalFileObject(FalFile $file, $resourcePointer = null) - { - $fileReference = $this->resourceFactory->createFileReferenceObject( - [ - 'uid_local' => $file->getUid(), - 'uid_foreign' => uniqid('NEW_'), - 'uid' => uniqid('NEW_'), - 'crop' => null, - ] - ); - return $this->createFileReferenceFromFalFileReferenceObject($fileReference, $resourcePointer); - } - - /** - * Import a resource and respect configuration given for properties - * - * @return FileReference - * @throws TypeConverterException - * @throws ExistingTargetFileNameException - */ - protected function importUploadedResource(array $uploadInfo, PropertyMappingConfigurationInterface $configuration) - { - if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($uploadInfo['name'])) { - throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1399312430); - } - - $allowedFileExtensions = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_ALLOWED_FILE_EXTENSIONS); - - if ($allowedFileExtensions !== null) { - $filePathInfo = PathUtility::pathinfo($uploadInfo['name']); - if (!GeneralUtility::inList($allowedFileExtensions, strtolower($filePathInfo['extension']))) { - throw new TypeConverterException('File extension is not allowed!', 1399312430); - } - } - - $extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class); - $moduleConfiguration = $extensionConfiguration->get('userimport'); - - if (!empty($moduleConfiguration['uploadStorageFolder'])) { - $uploadStorageFolder = $moduleConfiguration['uploadStorageFolder']; - } else { - throw new \Exception('You must configure an upload folder in the Extension Manager configuration.', 1643747930); - } - - $uploadFolderId = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $uploadStorageFolder; - $defaultConflictMode = DuplicationBehavior::RENAME; - $conflictMode = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $defaultConflictMode; - - $uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId); - $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode); - - $resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) && strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === false - ? $this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer']) - : null; - - return $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer); - } - - /** - * @param null|int $resourcePointer - * @return FileReference - */ - protected function createFileReferenceFromFalFileReferenceObject(FalFileReference $falFileReference, $resourcePointer = null) - { - if ($resourcePointer === null) { - /** @var FileReference $fileReference */ - $fileReference = $this->objectManager->get(FileReference::class); - } else { - $fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, FileReference::class, false); - } - - $fileReference->setOriginalResource($falFileReference); - - return $fileReference; - } - - public function injectResourceFactory(ResourceFactory $resourceFactory): void - { - $this->resourceFactory = $resourceFactory; - } - - public function injectHashService(HashService $hashService): void - { - $this->hashService = $hashService; - } - - public function injectPersistenceManager(PersistenceManager $persistenceManager): void - { - $this->persistenceManager = $persistenceManager; - } -} diff --git a/Classes/Service/SpreadsheetService.php b/Classes/Service/SpreadsheetService.php index 581f111..4d1add3 100644 --- a/Classes/Service/SpreadsheetService.php +++ b/Classes/Service/SpreadsheetService.php @@ -1,7 +1,5 @@ $fieldName) { - $value = $worksheet->getCellByColumnAndRow(Coordinate::columnIndexFromString($columnIndex), $rowIndex)->getValue(); + $value = $worksheet->getCell(Coordinate::indexesFromString($columnIndex . $rowIndex))->getValue(); $row[$fieldName] = empty($value) ? '' : $value; } diff --git a/Classes/Service/TcaService.php b/Classes/Service/TcaService.php index 17b1b07..e0e5e23 100644 --- a/Classes/Service/TcaService.php +++ b/Classes/Service/TcaService.php @@ -6,7 +6,6 @@ use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; -use TYPO3\CMS\Core\Database\QueryGenerator; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -23,6 +22,11 @@ class TcaService implements SingletonInterface { + public function __construct( + private readonly ConnectionPool $connectionPool, + ) { + } + /** * Return all pages of type folder containing frontend users * @@ -102,15 +106,18 @@ public function getFrontendUserTableUniqueFieldNames() */ public function getFrontendUserTableFieldNames() { - /** @var QueryGenerator $queryGenerator */ - $queryGenerator = GeneralUtility::makeInstance(QueryGenerator::class); - $queryGenerator->table = 'fe_users'; + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('fe_users'); + $result = $queryBuilder + ->select('*') + ->from('fe_users') + ->executeQuery()->fetchAssociative(); + + $fieldList = array_keys($result); $fieldsToExclude = ['image', 'TSconfig', 'lastlogin', 'felogin_forgotHash', 'uid', 'pid', 'deleted', 'tstamp', 'crdate', 'cruser_id']; - $fieldList = $queryGenerator->makeFieldList(); $fieldArray = []; - foreach (GeneralUtility::trimExplode(',', $fieldList) as $fieldName) { + foreach ($fieldList as $fieldName) { if (in_array($fieldName, $fieldsToExclude)) { // Ignore senseless or dangerous fields continue; diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php index 9c77a9a..4168f20 100644 --- a/Configuration/Backend/Modules.php +++ b/Configuration/Backend/Modules.php @@ -1,5 +1,7 @@ [ 'parent' => 'web', @@ -7,7 +9,7 @@ 'labels' => 'LLL:EXT:userimport/Resources/Private/Language/locallang_userimport.xlf', 'extensionName' => 'Userimport', 'controllerActions' => [ - \Visol\Userimport\Controller\UserimportController::class => [ + UserimportController::class => [ 'main', 'upload', 'options', diff --git a/Resources/Private/Backend/Layouts/Default.html b/Resources/Private/Backend/Layouts/Default.html deleted file mode 100644 index 8174273..0000000 --- a/Resources/Private/Backend/Layouts/Default.html +++ /dev/null @@ -1,35 +0,0 @@ - -
-
-
-
- - - - - - - - -
-
-
-
-
-
- -
-
- -
-
-
-
-
-

{f:translate(key: 'moduleTitle')}

- - -
-
-
-
diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html new file mode 100644 index 0000000..8e94ebc --- /dev/null +++ b/Resources/Private/Layouts/Default.html @@ -0,0 +1,16 @@ + + + +
+
+
+

{f:translate(key: 'moduleTitle')}

+ + +
+
+
+ diff --git a/Resources/Private/Backend/Partials/ClientSideValidationResults.html b/Resources/Private/Partials/ClientSideValidationResults.html similarity index 100% rename from Resources/Private/Backend/Partials/ClientSideValidationResults.html rename to Resources/Private/Partials/ClientSideValidationResults.html diff --git a/Resources/Private/Backend/Partials/FieldMapping.html b/Resources/Private/Partials/FieldMapping.html similarity index 100% rename from Resources/Private/Backend/Partials/FieldMapping.html rename to Resources/Private/Partials/FieldMapping.html diff --git a/Resources/Private/Backend/Partials/ImportOptions.html b/Resources/Private/Partials/ImportOptions.html similarity index 100% rename from Resources/Private/Backend/Partials/ImportOptions.html rename to Resources/Private/Partials/ImportOptions.html diff --git a/Resources/Private/Backend/Partials/ImportPreview.html b/Resources/Private/Partials/ImportPreview.html similarity index 100% rename from Resources/Private/Backend/Partials/ImportPreview.html rename to Resources/Private/Partials/ImportPreview.html diff --git a/Resources/Private/Backend/Partials/SpreadsheetPreview.html b/Resources/Private/Partials/SpreadsheetPreview.html similarity index 100% rename from Resources/Private/Backend/Partials/SpreadsheetPreview.html rename to Resources/Private/Partials/SpreadsheetPreview.html diff --git a/Resources/Private/Backend/Partials/ValidationResults.html b/Resources/Private/Partials/ValidationResults.html similarity index 100% rename from Resources/Private/Backend/Partials/ValidationResults.html rename to Resources/Private/Partials/ValidationResults.html diff --git a/Resources/Private/Backend/Templates/Userimport/FieldMapping.html b/Resources/Private/Templates/Default/Userimport/FieldMapping.html similarity index 100% rename from Resources/Private/Backend/Templates/Userimport/FieldMapping.html rename to Resources/Private/Templates/Default/Userimport/FieldMapping.html diff --git a/Resources/Private/Backend/Templates/Userimport/ImportPreview.html b/Resources/Private/Templates/Default/Userimport/ImportPreview.html similarity index 100% rename from Resources/Private/Backend/Templates/Userimport/ImportPreview.html rename to Resources/Private/Templates/Default/Userimport/ImportPreview.html diff --git a/Resources/Private/Backend/Templates/Userimport/Main.html b/Resources/Private/Templates/Default/Userimport/Main.html similarity index 100% rename from Resources/Private/Backend/Templates/Userimport/Main.html rename to Resources/Private/Templates/Default/Userimport/Main.html diff --git a/Resources/Private/Backend/Templates/Userimport/Options.html b/Resources/Private/Templates/Default/Userimport/Options.html similarity index 100% rename from Resources/Private/Backend/Templates/Userimport/Options.html rename to Resources/Private/Templates/Default/Userimport/Options.html diff --git a/Resources/Private/Backend/Templates/Userimport/PerformImport.html b/Resources/Private/Templates/Default/Userimport/PerformImport.html similarity index 100% rename from Resources/Private/Backend/Templates/Userimport/PerformImport.html rename to Resources/Private/Templates/Default/Userimport/PerformImport.html diff --git a/Tests/Unit/Controller/UserimportControllerTest.php b/Tests/Unit/Controller/UserimportControllerTest.php index 93e73d8..6581deb 100644 --- a/Tests/Unit/Controller/UserimportControllerTest.php +++ b/Tests/Unit/Controller/UserimportControllerTest.php @@ -1,12 +1,12 @@ */ class UserimportControllerTest extends UnitTestCase { @@ -28,5 +28,4 @@ protected function tearDown() { parent::tearDown(); } - } diff --git a/ext_localconf.php b/ext_localconf.php index 26f3365..ff97a8b 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,14 +1,10 @@ Date: Fri, 15 Aug 2025 22:33:41 +0200 Subject: [PATCH 08/12] feat: Add minimal tests --- .github/workflows/ci.yaml | 94 +++++++++++++++++++++++++++++++++++++++ composer.json | 7 +++ ecs.php | 53 ++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 ecs.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7308a41 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + check-composer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + tools: composer:v2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate composer.json + run: composer validate + + php-linting: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.3 + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php-version }}" + coverage: none + + - name: PHP lint + run: "find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l" + + xml-linting: + runs-on: ubuntu-latest + needs: + - check-composer + steps: + - uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + tools: composer:v2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install xmllint + run: sudo apt update && sudo apt-get install libxml2-utils + + - name: Install dependencies + run: composer install --no-progress --no-interaction --optimize-autoloader + + - name: Fetch schema for xliff + run: wget https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=.Build/xliff-core-1.2-strict.xsd + + - name: TYPO3 language files + run: xmllint --schema .Build/xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf') + + coding-guideline: + runs-on: ubuntu-latest + needs: + - check-composer + steps: + - uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + tools: composer:v2 + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: composer install --no-progress --no-interaction --optimize-autoloader + + - name: Coding Guideline + run: ./vendor/bin/ecs check diff --git a/composer.json b/composer.json index 8dcee60..2695b38 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,16 @@ } }, "require": { + "php": "<= 8.3", "typo3/cms-core": "^13.4", "phpoffice/phpspreadsheet": "^4" }, + "config": { + "allow-plugins": { + "typo3/cms-composer-installers": true, + "typo3/class-alias-loader": true + } + }, "autoload": { "psr-4": { "Visol\\Userimport\\": "Classes" diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..2defbd0 --- /dev/null +++ b/ecs.php @@ -0,0 +1,53 @@ +sets([ + SetList::PSR_12, + SetList::PHPUNIT, + ]); + $ecsConfig->paths([ + __DIR__, + ]); + $ecsConfig->skip([ + __DIR__ . '/.Build/', + __DIR__ . '/vendor/', + + // Rules + DeclareStrictTypesFixer::class, + HeaderCommentFixer::class => [ + __DIR__ . '/Configuration/*', + ], + PSR2ControlStructureSpacingSniff::class, + FunctionsFunctionCallSignatureSniff::class . '.SpaceAfterCloseBracket', + MethodsFunctionCallSignatureSniff::class . '.SpaceAfterCloseBracket', + ]); +}; From 15f8f31949e2f53cefaa896362a2f67ed6f5078b Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 22:49:12 +0200 Subject: [PATCH 09/12] fix: phpstan error --- Classes/Domain/Model/ImportJob.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Model/ImportJob.php b/Classes/Domain/Model/ImportJob.php index 5f38703..b36f8d0 100644 --- a/Classes/Domain/Model/ImportJob.php +++ b/Classes/Domain/Model/ImportJob.php @@ -2,6 +2,7 @@ namespace Visol\Userimport\Domain\Model; +use TYPO3\CMS\Extbase\Annotation\ORM\Transient; use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; @@ -30,7 +31,7 @@ class ImportJob extends AbstractEntity /** * @var string */ - #[TYPO3\CMS\Extbase\Annotation\ORM\Transient] + #[Transient] protected $importOptions; /** From 3e1e714debbabd9ebffb4540c9be719f27e1a431 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 15 Aug 2025 22:51:01 +0200 Subject: [PATCH 10/12] fixup! feat: Add minimal tests --- .github/workflows/ci.yaml | 3 + Classes/Domain/Model/ImportJob.php | 14 +- Resources/Private/Language/de.locallang.xlf | 145 ++++++++++++------ .../Language/de.locallang_userimport.xlf | 12 +- Resources/Private/Language/locallang.xlf | 98 ++++++------ .../Private/Language/locallang_userimport.xlf | 12 +- composer.json | 5 +- ecs.php | 1 + 8 files changed, 172 insertions(+), 118 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7308a41..d840afa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,6 +65,9 @@ jobs: - name: Install dependencies run: composer install --no-progress --no-interaction --optimize-autoloader + - name: Fetch schema for xliff + run: mkdir .Build + - name: Fetch schema for xliff run: wget https://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd --output-document=.Build/xliff-core-1.2-strict.xsd diff --git a/Classes/Domain/Model/ImportJob.php b/Classes/Domain/Model/ImportJob.php index b36f8d0..4c07c8d 100644 --- a/Classes/Domain/Model/ImportJob.php +++ b/Classes/Domain/Model/ImportJob.php @@ -18,13 +18,13 @@ ***/ class ImportJob extends AbstractEntity { - const IMPORT_OPTION_TARGET_FOLDER = 'targetFolder'; - const IMPORT_OPTION_FIRST_ROW_CONTAINS_FIELD_NAMES = 'firstRowContainsFieldNames'; - const IMPORT_OPTION_USE_EMAIL_AS_USERNAME = 'useEmailAsUsername'; - const IMPORT_OPTION_GENERATE_PASSWORD = 'generatePassword'; - const IMPORT_OPTION_USER_GROUPS = 'userGroups'; - const IMPORT_OPTION_UPDATE_EXISTING_USERS = 'updateExistingUsers'; - const IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD = 'updateExistingUsersUniqueField'; + public const IMPORT_OPTION_TARGET_FOLDER = 'targetFolder'; + public const IMPORT_OPTION_FIRST_ROW_CONTAINS_FIELD_NAMES = 'firstRowContainsFieldNames'; + public const IMPORT_OPTION_USE_EMAIL_AS_USERNAME = 'useEmailAsUsername'; + public const IMPORT_OPTION_GENERATE_PASSWORD = 'generatePassword'; + public const IMPORT_OPTION_USER_GROUPS = 'userGroups'; + public const IMPORT_OPTION_UPDATE_EXISTING_USERS = 'updateExistingUsers'; + public const IMPORT_OPTION_UPDATE_EXISTING_USERS_UNIQUE_FIELD = 'updateExistingUsersUniqueField'; protected ?FileReference $file = null; diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index 0869286..b8adf90 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -1,141 +1,186 @@ - - - + + +
- + + User Import Benutzer-Import - + + Select file + Datei auswählen + + + Upload folder Upload-Verzeichnis - + + You must configure an upload folder in the Extension Manager configuration. See README for more details. Bitte Upload-Verzeichnis im Extension Manager konfigurieren. Siehe README für mehr Details. - + + (See extension manager) (siehe Extension Manager) - - Datei auswählen - - + + Allowed file extensions are: xlsx, csv Erlaubte Datei-Typen sind: xlsx, csv - + + Upload Hochladen - + + Preview (File: %s) Vorschau (Datei: %s) - + + Only the first 5 rows are displayed. Es werden nur die ersten 5 Zeilen dargestellt. - + + Import options Import-Optionen - + + First row contains field names Erste Zeile enthält Spaltennamen - + + If selected, the first row is ignored when importing. Falls gewählt, wird die erste Zeile beim Import ignoriert. - + + Target folder Ziel-Ordner - + + User groups Benutzergruppen - + + The selected user groups are assigned to the created users. Die ausgewählten Benutzergruppen werden den importierten Benutzern zugewiesen. - + + Use e-mail column as username E-Mail-Spalte als Benutzername verwenden - + + The value of the e-mail column is used as username. Der Wert der Spalte E-Mail wird als Benutzername verwendet. - + + Generate password Kennwort generieren - + + A random password is generated for imported users. Alternatively, you can import a password column. Ein zufälliges Kennwort wird für importierte Benutzer generiert. Alternativ kann das Kennwort auch importiert werden. - + + Update existing users Bestehende Benutzer aktualisieren - + + Update existing users instead of adding new users. Anstatt neue Benutzer hinzuzufügen, werden bestehende Benutzer aktualisiert. - + + Field to check for update Feld für Aktualisierungs-Prüfung - + + If a record having the same value in the selected field is found, it is updated instead of inserted. Ein Benutzer wird dann aktualisiert, wenn bereits ein Benutzer mit demselben Wert im angegebenen Feld existiert. - + + Continue Weiter - + + Field Mapping Feldzuweisung - + + Column Spalte - + + Field mapping Feldzuweisung - + + Examples Beispiele - + + Map to field... Zuweisung zu Feld... - + + The username field must be mapped. Das Feld username muss zugewiesen werden. - + + The e-mail field must be mapped because the username is generated from it. Das Feld email muss zugewiesen werden, da daraus der Benutzername generiert wird. - + + Continue Weiter - + + Import Preview Import-Vorschau - + + Import data Daten importieren - + + Import log Import-Log - + + %s rows in source file. %s Zeilen in der hochgeladenen Datei. - + + %s records inserted. %s Datensätze importiert. - + + %s records updated. %s Datensätze aktualisiert. - + + File extension not allowed. Allowed file types: xlsx, csv Datei-Typ nicht erlaubt. Erlaubte Datei-Typen: xlsx, csv - + + Update failed for: %s Aktualisierung fehlgeschlagen für: %s - + + Update failed for: %s (Reason: More than one identical record to update was found.) Aktualisierung fehlgeschlagen für: %s (Grund: Mehr als ein identischer Datensatz für die Aktualisierung gefunden.) - + + Update successful for: %s Aktualisierung erfolgreich für: %s - + + Insert failed for: %s Import fehlgeschlagen für: %s - + + Insert successful for: %s Import erfolgreich für: %s - + + Go to target folder Zum Zielordner wechseln diff --git a/Resources/Private/Language/de.locallang_userimport.xlf b/Resources/Private/Language/de.locallang_userimport.xlf index 3f34b7d..188ccf7 100644 --- a/Resources/Private/Language/de.locallang_userimport.xlf +++ b/Resources/Private/Language/de.locallang_userimport.xlf @@ -1,12 +1,14 @@ - - - + + +
- + + User Import Benutzer-Import - + + Import Excel or CSV data as Frontend Users Import von Excel- oder CSV-Dateien als Website-Benutzer diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 3134d3d..82ef3dc 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -1,141 +1,141 @@ - - - + + +
- + User Import - + Select file - + Upload folder - + You must configure an upload folder in the Extension Manager configuration. See README for more details. - - (See extension manager) + + (See extension manager) - + Allowed file extensions are: xlsx, csv - + Upload - + Preview (File: %s) - + Only the first 5 rows are displayed. - + Import options - + First row contains field names - + If selected, the first row is ignored when importing. - + Target folder - + User groups - + The selected user groups are assigned to the created users. - + Use e-mail column as username - + The value of the e-mail column is used as username. - + Generate password - + A random password is generated for imported users. Alternatively, you can import a password column. - + Update existing users - + Update existing users instead of adding new users. - + Field to check for update - + If a record having the same value in the selected field is found, it is updated instead of inserted. - + Continue - + Field Mapping - + Column - + Field mapping - + Examples - + Map to field... - + The username field must be mapped. - + The e-mail field must be mapped because the username is generated from it. - + Continue - + Import Preview - + Import data - + Import log - + %s rows in source file. - + %s records inserted. - + %s records updated. - + File extension not allowed. Allowed file types: xlsx, csv - + Update failed for: %s - + Update failed for: %s (Reason: More than one identical record to update was found.) - + Update successful for: %s - + Insert failed for: %s - + Insert successful for: %s - + Go to target folder diff --git a/Resources/Private/Language/locallang_userimport.xlf b/Resources/Private/Language/locallang_userimport.xlf index 4880c0a..6bc36c8 100644 --- a/Resources/Private/Language/locallang_userimport.xlf +++ b/Resources/Private/Language/locallang_userimport.xlf @@ -1,14 +1,14 @@ - - - + + +
- + User Import - + Import Excel or CSV data as Frontend Users - \ No newline at end of file + diff --git a/composer.json b/composer.json index 2695b38..e540890 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,13 @@ } }, "require": { - "php": "<= 8.3", + "php": ">= 8.3", "typo3/cms-core": "^13.4", "phpoffice/phpspreadsheet": "^4" }, + "require-dev": { + "symplify/easy-coding-standard": "^12.4" + }, "config": { "allow-plugins": { "typo3/cms-composer-installers": true, diff --git a/ecs.php b/ecs.php index 2defbd0..b1dc8f7 100644 --- a/ecs.php +++ b/ecs.php @@ -40,6 +40,7 @@ $ecsConfig->skip([ __DIR__ . '/.Build/', __DIR__ . '/vendor/', + __DIR__ . '/public/', // Rules DeclareStrictTypesFixer::class, From f566568cc4db0c6a54bf89f4f75324ffabdbb3a2 Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Wed, 17 Sep 2025 14:38:05 +0200 Subject: [PATCH 11/12] fix: BE right management with icons (#48089) --- Configuration/Icons.php | 16 +++++++ .../tx_userimport_domain_model_importjob.php | 12 +++--- Resources/Private/Language/locallang_db.xlf | 20 +++++++++ .../tx_userimport_domain_model_importjob.svg | 42 +++++++++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 Configuration/Icons.php create mode 100644 Resources/Private/Language/locallang_db.xlf create mode 100644 Resources/Public/Icons/tx_userimport_domain_model_importjob.svg diff --git a/Configuration/Icons.php b/Configuration/Icons.php new file mode 100644 index 0000000..e4801b7 --- /dev/null +++ b/Configuration/Icons.php @@ -0,0 +1,16 @@ + [ + 'provider' => SvgIconProvider::class, + 'source' => $extIconPath . 'tx_userimport_domain_model_importjob.svg', + ], +]; diff --git a/Configuration/TCA/tx_userimport_domain_model_importjob.php b/Configuration/TCA/tx_userimport_domain_model_importjob.php index 3b39b99..9ab06dd 100644 --- a/Configuration/TCA/tx_userimport_domain_model_importjob.php +++ b/Configuration/TCA/tx_userimport_domain_model_importjob.php @@ -1,15 +1,13 @@ [ 'hideTable' => true, - 'title' => 'LLL:EXT:userimport/Resources/Private/Language/locallang_db.xlf:tx_userimport_domain_model_importjob', + 'title' => $ll . 'tx_userimport_domain_model_importjob', 'label' => 'title', 'tstamp' => 'tstamp', 'crdate' => 'crdate', @@ -18,7 +16,9 @@ 'disabled' => 'hidden', ], 'searchFields' => 'title,style,cached_votes,cached_rank,image,votes,', - 'iconfile' => 'EXT:userimport/Resources/Public/Icons/tx_userimport_domain_model_importjob.gif', + 'typeicon_classes' => [ + 'default' => 'tx_userimport-importjob', + ], ], 'types' => [ '1' => ['showitem' => 'hidden,--palette--;;1,file,import_options,field_mapping'], diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf new file mode 100644 index 0000000..6cdc278 --- /dev/null +++ b/Resources/Private/Language/locallang_db.xlf @@ -0,0 +1,20 @@ + + + +
+ + + User Import + + + File + + + Import options + + + Field mapping + + + + diff --git a/Resources/Public/Icons/tx_userimport_domain_model_importjob.svg b/Resources/Public/Icons/tx_userimport_domain_model_importjob.svg new file mode 100644 index 0000000..04e1a99 --- /dev/null +++ b/Resources/Public/Icons/tx_userimport_domain_model_importjob.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + From a1fff9a2df92c9288194e21d8b951c99c5398ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20L=C3=B6rtscher?= Date: Thu, 18 Sep 2025 16:08:36 +0200 Subject: [PATCH 12/12] chore(README): Update Version Matrix --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 31ac300..27bfa18 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ Make sure to upload your files containing user data to a secure folder! This package is currently maintained for the following versions: -| TYPO3 Version | Package Version | Branch | Maintained | -|-----------------------|-----------------|---------|---------------| -| TYPO3 11.5.x | 2.x | master | Yes | -| TYPO3 8.7.x | 1.x | - | No | +| TYPO3 Version | Package Version | Branch | Maintained | +|---------------|-----------------|--------|------------| +| TYPO3 13.4.x | 3.x | master | Yes | +| TYPO3 11.5.x | 2.x | v11 | No | +| TYPO3 8.7.x | 1.x | - | No | ### Setup a protected storage folder