408 lines
15 KiB
PHP
408 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Form\Domain\Finishers;
|
|
|
|
use Doctrine\DBAL\Exception;
|
|
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
|
|
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
|
use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
|
|
use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
|
|
|
|
/**
|
|
* This finisher saves the data from a submitted form into
|
|
* a database table.
|
|
*
|
|
* Configuration
|
|
* =============
|
|
*
|
|
* options.table (mandatory)
|
|
* -------------
|
|
* Save or update values into this table
|
|
*
|
|
* options.mode (default: insert)
|
|
* ------------
|
|
* Possible values are 'insert' or 'update'.
|
|
*
|
|
* insert: will create a new database row with the values from the
|
|
* submitted form and/or some predefined values.
|
|
* See options.elements and options.databaseFieldMappings
|
|
* update: will update a given database row with the values from the
|
|
* submitted form and/or some predefined values.
|
|
* 'options.whereClause' is then required.
|
|
*
|
|
* options.whereClause
|
|
* -------------------
|
|
* This where clause will be used for a database update action
|
|
*
|
|
* options.elements
|
|
* ----------------
|
|
* Use this to map form element values to existing database columns.
|
|
* Each key within options.elements has to match with a
|
|
* form element identifier within your form definition.
|
|
* The value for each key within options.elements is an array with
|
|
* additional information.
|
|
*
|
|
* options.elements.<elementIdentifier>.mapOnDatabaseColumn (mandatory)
|
|
* --------------------------------------------------------
|
|
* The value from the submitted form element with the identifier
|
|
* '<elementIdentifier>' will be written into this database column
|
|
*
|
|
* options.elements.<elementIdentifier>.skipIfValueIsEmpty (default: false)
|
|
* ------------------------------------------------------
|
|
* Set this to true if the database column should not be written
|
|
* if the value from the submitted form element with the identifier
|
|
* '<elementIdentifier>' is empty (think about password fields etc.)
|
|
*
|
|
* options.elements.<elementIdentifier>.hashed (default: false)
|
|
* ------------------------------------------------------
|
|
* Set this to true if the value from the submitted form element
|
|
* should be hashed before writing into the database.
|
|
*
|
|
* options.elements.<elementIdentifier>.saveFileIdentifierInsteadOfUid (default: false)
|
|
* -------------------------------------------------------------------
|
|
* This setting only rules for form elements which creates a FAL object
|
|
* like FileUpload or ImageUpload.
|
|
* By default, the uid of the FAL object will be written into
|
|
* the database column. Set this to true if you want to store the
|
|
* FAL identifier (1:/user_uploads/some_uploaded_pic.jpg) instead.
|
|
*
|
|
* options.databaseColumnMappings
|
|
* ------------------------------
|
|
* Use this to map database columns to static values (which can be
|
|
* made dynamic through typoscript overrides of course).
|
|
* Each key within options.databaseColumnMappings has to match with a
|
|
* existing database column.
|
|
* The value for each key within options.databaseColumnMappings is an
|
|
* array with additional information.
|
|
*
|
|
* This mapping is done *before* the options.elements mapping.
|
|
* This means if you map a database column to a value through
|
|
* options.databaseColumnMappings and map a submitted form element
|
|
* value to the same database column, the submitted form element value
|
|
* will override the value you set within options.databaseColumnMappings.
|
|
*
|
|
* options.databaseColumnMappings.<databaseColumnName>.value
|
|
* ---------------------------------------------------------
|
|
* The value which will be written to the database column.
|
|
* You can use the FormRuntime accessor feature to access every
|
|
* getable property from the TYPO3\CMS\Form\Domain\Runtime\FormRuntime
|
|
* Read the description within
|
|
* TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher::parseOption
|
|
* In short: use something like {<elementIdentifier>} to get the value
|
|
* from the submitted form element with the identifier
|
|
* <elementIdentifier>
|
|
*
|
|
* Don't be confused. If you use the FormRuntime accessor feature within
|
|
* options.databaseColumnMappings, the functionality is nearly equal
|
|
* to the options.elements configuration.
|
|
*
|
|
* options.databaseColumnMappings.<databaseColumnName>.skipIfValueIsEmpty (default: false)
|
|
* ---------------------------------------------------------------------
|
|
* Set this to true if the database column should not be written
|
|
* if the value from
|
|
* options.databaseColumnMappings.<databaseColumnName>.value is empty.
|
|
*
|
|
* Example
|
|
* =======
|
|
*
|
|
* finishers:
|
|
* -
|
|
* identifier: SaveToDatabase
|
|
* options:
|
|
* table: 'fe_users'
|
|
* mode: update
|
|
* whereClause:
|
|
* uid: 1
|
|
* databaseColumnMappings:
|
|
* pid:
|
|
* value: 1
|
|
* elements:
|
|
* text-1:
|
|
* mapOnDatabaseColumn: 'first_name'
|
|
* text-2:
|
|
* mapOnDatabaseColumn: 'last_name'
|
|
* text-3:
|
|
* mapOnDatabaseColumn: 'username'
|
|
* advancedpassword-1:
|
|
* mapOnDatabaseColumn: 'password'
|
|
* skipIfValueIsEmpty: true
|
|
* hashed: true
|
|
*
|
|
* Multiple database operations
|
|
* ============================
|
|
*
|
|
* You can write options as an array to perform multiple database operations.
|
|
*
|
|
* finishers:
|
|
* -
|
|
* identifier: SaveToDatabase
|
|
* options:
|
|
* 1:
|
|
* table: 'my_table'
|
|
* mode: insert
|
|
* databaseColumnMappings:
|
|
* some_column:
|
|
* value: 'cool'
|
|
* 2:
|
|
* table: 'my_other_table'
|
|
* mode: update
|
|
* whereClause:
|
|
* pid: 1
|
|
* databaseColumnMappings:
|
|
* some_other_column:
|
|
* value: '{SaveToDatabase.insertedUids.1}'
|
|
*
|
|
* This would perform 2 database operations.
|
|
* One insert and one update.
|
|
* You can access the inserted uids with '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'
|
|
* If you perform an insert operation, the value of the inserted database row will be stored
|
|
* within the FinisherVariableProvider.
|
|
* <theArrayKeyNumberWithinOptions> references to the numeric key within options
|
|
* within which the insert operation is executed.
|
|
*
|
|
* Scope: frontend
|
|
*/
|
|
class SaveToDatabaseFinisher extends AbstractFinisher
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $defaultOptions = [
|
|
'table' => null,
|
|
'mode' => 'insert',
|
|
'whereClause' => [],
|
|
'elements' => [],
|
|
'databaseColumnMappings' => [],
|
|
];
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Core\Database\Connection
|
|
*/
|
|
protected $databaseConnection;
|
|
|
|
/**
|
|
* Executes this finisher
|
|
* @see AbstractFinisher::execute()
|
|
*
|
|
* @throws FinisherException
|
|
*/
|
|
protected function executeInternal(): void
|
|
{
|
|
$options = [];
|
|
if (isset($this->options['table'])) {
|
|
$options[] = $this->options;
|
|
} else {
|
|
$options = $this->options;
|
|
}
|
|
|
|
foreach ($options as $optionKey => $option) {
|
|
$this->options = $option;
|
|
$this->process($optionKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare data for saving to database
|
|
*/
|
|
protected function prepareData(array $elementsConfiguration, array $databaseData): array
|
|
{
|
|
foreach ($this->getFormValues() as $elementIdentifier => $elementValue) {
|
|
if (
|
|
($elementValue === null || $elementValue === '')
|
|
&& isset($elementsConfiguration[$elementIdentifier])
|
|
&& isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'])
|
|
&& $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$element = $this->getElementByIdentifier($elementIdentifier);
|
|
if (
|
|
!$element
|
|
|| !isset($elementsConfiguration[$elementIdentifier])
|
|
|| !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn'])
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) {
|
|
$saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'];
|
|
} else {
|
|
$saveFileIdentifierInsteadOfUid = false;
|
|
}
|
|
|
|
if ($elementValue instanceof FileReference) {
|
|
$elementValue = $this->prepareFileForDatabase($elementValue, $saveFileIdentifierInsteadOfUid);
|
|
} elseif ($elementValue instanceof ObjectStorage) {
|
|
$fileIdentifiers = [];
|
|
foreach ($elementValue as $singleElement) {
|
|
if ($singleElement instanceof FileReference) {
|
|
$fileIdentifiers[] = $this->prepareFileForDatabase($singleElement, $saveFileIdentifierInsteadOfUid);
|
|
}
|
|
}
|
|
$elementValue = implode(',', $fileIdentifiers);
|
|
} elseif (is_array($elementValue)) {
|
|
$elementValue = implode(',', $elementValue);
|
|
} elseif ($elementValue instanceof \DateTimeInterface) {
|
|
$format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U';
|
|
$elementValue = $elementValue->format($format);
|
|
} elseif ($elementValue && ($elementsConfiguration[$elementIdentifier]['hashed'] ?? false) === true) {
|
|
$hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE');
|
|
$elementValue = $hashInstance->getHashedPassword($elementValue);
|
|
}
|
|
|
|
$databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
|
|
}
|
|
return $databaseData;
|
|
}
|
|
|
|
/**
|
|
* Perform the current database operation
|
|
* @throws FinisherException
|
|
*/
|
|
protected function process(int $iterationCount): void
|
|
{
|
|
$this->throwExceptionOnInconsistentConfiguration();
|
|
|
|
$table = $this->parseOption('table');
|
|
$table = is_string($table) ? $table : '';
|
|
$elementsConfiguration = $this->parseOption('elements');
|
|
$elementsConfiguration = is_array($elementsConfiguration) ? $elementsConfiguration : [];
|
|
$databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings');
|
|
|
|
$this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
|
|
|
$databaseData = [];
|
|
foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) {
|
|
$value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value');
|
|
if (
|
|
empty($value)
|
|
&& ($databaseColumnConfiguration['skipIfValueIsEmpty'] ?? false) === true
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$databaseData[$databaseColumnName] = $value;
|
|
}
|
|
|
|
$databaseData = $this->prepareData($elementsConfiguration, $databaseData);
|
|
|
|
try {
|
|
$this->saveToDatabase($databaseData, $table, $iterationCount);
|
|
} catch (Exception $e) {
|
|
throw new FinisherException(
|
|
'Failed to save data to database table: ' . $table . '. Error message:' . $e->getMessage(),
|
|
1754050114,
|
|
$e
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save or insert the values from
|
|
* $databaseData into the table $table
|
|
* @throws Exception
|
|
*/
|
|
protected function saveToDatabase(array $databaseData, string $table, int $iterationCount): void
|
|
{
|
|
if (!empty($databaseData)) {
|
|
if ($this->parseOption('mode') === 'update') {
|
|
$whereClause = $this->parseOption('whereClause');
|
|
foreach ($whereClause as $columnName => $columnValue) {
|
|
$whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
|
|
}
|
|
$this->databaseConnection->update(
|
|
$table,
|
|
$databaseData,
|
|
$whereClause
|
|
);
|
|
} else {
|
|
$this->databaseConnection->insert($table, $databaseData);
|
|
try {
|
|
$insertedUid = (int)$this->databaseConnection->lastInsertId();
|
|
} catch (Exception) {
|
|
// Some database tables like sys_category_record_mm may not
|
|
// have an "identity" (uid column). In this case DBAL may
|
|
// throw an exception, which we gracefully handle here.
|
|
$insertedUid = 0;
|
|
}
|
|
$this->finisherContext->getFinisherVariableProvider()->add(
|
|
$this->shortFinisherIdentifier,
|
|
'insertedUids.' . $iterationCount,
|
|
$insertedUid
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws an exception if some inconsistent configuration
|
|
* are detected.
|
|
*
|
|
* @throws FinisherException
|
|
*/
|
|
protected function throwExceptionOnInconsistentConfiguration(): void
|
|
{
|
|
if (
|
|
$this->parseOption('mode') === 'update'
|
|
&& empty($this->parseOption('whereClause'))
|
|
) {
|
|
throw new FinisherException(
|
|
'An empty option "whereClause" is not allowed in update mode.',
|
|
1480469086
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the values of the submitted form
|
|
*/
|
|
protected function getFormValues(): array
|
|
{
|
|
return $this->finisherContext->getFormValues();
|
|
}
|
|
|
|
/**
|
|
* Returns a form element object for a given identifier.
|
|
*
|
|
* @return FormElementInterface|null
|
|
*/
|
|
protected function getElementByIdentifier(string $elementIdentifier): ?FormElementInterface
|
|
{
|
|
return $this
|
|
->finisherContext
|
|
->getFormRuntime()
|
|
->getFormDefinition()
|
|
->getElementByIdentifier($elementIdentifier);
|
|
}
|
|
|
|
protected function prepareFileForDatabase(FileReference $fileReference, bool $saveFileIdentifierInsteadOfUid = false): int|string
|
|
{
|
|
if ($saveFileIdentifierInsteadOfUid) {
|
|
$elementValue = $fileReference->getOriginalResource()->getCombinedIdentifier();
|
|
} else {
|
|
$elementValue = $fileReference->getOriginalResource()->getProperty('uid_local');
|
|
}
|
|
|
|
return $elementValue;
|
|
}
|
|
}
|