![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/vendor/magento/module-import-export/Model/Import/ |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\ImportExport\Model\Import; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\StringUtils; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\ImportFactory; use Magento\ImportExport\Model\ResourceModel\Helper; use Magento\ImportExport\Model\ResourceModel\Import\Data as DataSourceModel; use Magento\Store\Model\ScopeInterface; /** * Import entity abstract model * * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ abstract class AbstractEntity implements EntityInterface { /** * Custom row import behavior column name */ public const COLUMN_ACTION = '_action'; /** * Value in custom column for delete behaviour */ public const COLUMN_ACTION_VALUE_DELETE = 'delete'; /** * Path to bunch size configuration */ public const XML_PATH_BUNCH_SIZE = 'import/format_v2/bunch_size'; /** * Path to page size configuration */ public const XML_PATH_PAGE_SIZE = 'import/format_v2/page_size'; /** * Size of varchar value */ public const DB_MAX_VARCHAR_LENGTH = 256; /** * Size of text value */ public const DB_MAX_TEXT_LENGTH = 65536; public const ERROR_CODE_SYSTEM_EXCEPTION = 'systemException'; public const ERROR_CODE_COLUMN_NOT_FOUND = 'columnNotFound'; public const ERROR_CODE_COLUMN_EMPTY_HEADER = 'columnEmptyHeader'; public const ERROR_CODE_COLUMN_NAME_INVALID = 'columnNameInvalid'; public const ERROR_CODE_ATTRIBUTE_NOT_VALID = 'attributeNotInvalid'; public const ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicateUniqueAttribute'; public const ERROR_CODE_ILLEGAL_CHARACTERS = 'illegalCharacters'; public const ERROR_CODE_INVALID_ATTRIBUTE = 'invalidAttributeName'; public const ERROR_CODE_WRONG_QUOTES = 'wrongQuotes'; public const ERROR_CODE_COLUMNS_NUMBER = 'wrongColumnsNumber'; public const ERROR_EXCEEDED_MAX_LENGTH = 'exceededMaxLength'; public const ERROR_INVALID_ATTRIBUTE_TYPE = 'invalidAttributeType'; public const ERROR_INVALID_ATTRIBUTE_OPTION = 'absentAttributeOption'; /** * @var array */ protected $errorMessageTemplates = [ self::ERROR_CODE_SYSTEM_EXCEPTION => 'General system exception happened', self::ERROR_CODE_COLUMN_NOT_FOUND => 'We can\'t find required columns: %s.', self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'Columns number: "%s" have empty headers', self::ERROR_CODE_COLUMN_NAME_INVALID => 'Column names: "%s" are invalid', self::ERROR_CODE_ATTRIBUTE_NOT_VALID => "Please correct the value for '%s'", self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE => "Duplicate Unique Attribute for '%s'", self::ERROR_CODE_ILLEGAL_CHARACTERS => "Illegal character used for attribute %s", self::ERROR_CODE_INVALID_ATTRIBUTE => 'Header contains invalid attribute(s): "%s"', self::ERROR_CODE_WRONG_QUOTES => "Curly quotes used instead of straight quotes", self::ERROR_CODE_COLUMNS_NUMBER => "Number of columns does not correspond to the number of rows in the header", self::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length', self::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value', self::ERROR_INVALID_ATTRIBUTE_OPTION => "Value for %s attribute contains incorrect value" . ", see acceptable values on settings specified for Admin", ]; /** * @var AdapterInterface */ protected $_connection; /** * Has data process validation done? * * @var bool */ protected $_dataValidated = false; /** * @var array */ protected $validColumnNames = []; /** * If we should check column names * * @var bool */ protected $needColumnCheck = false; /** * DB data source model * * @var DataSourceModel */ protected $_dataSourceModel; /** * @var ProcessingErrorAggregatorInterface */ protected $errorAggregator; /** * Flag to disable import * * @var bool */ protected $_importAllowed = true; /** * Magento string lib * * @var StringUtils */ protected $string; /** * Entity model parameters * * @var array */ protected $_parameters = []; /** * Column names that holds values with particular meaning * * @var string[] */ protected $_specialAttributes = [self::COLUMN_ACTION]; /** * Permanent entity columns * * @var string[] */ protected $_permanentAttributes = []; /** * Number of entities processed by validation * * @var int */ protected $_processedEntitiesCount = 0; /** * Number of rows processed by validation * * @var int */ protected $_processedRowsCount = 0; /** * Need to log in import history * * @var bool */ protected $logInHistory = true; /** * Rows which will be skipped during import * * [Row number 1] => true, * ... * [Row number N] => true * * @var array */ protected $_skippedRows = []; /** * Array of numbers of validated rows as keys and boolean TRUE as values * * @var array */ protected $_validatedRows = []; /** * Source model * * @var AbstractSource */ protected $_source; /** * Array of unique attributes * * @var array */ protected $_uniqueAttributes = []; /** * List of available behaviors * * @var string[] */ protected $_availableBehaviors = [ \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE, \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE, \Magento\ImportExport\Model\Import::BEHAVIOR_CUSTOM, ]; /** * Number of items to fetch from db in one query * * @var int */ protected $_pageSize; /** * Maximum size of packet, that can be sent to DB * * @var int */ protected $_maxDataSize; /** * Number of items to save to the db in one query * * @var int */ protected $_bunchSize; /** * Code of a primary attribute which identifies the entity group if import contains of multiple rows * * @var string */ protected $masterAttributeCode; /** * Core store config * * @var ScopeConfigInterface */ protected $_scopeConfig; /** * Count if created items * * @var int */ protected $countItemsCreated = 0; /** * Count if updated items * * @var int */ protected $countItemsUpdated = 0; /** * Count if deleted items * * @var int */ protected $countItemsDeleted = 0; /** * Json Serializer Instance * * @var Json */ private $serializer; /** * Ids of saved data in DB * * @var array */ private array $ids = []; /** * @param StringUtils $string * @param ScopeConfigInterface $scopeConfig * @param ImportFactory $importFactory * @param Helper $resourceHelper * @param ResourceConnection $resource * @param ProcessingErrorAggregatorInterface $errorAggregator * @param array $data * @param Json|null $serializer * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( StringUtils $string, ScopeConfigInterface $scopeConfig, ImportFactory $importFactory, Helper $resourceHelper, ResourceConnection $resource, ProcessingErrorAggregatorInterface $errorAggregator, array $data = [], Json $serializer = null ) { $this->string = $string; $this->_scopeConfig = $scopeConfig; $this->_dataSourceModel = $data['data_source_model'] ?? $importFactory->create()->getDataSourceModel(); $this->_maxDataSize = $data['max_data_size'] ?? $resourceHelper->getMaxDataSize(); $this->_connection = $data['connection'] ?? $resource->getConnection(); $this->errorAggregator = $errorAggregator; $this->_pageSize = $data['page_size'] ?? ((int) $this->_scopeConfig->getValue( static::XML_PATH_PAGE_SIZE, ScopeInterface::SCOPE_STORE ) ?: 0); $this->_bunchSize = $data['bunch_size'] ?? ((int) $this->_scopeConfig->getValue( static::XML_PATH_BUNCH_SIZE, ScopeInterface::SCOPE_STORE ) ?: 0); foreach ($this->errorMessageTemplates as $errorCode => $message) { $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); } $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } /** * Returns Error aggregator * * @return ProcessingErrorAggregatorInterface */ public function getErrorAggregator() { return $this->errorAggregator; } /** * Import data rows * * @abstract * @return boolean */ abstract protected function _importData(); /** * Imported entity type code getter * * @abstract * @return string */ abstract public function getEntityTypeCode(); /** * Change row data before saving in DB table * * @param array $rowData * @return array */ protected function _prepareRowForDb(array $rowData) { /** * Convert all empty strings to null values, as * a) we don't use empty string in DB * b) empty strings instead of numeric values will product errors in Sql Server */ foreach ($rowData as $key => $val) { if ($val === '') { $rowData[$key] = null; } } return $rowData; } /** * Add errors to error aggregator * * @param string $code * @param array|mixed $errors * @return void */ protected function addErrors($code, $errors) { if ($errors) { $this->getErrorAggregator()->addError( $code, ProcessingError::ERROR_LEVEL_CRITICAL, null, implode('", "', $errors) ); } } /** * Validate data rows and save bunches to DB * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _saveValidatedBunches() { $source = $this->getSource(); $bunchRows = []; $startNewBunch = false; $source->rewind(); $this->_dataSourceModel->cleanProcessedBunches(); $mainAttributeCode = $this->getMasterAttributeCode(); while ($source->valid() || count($bunchRows) || isset($entityGroup)) { if ($startNewBunch || !$source->valid()) { /* If the end approached add last validated entity group to the bunch */ if (!$source->valid() && isset($entityGroup)) { foreach ($entityGroup as $key => $value) { $bunchRows[$key] = $value; } unset($entityGroup); } $this->ids[] = $this->_dataSourceModel->saveBunch($this->getEntityTypeCode(), $this->getBehavior(), $bunchRows); $bunchRows = []; $startNewBunch = false; } if ($source->valid()) { $valid = true; try { $rowData = $source->current(); foreach ($rowData as $attrName => $element) { if (!mb_check_encoding($element, 'UTF-8')) { $valid = false; $this->addRowError( AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS, $this->_processedRowsCount, $attrName ); } } } catch (\InvalidArgumentException $e) { $valid = false; $this->addRowError($e->getMessage(), $this->_processedRowsCount); } if (!$valid) { $this->_processedRowsCount++; $source->next(); continue; } if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) { /* Add entity group that passed validation to bunch */ if (isset($entityGroup)) { foreach ($entityGroup as $key => $value) { $bunchRows[$key] = $value; } $productDataSize = strlen($this->serializer->serialize($bunchRows)); /* Check if the new bunch should be started */ $isBunchSizeExceeded = ($this->_bunchSize > 0 && count($bunchRows) >= $this->_bunchSize); $startNewBunch = $productDataSize >= $this->_maxDataSize || $isBunchSizeExceeded; } /* And start a new one */ $entityGroup = []; } if (isset($entityGroup) && isset($rowData) && $this->validateRow($rowData, $source->key())) { /* Add row to entity group */ $entityGroup[$source->key()] = $this->_prepareRowForDb($rowData); } elseif (isset($entityGroup)) { /* In case validation of one line of the group fails kill the entire group */ unset($entityGroup); } $this->_processedRowsCount++; $source->next(); } } return $this; } /** * Add error with corresponding current data source row number. * * @param string $errorCode Error code or simply column name * @param int $errorRowNum Row number. * @param string $colName OPTIONAL Column name. * @param string $errorMessage OPTIONAL Column name. * @param string $errorLevel * @param string $errorDescription * @return $this */ public function addRowError( $errorCode, $errorRowNum, $colName = null, $errorMessage = null, $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL, $errorDescription = null ) { $errorCode = (string)$errorCode; $this->getErrorAggregator()->addError( $errorCode, $errorLevel, $errorRowNum, $colName, $errorMessage, $errorDescription ); return $this; } /** * Add message template for specific error code from outside * * @param string $errorCode Error code * @param string $message Message template * @return $this */ public function addMessageTemplate($errorCode, $message) { $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message); return $this; } /** * Import behavior getter * * @param array|null $rowData * @return string */ public function getBehavior(array $rowData = null) { if (isset( $this->_parameters['behavior'] ) && in_array( $this->_parameters['behavior'], $this->_availableBehaviors ) ) { $behavior = $this->_parameters['behavior']; if ($rowData !== null && $behavior == \Magento\ImportExport\Model\Import::BEHAVIOR_CUSTOM) { // try analyze value in self::COLUMN_CUSTOM column and return behavior for given $rowData if (array_key_exists(self::COLUMN_ACTION, $rowData)) { if ($rowData[self::COLUMN_ACTION] && strtolower($rowData[self::COLUMN_ACTION]) == self::COLUMN_ACTION_VALUE_DELETE ) { $behavior = \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE; } else { // as per task description, if column value is different to self::COLUMN_CUSTOM_VALUE_DELETE, // we should always use default behavior return self::getDefaultBehavior(); } if (in_array($behavior, $this->_availableBehaviors)) { return $behavior; } } } else { // if method is invoked without $rowData we should just return $this->_parameters['behavior'] return $behavior; } } return self::getDefaultBehavior(); } /** * Get default import behavior * * @return string * phpcs:disable Magento2.Functions.StaticFunction */ public static function getDefaultBehavior() { return \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE; } /** * Returns number of checked entities * * @return int */ public function getProcessedEntitiesCount() { return $this->_processedEntitiesCount; } /** * Returns number of checked rows * * @return int */ public function getProcessedRowsCount() { return $this->_processedRowsCount; } /** * Source object getter * * @return AbstractSource * @throws \Magento\Framework\Exception\LocalizedException */ public function getSource() { if (!$this->_source) { throw new \Magento\Framework\Exception\LocalizedException(__('The source is not set.')); } return $this->_source; } /** * Import process start * * @return bool Result of operation */ public function importData() { return $this->_importData(); } /** * Is attribute contains particular data (not plain entity attribute) * * @param string $attributeCode * @return bool */ public function isAttributeParticular($attributeCode) { return in_array($attributeCode, $this->_specialAttributes); } /** * Returns the master attribute code to use in an import * * @return string */ public function getMasterAttributeCode() { return $this->masterAttributeCode; } /** * Check one attribute can be overridden in child * * @param string $attributeCode Attribute code * @param array $attributeParams Attribute params * @param array $rowData Row data * @param int $rowNumber * @param string $multiSeparator * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function isAttributeValid( $attributeCode, array $attributeParams, array $rowData, $rowNumber, $multiSeparator = Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR ) { $message = ''; $rowData[$attributeCode] = $rowData[$attributeCode] ?? ''; switch ($attributeParams['type']) { case 'varchar': $value = $this->string->cleanString($rowData[$attributeCode]); $valid = $this->string->strlen($value) < self::DB_MAX_VARCHAR_LENGTH; $message = self::ERROR_EXCEEDED_MAX_LENGTH; break; case 'decimal': $value = trim($rowData[$attributeCode]); $valid = (double)$value == $value && is_numeric($value); $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'select': case 'multiselect': case 'boolean': $valid = true; foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeCode])) as $value) { $valid = isset($attributeParams['options'][$value]); if (!$valid) { break; } } $message = self::ERROR_INVALID_ATTRIBUTE_OPTION; break; case 'int': $value = trim($rowData[$attributeCode]); $valid = (int)$value == $value && is_numeric($value); $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'datetime': $value = trim($rowData[$attributeCode]); $valid = strtotime($value) !== false; $message = self::ERROR_INVALID_ATTRIBUTE_TYPE; break; case 'text': $value = $this->string->cleanString($rowData[$attributeCode]); $valid = $this->string->strlen($value) < self::DB_MAX_TEXT_LENGTH; $message = self::ERROR_EXCEEDED_MAX_LENGTH; break; default: $valid = true; break; } if (!$valid) { if ($message == self::ERROR_INVALID_ATTRIBUTE_TYPE) { $message = sprintf( $this->errorMessageTemplates[$message], $attributeCode, $attributeParams['type'] ); } $this->addRowError($message, $rowNumber, $attributeCode); } elseif (!empty($attributeParams['is_unique'])) { if (isset($this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]])) { $this->addRowError(self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNumber, $attributeCode); return false; } $this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]] = true; } return (bool)$valid; } /** * Import possibility getter * * @return bool */ public function isImportAllowed() { return $this->_importAllowed; } /** * Returns TRUE if row is valid and not in skipped rows array * * @param array $rowData * @param int $rowNumber * @return bool */ public function isRowAllowedToImport(array $rowData, $rowNumber) { return $this->validateRow($rowData, $rowNumber) && !isset($this->_skippedRows[$rowNumber]); } /** * Is import need to log in history. * * @return bool */ public function isNeedToLogInHistory() { return $this->logInHistory; } /** * Validate data row * * @param array $rowData * @param int $rowNumber * @return bool */ abstract public function validateRow(array $rowData, $rowNumber); /** * Set data from outside to change behavior * * @param array $parameters * @return $this */ public function setParameters(array $parameters) { $this->_parameters = $parameters; return $this; } /** * Source model setter * * @param AbstractSource $source * @return $this */ public function setSource(AbstractSource $source) { $this->_source = $source; $this->_dataValidated = false; return $this; } /** * Validate data * * @return ProcessingErrorAggregatorInterface * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function validateData() { if (!$this->_dataValidated) { $this->getErrorAggregator()->clear(); // do all permanent columns exist? $absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames()); $this->addErrors(self::ERROR_CODE_COLUMN_NOT_FOUND, $absentColumns); // check attribute columns names validity $columnNumber = 0; $emptyHeaderColumns = []; $invalidColumns = []; $invalidAttributes = []; foreach ($this->getSource()->getColNames() as $columnName) { $columnNumber++; if (!$this->isAttributeParticular($columnName)) { if ($columnName === null || trim($columnName) == '') { $emptyHeaderColumns[] = $columnNumber; } elseif (!preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) { $invalidColumns[] = $columnName; } elseif ($this->needColumnCheck && !in_array($columnName, $this->getValidColumnNames())) { $invalidAttributes[] = $columnName; } } } $this->addErrors(self::ERROR_CODE_INVALID_ATTRIBUTE, $invalidAttributes); $this->addErrors(self::ERROR_CODE_COLUMN_EMPTY_HEADER, $emptyHeaderColumns); $this->addErrors(self::ERROR_CODE_COLUMN_NAME_INVALID, $invalidColumns); if (!$this->getErrorAggregator()->getErrorsCount()) { $this->_saveValidatedBunches(); $this->_dataValidated = true; } } return $this->getErrorAggregator(); } /** * Get count of created items * * @return int */ public function getCreatedItemsCount() { return $this->countItemsCreated; } /** * Get count of updated items * * @return int */ public function getUpdatedItemsCount() { return $this->countItemsUpdated; } /** * Get count of deleted items * * @return int */ public function getDeletedItemsCount() { return $this->countItemsDeleted; } /** * Update proceed items counter * * @param array $created * @param array $updated * @param array $deleted * @return $this */ protected function updateItemsCounterStats(array $created = [], array $updated = [], array $deleted = []) { $this->countItemsCreated += count($created); $this->countItemsUpdated += count($updated); $this->countItemsDeleted += count($deleted); return $this; } /** * Retrieve valid column names * * @return array */ public function getValidColumnNames() { return $this->validColumnNames; } /** * Retrieve Ids of Validated Rows * * @return array */ public function getIds() : array { return $this->ids; } /** * Set Ids of Validated Rows * * @param array $ids * @return void */ public function setIds(array $ids) { $this->ids = $ids; } /** * Gets the currently used DataSourceModel * * @return DataSourceModel */ public function getDataSourceModel() : DataSourceModel { return $this->_dataSourceModel; } }