<?php
/**
* @version	$Id: cat_dbitem_export_helper.php 15246 2012-03-27 11:51:06Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	define('EXPORT_STEP', 100); // export by 200 items (e.g. links)
	define('IMPORT_STEP', 20); // export by 200 items (e.g. links)
	define('IMPORT_CHUNK', 10240); // 10240); //30720); //50120); // 5 KB

	define('IMPORT_TEMP', 1);
	define('IMPORT_LIVE', 2);

	class kCatDBItemExportHelper extends kHelper {

		var $false = false;

		var $cache = Array();

		/**
		 * Allows to find out what items are new in cache
		 *
		 * @var Array
		 */
		var $cacheStatus = Array();

		var $cacheTable = '';

		var $exportFields = Array();

		/**
		 * Export options
		 *
		 * @var Array
		 */
		var $exportOptions = Array();

		/**
		 * Item beeing currenly exported
		 *
		 * @var kCatDBItem
		 */
		var $curItem = null;

		/**
		 * Dummy category object
		 *
		 * @var CategoriesItem
		 */
		var $dummyCategory = null;

		/**
		 * Pointer to opened file
		 *
		 * @var resource
		 */
		var $filePointer = null;

		/**
		 * Custom fields definition of current item
		 *
		 * @var Array
		 */
		var $customFields = Array();

		public function __construct()
		{
			parent::__construct();

			$this->cacheTable = TABLE_PREFIX.'ImportCache';
		}

		/**
		 * Returns value from cache if found or false otherwise
		 *
		 * @param string $type
		 * @param int $key
		 * @return mixed
		 */
		function getFromCache($type, $key)
		{
			return getArrayValue($this->cache, $type, $key);
		}

		/**
		 * Adds value to be cached
		 *
		 * @param string $type
		 * @param int $key
		 * @param mixed $value
		 * @param bool $is_new
		 */
		function addToCache($type, $key, $value, $is_new = true)
		{
			/*if ( !isset($this->cache[$type]) ) {
				$this->cache[$type] = Array ();
			}*/

			$this->cache[$type][$key] = $value;

			if ( $is_new ) {
				$this->cacheStatus[$type][$key] = true;
			}
		}

		function storeCache($cache_types)
		{
			$fields_hash = Array ();
			$cache_types = explode(',', $cache_types);

			foreach ($cache_types as $cache_type) {
				$fields_hash = Array ('CacheName' => $cache_type);
				$cache = getArrayValue($this->cacheStatus, $cache_type);

				if ( !$cache ) {
					$cache = Array ();
				}

				foreach ($cache as $var_name => $cache_status) {
					$fields_hash['VarName'] = $var_name;
					$fields_hash['VarValue'] = $this->cache[$cache_type][$var_name];

					$this->Conn->doInsert($fields_hash, $this->cacheTable, 'INSERT', false);
				}
			}

			if ( isset($fields_hash['VarName']) ) {
				$this->Conn->doInsert($fields_hash, $this->cacheTable, 'INSERT');
			}
		}

		function loadCache()
		{
			$this->cache = Array ();

			$sql = 'SELECT *
					FROM ' . $this->cacheTable;
			$records = $this->Conn->GetIterator($sql);

			foreach ($records as $record) {
				$this->addToCache($record['CacheName'], $record['VarName'], $record['VarValue'], false);
			}
		}

		/**
		 * Fill required fields with dummy values
		 *
		 * @param kEvent|bool $event
		 * @param kCatDBItem|bool $object
		 * @param bool $set_status
		 */
		function fillRequiredFields($event, &$object, $set_status = false)
		{
			if ( $object == $this->false ) {
				$object = $event->getObject();
				/* @var $object kCatDBItem */
			}

			$has_empty = false;
			$fields = $object->getFields();

			if ( $object->isField('CreatedById') ) {
				// CSV file was created without required CreatedById column
				if ( $object->isRequired('CreatedById') ) {
					$object->setRequired('CreatedById', false);
				}

				if ( !is_numeric( $object->GetDBField('CreatedById') ) ) {
					$object->SetDBField('CreatedById', $this->Application->RecallVar('user_id'));
				}
			}

			foreach ($fields as $field_name => $field_options) {
				if ( $object->isVirtualField($field_name) || !$object->isRequired($field_name) ) {
					continue;
				}

				if ( $object->GetDBField($field_name) ) {
					continue;
				}

				$formatter_class = getArrayValue($field_options, 'formatter');

				if ( $formatter_class ) {
					// not tested
					$formatter = $this->Application->recallObject($formatter_class);
					/* @var $formatter kFormatter */

					$sample_value = $formatter->GetSample($field_name, $field_options, $object);
				}

				$has_empty = true;
				$object->SetField($field_name, isset($sample_value) && $sample_value ? $sample_value : 'no value');
			}
			$object->UpdateFormattersSubFields();

			if ( $set_status && $has_empty ) {
				$object->SetDBField('Status', 0);
			}
		}

		/**
		 * Verifies that all user entered export params are correct
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access protected
		 */
		protected function verifyOptions($event)
		{
			if ($this->Application->RecallVar($event->getPrefixSpecial().'_ForceNotValid'))
			{
				$this->Application->StoreVar($event->getPrefixSpecial().'_ForceNotValid', 0);
				return false;
			}

			$this->fillRequiredFields($event, $this->false);

			$object = $event->getObject();
			/* @var $object kCatDBItem */

			$cross_unique_fields = Array('FieldsSeparatedBy', 'FieldsEnclosedBy');
			if (($object->GetDBField('CategoryFormat') == 1) || ($event->Special == 'import')) // in one field
			{
				$object->setRequired('CategorySeparator');
				$cross_unique_fields[] = 'CategorySeparator';
			}

			$ret = $object->Validate();

			// check if cross unique fields has no same values
			foreach ($cross_unique_fields as $field_index => $field_name)
			{
				if ($object->GetErrorPseudo($field_name) == 'required') {
					continue;
				}

				$check_fields = $cross_unique_fields;
				unset($check_fields[$field_index]);

				foreach ($check_fields as $check_field)
				{
					if ($object->GetDBField($field_name) == $object->GetDBField($check_field))
					{
						$object->SetError($check_field, 'unique');
					}
				}
			}

			if ($event->Special == 'import')
			{
				$this->exportOptions = $this->loadOptions($event);

				$automatic_fields = ($object->GetDBField('FieldTitles') == 1);
				$object->setRequired('ExportColumns', !$automatic_fields);
				$category_prefix = '__CATEGORY__';
				if ( $automatic_fields && ($this->exportOptions['SkipFirstRow']) ) {
					$this->openFile($event);
					$this->exportOptions['ExportColumns'] = $this->readRecord();

					if (!$this->exportOptions['ExportColumns']) {
						$this->exportOptions['ExportColumns'] = Array ();
					}

					$this->closeFile();

					// remove additional (non-parseble columns)
					foreach ($this->exportOptions['ExportColumns'] as $field_index => $field_name) {
						if (!$this->validateField($field_name, $object)) {
							unset($this->exportOptions['ExportColumns'][$field_index]);
						}
					}
					$category_prefix = '';
				}

				// 1. check, that we have column definitions
				if (!$this->exportOptions['ExportColumns']) {
					$object->setError('ExportColumns', 'required');
					$ret = false;
				}
				else {
					// 1.1. check that all required fields are present in imported file
					$missing_columns = Array();
					$fields = $object->getFields();

					foreach ($fields as $field_name => $field_options) {
						if ($object->skipField($field_name)) continue;
						if ( $object->isRequired($field_name) && !in_array($field_name, $this->exportOptions['ExportColumns']) ) {
							$missing_columns[] = $field_name;
							$object->setError('ExportColumns', 'required_fields_missing', 'la_error_RequiredColumnsMissing');
							$ret = false;
						}
					}

					if (!$ret && $this->Application->isDebugMode()) {
						$this->Application->Debugger->appendHTML('Missing required for import/export:');
						$this->Application->Debugger->dumpVars($missing_columns);
					}
				}


				// 2. check, that we have only mixed category field or only separated category fields
				$category_found['mixed'] = false;
				$category_found['separated'] = false;

				foreach ($this->exportOptions['ExportColumns'] as $import_field) {
					if (preg_match('/^'.$category_prefix.'Category(Path|[0-9]+)/', $import_field, $rets)) {
						$category_found[$rets[1] == 'Path' ? 'mixed' : 'separated'] = true;
					}
				}
				if ($category_found['mixed'] && $category_found['separated']) {
					$object->SetError('ExportColumns', 'unique_category', 'la_error_unique_category_field');
					$ret = false;
				}

				// 3. check, that duplicates check fields are selected & present in imported fields
				if ($this->exportOptions['ReplaceDuplicates']) {
					if ($this->exportOptions['CheckDuplicatesMethod'] == 1) {
						$check_fields = Array($object->IDField);
					}
					else {
						$check_fields = $this->exportOptions['DuplicateCheckFields'] ? explode('|', substr($this->exportOptions['DuplicateCheckFields'], 1, -1)) : Array();
						$object = $event->getObject();

						$fields = $object->getFields();
						$language_id = $this->Application->GetDefaultLanguageId();

						foreach ($check_fields as $index => $check_field) {
							foreach ($fields as $field_name => $field_options) {
								if ($field_name == 'l'.$language_id.'_'.$check_field) {
									$check_fields[$index] = 'l'.$language_id.'_'.$check_field;
									break;
								}
							}
						}
					}
					$this->exportOptions['DuplicateCheckFields'] = $check_fields;

					if (!$check_fields) {
						$object->setError('CheckDuplicatesMethod', 'required');
						$ret = false;
					}
					else {
						foreach ($check_fields as $check_field) {
							$check_field = preg_replace('/^cust_(.*)/', 'Custom_\\1', $check_field);
							if (!in_array($check_field, $this->exportOptions['ExportColumns'])) {
								$object->setError('ExportColumns', 'required');
								$ret = false;
								break;
							}
						}
					}
				}
				$this->saveOptions($event);
			}

			return $ret;
		}

		/**
		 * Returns filename to read import data from
		 *
		 * @return string
		 */
		function getImportFilename()
		{
			if ($this->exportOptions['ImportSource'] == 1)
			{
				$ret = $this->exportOptions['ImportFilename']; // ['name']; commented by Kostja
			}
			else {
				$ret = $this->exportOptions['ImportLocalFilename'];
			}
			return EXPORT_PATH.'/'.$ret;
		}

		/**
		 * Returns filename to write export data to
		 *
		 * @return string
		 */
		function getExportFilename()
		{
			$extension = $this->getFileExtension();
			$filename = preg_replace('/(.*)\.' . $extension . '$/', '\1', $this->exportOptions['ExportFilename']) . '.' . $extension;

			return EXPORT_PATH . DIRECTORY_SEPARATOR . $filename;
		}

		/**
		 * Opens file required for export/import operations
		 *
		 * @param kEvent $event
		 */
		function openFile($event)
		{
			$file_helper = $this->Application->recallObject('FileHelper');
			/* @var $file_helper FileHelper */

			$file_helper->CheckFolder(EXPORT_PATH);

			if ($event->Special == 'export') {
				$write_mode = ($this->exportOptions['start_from'] == 0) ? 'w' : 'a';
				$this->filePointer = fopen($this->getExportFilename(), $write_mode);
			}
			else {
				$this->filePointer = fopen($this->getImportFilename(), 'r');
			}

			// skip UTF-8 BOM Modifier
			$first_chars = fread($this->filePointer, 3);
			if (bin2hex($first_chars) != 'efbbbf') {
				fseek($this->filePointer, 0);
			}
		}

		/**
		 * Closes opened file
		 *
		 */
		function closeFile()
		{
			fclose($this->filePointer);
		}

		function getCustomSQL()
		{
			$ml_formatter = $this->Application->recallObject('kMultiLanguage');
			/* @var $ml_formatter kMultiLanguage */

			$custom_sql = '';

			foreach ($this->customFields as $custom_id => $custom_name) {
				$custom_sql .= 'custom_data.' . $ml_formatter->LangFieldName('cust_' . $custom_id) . ' AS cust_' . $custom_name . ', ';
			}

			return substr($custom_sql, 0, -2);
		}

		function getPlainExportSQL($count_only = false)
		{
			if ( $count_only && isset($this->exportOptions['ForceCountSQL']) ) {
				$sql = $this->exportOptions['ForceCountSQL'];
			}
			elseif ( !$count_only && isset($this->exportOptions['ForceSelectSQL']) ) {
				$sql = $this->exportOptions['ForceSelectSQL'];
			}
			else {
				$items_list = $this->Application->recallObject($this->curItem->Prefix . '.export-items-list', $this->curItem->Prefix . '_List');
				/* @var $items_list kDBList */

				$items_list->SetPerPage(-1);

				if ( $this->exportOptions['export_ids'] != '' ) {
					$items_list->addFilter('export_ids', $items_list->TableName . '.' . $items_list->IDField . ' IN (' . implode(',', $this->exportOptions['export_ids']) . ')');
				}

				if ( $count_only ) {
					$sql = $items_list->getCountSQL($items_list->GetSelectSQL(true, false));
				}
				else {
					$sql = $items_list->GetSelectSQL();
				}
			}

			if ( !$count_only ) {
				$sql .= ' LIMIT ' . $this->exportOptions['start_from'] . ',' . EXPORT_STEP;
			}
			/*else {
				$sql = preg_replace("/^\s*SELECT(.*?\s)FROM(?!_)/is", "SELECT COUNT(*) AS count FROM ", $sql);
			}*/

			return $sql;
		}

		function getExportSQL($count_only = false)
		{
			if ( !$this->Application->getUnitOption($this->curItem->Prefix, 'CatalogItem') ) {
				return $this->GetPlainExportSQL($count_only); // in case this is not a CategoryItem
			}

			if ( $this->exportOptions['export_ids'] === false ) {
				// get links from current category & all it's subcategories
				$join_clauses = Array ();

				$custom_sql = $this->getCustomSQL();

				if ( $custom_sql ) {
					$custom_table = $this->Application->getUnitOption($this->curItem->Prefix . '-cdata', 'TableName');
					$join_clauses[$custom_table . ' custom_data'] = 'custom_data.ResourceId = item_table.ResourceId';
				}

				$join_clauses[TABLE_PREFIX . 'CategoryItems ci'] = 'ci.ItemResourceId = item_table.ResourceId';
				$join_clauses[TABLE_PREFIX . 'Categories c'] = 'c.CategoryId = ci.CategoryId';

				$sql = 'SELECT item_table.*, ci.CategoryId' . ($custom_sql ? ', ' . $custom_sql : '') . '
						FROM ' . $this->curItem->TableName . ' item_table';

				foreach ($join_clauses as $table_name => $join_expression) {
					$sql .= ' LEFT JOIN ' . $table_name . ' ON ' . $join_expression;
				}

				$sql .= ' WHERE ';

				if ( $this->exportOptions['export_cats_ids'][0] == 0 ) {
					$sql .= '1';
				}
				else {
					foreach ($this->exportOptions['export_cats_ids'] as $category_id) {
						$sql .= '(c.ParentPath LIKE "%|' . $category_id . '|%") OR ';
					}

					$sql = substr($sql, 0, -4);
				}

				$sql .= ' ORDER BY ci.PrimaryCat DESC'; // NEW
			}
			else {
				// get only selected links
				$sql = 'SELECT item_table.*, ' . $this->exportOptions['export_cats_ids'][0] . ' AS CategoryId
						FROM ' . $this->curItem->TableName . ' item_table
						WHERE ' . $this->curItem->IDField . ' IN (' . implode(',', $this->exportOptions['export_ids']) . ')';
			}

			if ( !$count_only ) {
				$sql .= ' LIMIT ' . $this->exportOptions['start_from'] . ',' . EXPORT_STEP;
			}
			else {
				$sql = preg_replace("/^\s*SELECT(.*?\s)FROM(?!_)/is", "SELECT COUNT(*) AS count FROM ", $sql);
			}

			return $sql;
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 */
		function performExport($event)
		{
			$this->exportOptions = $this->loadOptions($event);
			$this->exportFields = $this->exportOptions['ExportColumns'];
			$this->curItem = $event->getObject( Array('skip_autoload' => true) );
			$this->customFields = $this->Application->getUnitOption($event->Prefix, 'CustomFields');
			$this->openFile($event);

			if ($this->exportOptions['start_from'] == 0) // first export step
			{
				if (!getArrayValue($this->exportOptions, 'IsBaseCategory')) {
					$this->exportOptions['IsBaseCategory'] = 0;
				}

				if ($this->exportOptions['IsBaseCategory'] ) {
					$sql = 'SELECT ParentPath
							FROM '.TABLE_PREFIX.'Categories
							WHERE CategoryId = ' . (int)$this->Application->GetVar('m_cat_id');
					$parent_path = $this->Conn->GetOne($sql);
					$parent_path = explode('|', substr($parent_path, 1, -1));
					if ($parent_path && $parent_path[0] == $this->Application->getBaseCategory()) {
						array_shift($parent_path);
					}

					$this->exportOptions['BaseLevel'] = count($parent_path); // level to cut from other categories
				}

				// 1. export field titles if required
				if ($this->exportOptions['IncludeFieldTitles'])
				{
					$data_array = Array();
					foreach ($this->exportFields as $export_field)
					{
						$data_array = array_merge($data_array, $this->getFieldCaption($export_field));
					}
					$this->writeRecord($data_array);
				}
				$this->exportOptions['total_records'] = $this->Conn->GetOne( $this->getExportSQL(true) );
			}

			// 2. export data
			$records = $this->Conn->Query( $this->getExportSQL() );
			$records_exported = 0;
			foreach ($records as $record_info) {
				$this->curItem->LoadFromHash($record_info);

				$data_array = Array();
				foreach ($this->exportFields as $export_field)
				{
					$data_array = array_merge($data_array, $this->getFieldValue($export_field) );
				}
				$this->writeRecord($data_array);
				$records_exported++;
			}
			$this->closeFile();

			$this->exportOptions['start_from'] += $records_exported;
			$this->saveOptions($event);

			return $this->exportOptions;
		}

		function getItemFields()
		{
			// just in case dummy user selected automtic mode & moved columns too :(
			$src_options = $this->curItem->GetFieldOption('ExportColumns', 'options');
			$dst_options = $this->curItem->GetFieldOption('AvailableColumns', 'options');

			return array_merge($dst_options, $src_options);
		}

		/**
		 * Checks if field really belongs to importable field list
		 *
		 * @param string $field_name
		 * @param kCatDBItem $object
		 * @return bool
		 */
		function validateField($field_name, &$object)
		{
			// 1. convert custom field
			$field_name = preg_replace('/^Custom_(.*)/', '__CUSTOM__\\1', $field_name);

			// 2. convert category field (mixed version & separated version)
			$field_name = preg_replace('/^Category(Path|[0-9]+)/', '__CATEGORY__Category\\1', $field_name);

			$valid_fields = $object->getPossibleExportColumns();
			return isset($valid_fields[$field_name]) || isset($valid_fields['__VIRTUAL__'.$field_name]);
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 */
		function performImport($event)
		{
			if (!$this->exportOptions) {
				// load import options in case if not previously loaded in verification function
				$this->exportOptions = $this->loadOptions($event);
			}

			$backup_category_id = $this->Application->GetVar('m_cat_id');
			$this->Application->SetVar('m_cat_id', (int)$this->Application->RecallVar('ImportCategory') );

			$this->openFile($event);

			$bytes_imported = 0;
			if ($this->exportOptions['start_from'] == 0) // first export step
			{
				// 1st time run
				if ($this->exportOptions['SkipFirstRow']) {
					$this->readRecord();
					$this->exportOptions['start_from'] = ftell($this->filePointer);
					$bytes_imported = ftell($this->filePointer);
				}

				$current_category_id = $this->Application->GetVar('m_cat_id');
				if ($current_category_id > 0) {
					$sql = 'SELECT ParentPath FROM '.TABLE_PREFIX.'Categories WHERE CategoryId = '.$current_category_id;
					$this->exportOptions['ImportCategoryPath'] = $this->Conn->GetOne($sql);
				}
				else {
					$this->exportOptions['ImportCategoryPath'] = '';
				}
				$this->exportOptions['total_records'] = filesize($this->getImportFilename());
			}
			else {
				$this->loadCache();
			}

			$this->exportFields = $this->exportOptions['ExportColumns'];
			$this->addToCache('category_parent_path', $this->Application->GetVar('m_cat_id'), $this->exportOptions['ImportCategoryPath']);

			// 2. import data
			$this->dummyCategory = $this->Application->recallObject('c.-tmpitem', 'c', Array('skip_autoload' => true));
			fseek($this->filePointer, $this->exportOptions['start_from']);

			$items_processed = 0;
			while (($bytes_imported < IMPORT_CHUNK && $items_processed < IMPORT_STEP) && !feof($this->filePointer)) {
				$data = $this->readRecord();
				if ($data) {
					if ($this->exportOptions['ReplaceDuplicates']) {
						// set fields used as keys for replace duplicates code
						$this->resetImportObject($event, IMPORT_TEMP, $data);
					}

					$this->processCurrentItem($event, $data);
				}
				$bytes_imported = ftell($this->filePointer) - $this->exportOptions['start_from'];
				$items_processed++;
			}

			$this->closeFile();
			$this->Application->SetVar('m_cat_id', $backup_category_id);

			$this->exportOptions['start_from'] += $bytes_imported;
			$this->storeCache('new_ids');

			$this->saveOptions($event);

			if ($this->exportOptions['start_from'] == $this->exportOptions['total_records']) {
				$this->Conn->Query('TRUNCATE TABLE '.$this->cacheTable);
			}

			return $this->exportOptions;
		}

		function setCurrentID()
		{
			$this->curItem->setID( $this->curItem->GetDBField($this->curItem->IDField) );
		}

		/**
		 * Sets value of import/export object
		 * @param int $field_index
		 * @param mixed $value
		 * @return void
		 * @access protected
		 */
		protected function setFieldValue($field_index, $value)
		{
			if ( empty($value) ) {
				$value = null;
			}

			$field_name = getArrayValue($this->exportFields, $field_index);
			if ( $field_name == 'ResourceId' ) {
				return ;
			}

			if ( substr($field_name, 0, 7) == 'Custom_' ) {
				$field_name = 'cust_' . substr($field_name, 7);
				$this->curItem->SetField($field_name, $value);
			}
			elseif ( $field_name == 'CategoryPath' || $field_name == '__CATEGORY__CategoryPath' ) {
				$this->curItem->CategoryPath = $value ? explode($this->exportOptions['CategorySeparator'], $value) : Array ();
			}
			elseif ( substr($field_name, 0, 8) == 'Category' ) {
				$this->curItem->CategoryPath[(int)substr($field_name, 8) - 1] = $value;
			}
			elseif ( substr($field_name, 0, 20) == '__CATEGORY__Category' ) {
				$this->curItem->CategoryPath[(int)substr($field_name, 20) - 1] = $value;
			}
			elseif ( substr($field_name, 0, 11) == '__VIRTUAL__' ) {
				$field_name = substr($field_name, 11);
				$this->curItem->SetField($field_name, $value);
			}
			else {
				$this->curItem->SetField($field_name, $value);
			}

			if ( $this->curItem->GetErrorPseudo($field_name) ) {
				$this->curItem->SetDBField($field_name, null);
				$this->curItem->RemoveError($field_name);
			}
		}

		/**
		 * Resets import object
		 *
		 * @param kEvent $event
		 * @param int $object_type
		 * @param Array $record_data
		 * @return void
		 */
		function resetImportObject($event, $object_type, $record_data = null)
		{
			switch ($object_type) {
				case IMPORT_TEMP:
					$this->curItem = $event->getObject( Array('skip_autoload' => true) );
					break;

				case IMPORT_LIVE:
					$this->curItem = $this->Application->recallObject($event->Prefix.'.-tmpitem'.$event->Special, $event->Prefix, Array('skip_autoload' => true));
					break;
			}
			$this->curItem->Clear();
			$this->curItem->SetDBField('CategoryId', NULL); // since default value is import root category
			$this->customFields = $this->Application->getUnitOption($event->Prefix, 'CustomFields');

			if (isset($record_data)) {
				$this->setImportData($record_data);
			}
		}

		function setImportData($record_data)
		{
			foreach ($record_data as $field_index => $field_value) {
				$this->setFieldValue($field_index, $field_value);
			}
			$this->setCurrentID();
		}


		function getItemCategory()
		{
			static $lang_prefix = null;
			$backup_category_id = $this->Application->GetVar('m_cat_id');

			$category_id = $this->getFromCache('category_names', implode(':', $this->curItem->CategoryPath));
			if ($category_id) {
				$this->Application->SetVar('m_cat_id', $category_id);
				return $category_id;
			}

			if (is_null($lang_prefix)) {
				$lang_prefix = 'l'.$this->Application->GetVar('m_lang').'_';
			}

			foreach ($this->curItem->CategoryPath as $category_index => $category_name) {
				if (!$category_name) continue;
				$category_key = kUtil::crc32( implode(':', array_slice($this->curItem->CategoryPath, 0, $category_index + 1) ) );

				$category_id = $this->getFromCache('category_names', $category_key);
				if ($category_id === false) {
					// get parent category path to search only in it
					$current_category_id = $this->Application->GetVar('m_cat_id');
//					$parent_path = $this->getParentPath($current_category_id);

					// get category id from database by name
					$sql = 'SELECT CategoryId
							FROM '.TABLE_PREFIX.'Categories
							WHERE ('.$lang_prefix.'Name = '.$this->Conn->qstr($category_name).') AND (ParentId = '.(int)$current_category_id.')';
					$category_id = $this->Conn->GetOne($sql);

					if ( $category_id === false ) {
						// category not in db -> create
						$category_fields = Array (
							$lang_prefix.'Name' => $category_name, $lang_prefix.'Description' => $category_name,
							'Status' => STATUS_ACTIVE, 'ParentId' => $current_category_id, 'AutomaticFilename' => 1
						);

						$this->dummyCategory->Clear();
						$this->dummyCategory->SetDBFieldsFromHash($category_fields);

						if ( $this->dummyCategory->Create() ) {
							$category_id = $this->dummyCategory->GetID();
							$this->addToCache('category_parent_path', $category_id, $this->dummyCategory->GetDBField('ParentPath'));
							$this->addToCache('category_names', $category_key, $category_id);
						}
					}
					else {
						$this->addToCache('category_names', $category_key, $category_id);
					}
				}

				if ($category_id) {
					$this->Application->SetVar('m_cat_id', $category_id);
				}
			}
			if (!$this->curItem->CategoryPath) {
				$category_id = $backup_category_id;
			}

			return $category_id;
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 * @param Array $record_data
		 * @return bool
		 */
		function processCurrentItem($event, $record_data)
		{
			$save_method = 'Create';
			$load_keys = Array();

			// create/update categories
			$backup_category_id = $this->Application->GetVar('m_cat_id');

			// perform replace duplicates code
			if ($this->exportOptions['ReplaceDuplicates']) {
				// get replace keys first, then reset current item to empty one
				$category_id = $this->getItemCategory();
				if ($this->exportOptions['CheckDuplicatesMethod'] == 1) {
					if ($this->curItem->GetID()) {
						$load_keys = Array($this->curItem->IDField => $this->curItem->GetID());
					}
				}
				else {
					$key_fields = $this->exportOptions['DuplicateCheckFields'];
					foreach ($key_fields as $key_field) {
						$load_keys[$key_field] = $this->curItem->GetDBField($key_field);
					}
				}

				$this->resetImportObject($event, IMPORT_LIVE);

				if (count($load_keys)) {
					$where_clause = '';
					$language_id = (int)$this->Application->GetVar('m_lang');

					if (!$language_id) {
						$language_id = 1;
					}

					foreach ($load_keys as $field_name => $field_value) {
						if (preg_match('/^cust_(.*)/', $field_name, $regs)) {
							$custom_id = array_search($regs[1], $this->customFields);
							$field_name = 'l'.$language_id.'_cust_'.$custom_id;
							$where_clause .= '(custom_data.`'.$field_name.'` = '.$this->Conn->qstr($field_value).') AND ';
						}
						else {
							$where_clause .= '(item_table.`'.$field_name.'` = '.$this->Conn->qstr($field_value).') AND ';
						}

					}
					$where_clause = substr($where_clause, 0, -5);

					$item_id = $this->getFromCache('new_ids', kUtil::crc32($where_clause));
					if (!$item_id) {
						if ($this->exportOptions['CheckDuplicatesMethod'] == 2) {
							// by other fields
							$parent_path = $this->getParentPath($category_id);
							$where_clause = '(c.ParentPath LIKE "'.$parent_path.'%") AND '.$where_clause;
						}

						$cdata_table = $this->Application->getUnitOption($event->Prefix.'-cdata', 'TableName');
						$sql = 'SELECT '.$this->curItem->IDField.'
								FROM '.$this->curItem->TableName.' item_table
								LEFT JOIN '.$cdata_table.' custom_data ON custom_data.ResourceId = item_table.ResourceId
								LEFT JOIN '.TABLE_PREFIX.'CategoryItems ci ON ci.ItemResourceId = item_table.ResourceId
								LEFT JOIN '.TABLE_PREFIX.'Categories c ON c.CategoryId = ci.CategoryId
								WHERE '.$where_clause;
						$item_id = $this->Conn->GetOne($sql);
					}
					$save_method = $item_id && $this->curItem->Load($item_id) ? 'Update' : 'Create';
					if ($save_method == 'Update') {
						// replace id from csv file with found id (only when ID is found in cvs file)
						if (in_array($this->curItem->IDField, $this->exportFields)) {
							$record_data[ array_search($this->curItem->IDField, $this->exportFields) ] = $item_id;
						}
					}
				}

				$this->setImportData($record_data);
			}
			else {
				$this->resetImportObject($event, IMPORT_LIVE, $record_data);
				$category_id = $this->getItemCategory();
			}

			// create main record
			if ($save_method == 'Create') {
				$this->fillRequiredFields($this->false, $this->curItem, true);
			}

//			$sql_start = microtime(true);
			if (!$this->curItem->$save_method()) {
				$this->Application->SetVar('m_cat_id', $backup_category_id);
				return false;
			}
//			$sql_end = microtime(true);
//			$this->saveLog('SQL ['.$save_method.'] Time: '.($sql_end - $sql_start).'s');

			if ($load_keys && ($save_method == 'Create') && $this->exportOptions['ReplaceDuplicates']) {
				// map new id to old id
				$this->addToCache('new_ids', kUtil::crc32($where_clause), $this->curItem->GetID() );
			}

			// assign item to categories
			$this->curItem->assignToCategory($category_id, false);

			$this->Application->SetVar('m_cat_id', $backup_category_id);
			return true;
		}

		/*function saveLog($msg)
		{
			static $first_time = true;

			$fp = fopen((defined('RESTRICTED') ? RESTRICTED : FULL_PATH) . '/sqls.log', $first_time ? 'w' : 'a');
			fwrite($fp, $msg."\n");
			fclose($fp);

			$first_time = false;
		}*/

		/**
		 * Returns category parent path, if possible, then from cache
		 *
		 * @param int $category_id
		 * @return string
		 */
		function getParentPath($category_id)
		{
			$parent_path = $this->getFromCache('category_parent_path', $category_id);
			if ($parent_path === false) {
				$sql = 'SELECT ParentPath
						FROM '.TABLE_PREFIX.'Categories
						WHERE CategoryId = '.$category_id;
				$parent_path = $this->Conn->GetOne($sql);
				$this->addToCache('category_parent_path', $category_id, $parent_path);
			}
			return $parent_path;
		}

		function getFileExtension()
		{
			return $this->exportOptions['ExportFormat'] == 1 ? 'csv' : 'xml';
		}

		function getLineSeparator($option = 'LineEndings')
		{
			return $this->exportOptions[$option] == 1 ? "\r\n" : "\n";
		}

		/**
		 * Returns field caption for any exported field
		 *
		 * @param string $field
		 * @return string
		 */
		function getFieldCaption($field)
		{
			if (substr($field, 0, 10) == '__CUSTOM__')
			{
				$ret = 'Custom_'.substr($field, 10, strlen($field) );
			}
			elseif (substr($field, 0, 12) == '__CATEGORY__')
			{
				return $this->getCategoryTitle();
			}
			elseif (substr($field, 0, 11) == '__VIRTUAL__') {
				$ret = substr($field, 11);
			}
			else
			{
				$ret = $field;
			}

			return Array($ret);
		}

		/**
		 * Returns requested field value (including custom fields and category fields)
		 *
		 * @param string $field
		 * @return string
		 */
		function getFieldValue($field)
		{
			if (substr($field, 0, 10) == '__CUSTOM__') {
				$field = 'cust_'.substr($field, 10, strlen($field));
				$ret = $this->curItem->GetField($field);
			}
			elseif (substr($field, 0, 12) == '__CATEGORY__') {
				return $this->getCategoryPath();
			}
			elseif (substr($field, 0, 11) == '__VIRTUAL__') {
				$field = substr($field, 11);
				$ret = $this->curItem->GetField($field);
			}
			else
			{
				$ret = $this->curItem->GetField($field);
			}

			$ret = str_replace("\r\n", $this->getLineSeparator('LineEndingsInside'), $ret);
			return Array($ret);
		}

		/**
		 * Returns category field(-s) caption based on export mode
		 *
		 * @return string
		 */
		function getCategoryTitle()
		{
			// category path in separated fields
			$category_count = $this->getMaxCategoryLevel();
			if ($this->exportOptions['CategoryFormat'] == 1)
			{
				// category path in one field
				return $category_count ? Array('CategoryPath') : Array();
			}
			else
			{
				$i = 0;
				$ret = Array();
				while ($i < $category_count) {
					$ret[] = 'Category'.($i + 1);
					$i++;
				}
				return $ret;
			}
		}

		/**
		 * Returns category path in required format for current link
		 *
		 * @return string
		 */
		function getCategoryPath()
		{
			$category_id = $this->curItem->GetDBField('CategoryId');
			$category_path = $this->getFromCache('category_path', $category_id);

			if ( !$category_path ) {
				$ml_formatter = $this->Application->recallObject('kMultiLanguage');
				/* @var $ml_formatter kMultiLanguage */

				$sql = 'SELECT ' . $ml_formatter->LangFieldName('CachedNavbar') . '
						FROM ' . TABLE_PREFIX . 'Categories
						WHERE CategoryId = ' . $category_id;
				$category_path = $this->Conn->GetOne($sql);

				$category_path = $category_path ? explode('&|&', $category_path) : Array ();

				if ( $category_path && strtolower($category_path[0]) == 'content' ) {
					array_shift($category_path);
				}

				if ( $this->exportOptions['IsBaseCategory'] ) {
					$i = $this->exportOptions['BaseLevel'];
					while ( $i > 0 ) {
						array_shift($category_path);
						$i--;
					}
				}

				$category_count = $this->getMaxCategoryLevel();

				if ( $this->exportOptions['CategoryFormat'] == 1 ) {
					// category path in single field
					$category_path = $category_count ? Array (implode($this->exportOptions['CategorySeparator'], $category_path)) : Array ();
				}
				else {
					// category path in separated fields
					$levels_used = count($category_path);

					if ( $levels_used < $category_count ) {
						$i = 0;
						while ( $i < $category_count - $levels_used ) {
							$category_path[] = '';
							$i++;
						}
					}
				}
				$this->addToCache('category_path', $category_id, $category_path);
			}

			return $category_path;
		}

		/**
		 * Get maximal category deep level from links beeing exported
		 *
		 * @return int
		 */
		function getMaxCategoryLevel()
		{
			static $max_level = -1;

			if ($max_level != -1)
			{
				return $max_level;
			}

			$sql = 'SELECT IF(c.CategoryId IS NULL, 0, MAX( LENGTH(c.ParentPath) - LENGTH( REPLACE(c.ParentPath, "|", "") ) - 1 ))
					FROM '.$this->curItem->TableName.' item_table
					LEFT JOIN '.TABLE_PREFIX.'CategoryItems ci ON item_table.ResourceId = ci.ItemResourceId
					LEFT JOIN '.TABLE_PREFIX.'Categories c ON c.CategoryId = ci.CategoryId
					WHERE (ci.PrimaryCat = 1) AND ';

			$where_clause = '';
			if ($this->exportOptions['export_ids'] === false) {
				// get links from current category & all it's subcategories
				if ($this->exportOptions['export_cats_ids'][0] == 0) {
					$where_clause = 1;
				}
				else {
					foreach ($this->exportOptions['export_cats_ids'] as $category_id) {
						$where_clause .= '(c.ParentPath LIKE "%|'.$category_id.'|%") OR ';
					}
					$where_clause = substr($where_clause, 0, -4);
				}
			}
			else {
				// get only selected links
				$where_clause = $this->curItem->IDField.' IN ('.implode(',', $this->exportOptions['export_ids']).')';
			}

			$max_level = $this->Conn->GetOne($sql.'('.$where_clause.')');

			if ($this->exportOptions['IsBaseCategory'] ) {
				$max_level -= $this->exportOptions['BaseLevel'];
			}

			return $max_level;
		}

		/**
		 * Saves one record to export file
		 *
		 * @param Array $fields_hash
		 */
		function writeRecord($fields_hash)
		{
			kUtil::fputcsv($this->filePointer, $fields_hash, $this->exportOptions['FieldsSeparatedBy'], $this->exportOptions['FieldsEnclosedBy'], $this->getLineSeparator() );
		}

		function readRecord()
		{
			return fgetcsv($this->filePointer, 10000, $this->exportOptions['FieldsSeparatedBy'], $this->exportOptions['FieldsEnclosedBy']);
		}

		/**
		 * Saves import/export options
		 *
		 * @param kEvent $event
		 * @param Array $options
		 * @return void
		 */
		function saveOptions($event, $options = null)
		{
			if ( !isset($options) ) {
				$options = $this->exportOptions;
			}

			$this->Application->StoreVar($event->getPrefixSpecial() . '_options', serialize($options));
		}

		/**
		 * Loads import/export options
		 *
		 * @param kEvent $event
		 * @return Array
		 */
		function loadOptions($event)
		{
			return unserialize( $this->Application->RecallVar($event->getPrefixSpecial() . '_options') );
		}

		/**
		 * Sets correct available & export fields
		 *
		 * @param kEvent $event
		 */
		function prepareExportColumns($event)
		{
			$object = $event->getObject( Array('skip_autoload' => true) );
			/* @var $object kCatDBItem */

			if ( !$object->isField('ExportColumns') ) {
				// import/export prefix was used (see kDBEventHandler::prepareObject) but object don't plan to be imported/exported
				return ;
			}

			$available_columns = Array();

			if ($this->Application->getUnitOption($event->Prefix, 'CatalogItem')) {
				// category field (mixed)
				$available_columns['__CATEGORY__CategoryPath'] = 'CategoryPath';

				if ($event->Special == 'import') {
					// category field (separated fields)
					$max_level = $this->Application->ConfigValue('MaxImportCategoryLevels');
					$i = 0;
					while ($i < $max_level) {
						$available_columns['__CATEGORY__Category'.($i + 1)] = 'Category'.($i + 1);
						$i++;
					}
				}
			}

			// db fields
			$fields = $object->getFields();

			foreach ($fields as $field_name => $field_options) {
				if ( !$object->skipField($field_name) ) {
					$available_columns[$field_name] = $field_name.( $object->isRequired($field_name) ? '*' : '');
				}
			}

			$handler = $this->Application->recallObject($event->Prefix.'_EventHandler');
			/* @var $handler kDBEventHandler */

			$available_columns = array_merge($available_columns, $handler->getCustomExportColumns($event));

			// custom fields
			$custom_fields = $object->getCustomFields();

			foreach ($custom_fields as $custom_id => $custom_name)
			{
				$available_columns['__CUSTOM__'.$custom_name] = $custom_name;
			}

			// columns already in use
			$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
			if ($items_info)
			{
				list($item_id, $field_values) = each($items_info);
				$export_keys = $field_values['ExportColumns'];
				$export_keys = $export_keys ? explode('|', substr($export_keys, 1, -1) ) : Array();
			}
			else {
				$export_keys = Array();
			}

			$export_columns = Array();
			foreach ($export_keys as $field_key)
			{
				$field_name = $this->getExportField($field_key);
				$export_columns[$field_key] = $field_name;
				unset($available_columns[$field_key]);
			}

			$options = $object->GetFieldOptions('ExportColumns');
			$options['options'] = $export_columns;
			$object->SetFieldOptions('ExportColumns', $options);

			$options = $object->GetFieldOptions('AvailableColumns');
			$options['options'] = $available_columns;
			$object->SetFieldOptions('AvailableColumns', $options);

			$this->updateImportFiles($event);
			$this->PrepareExportPresets($event);
		}

		/**
		 * Prepares export presets
		 *
		 * @param kEvent $event
		 * @return void
		 */
		function PrepareExportPresets($event)
		{
			$object = $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$options = $object->GetFieldOptions('ExportPresets');
			$export_settings = $this->Application->RecallPersistentVar('export_settings');

			if ( !$export_settings ) {
				return;
			}

			$export_settings = unserialize($export_settings);

			if ( !isset($export_settings[$event->Prefix]) ) {
				return;
			}

			$export_presets = array ('' => '');

			foreach ($export_settings[$event->Prefix] as $key => $val) {
				$export_presets[implode('|', $val['ExportColumns'])] = $key;
			}

			$options['options'] = $export_presets;
			$object->SetFieldOptions('ExportPresets', $options);
		}

		function getExportField($field_key)
		{
			$prepends = Array('__CUSTOM__', '__CATEGORY__');
			foreach ($prepends as $prepend)
			{
				if (substr($field_key, 0, strlen($prepend) ) == $prepend)
				{
					$field_key = substr($field_key, strlen($prepend), strlen($field_key) );
					break;
				}
			}
			return $field_key;
		}

		/**
		 * Updates uploaded files list
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function updateImportFiles($event)
		{
			if ( $event->Special != 'import' ) {
				return ;
			}

			$file_helper = $this->Application->recallObject('FileHelper');
			/* @var $file_helper FileHelper */

			$import_filenames = Array ();
			$file_helper->CheckFolder(EXPORT_PATH);

			$iterator = new DirectoryIterator(EXPORT_PATH);
			/* @var $file_info DirectoryIterator */

			foreach ($iterator as $file_info) {
				$file = $file_info->getFilename();

				if ( $file_info->isDir() || $file == 'dummy' || $file_info->getSize() == 0 ) {
					continue;
				}

				$import_filenames[$file] = $file . ' (' . kUtil::formatSize( $file_info->getSize() ) . ')';
			}

			$object = $event->getObject();
			/* @var $object kDBItem */

			$object->SetFieldOption('ImportLocalFilename', 'options', $import_filenames);
		}

		/**
		 * Returns module folder
		 *
		 * @param kEvent $event
		 * @return string
		 */
		function getModuleName($event)
		{
			$module_path = $this->Application->getUnitOption($event->Prefix, 'ModuleFolder') . '/';
			$module_name = $this->Application->findModule('Path', $module_path, 'Name');

			return mb_strtolower($module_name);
		}

		/**
		 * Export form validation & processing
		 *
		 * @param kEvent $event
		 */
		function OnExportBegin($event)
		{
			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

			if ( !$items_info ) {
				$items_info = unserialize($this->Application->RecallVar($event->getPrefixSpecial() . '_ItemsInfo'));
				$this->Application->SetVar($event->getPrefixSpecial(true), $items_info);
			}

			list($item_id, $field_values) = each($items_info);

			$object = $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$object->SetFieldsFromHash($field_values);
			$field_values['ImportFilename'] = $object->GetDBField('ImportFilename'); //if upload formatter has renamed the file during moving !!!

			$object->setID($item_id);
			$this->setRequiredFields($event);

			// save export/import options
			if ( $event->Special == 'export' ) {
				$export_ids = $this->Application->RecallVar($event->Prefix . '_export_ids');
				$export_cats_ids = $this->Application->RecallVar($event->Prefix . '_export_cats_ids');

				// used for multistep export
				$field_values['export_ids'] = $export_ids ? explode(',', $export_ids) : false;
				$field_values['export_cats_ids'] = $export_cats_ids ? explode(',', $export_cats_ids) : Array ($this->Application->GetVar('m_cat_id'));
			}

			$field_values['ExportColumns'] = $field_values['ExportColumns'] ? explode('|', substr($field_values['ExportColumns'], 1, -1) ) : Array();
			$field_values['start_from'] = 0;

			$nevent = new kEvent($event->Prefix . ':OnBeforeExportBegin');
			$nevent->setEventParam('options', $field_values);
			$this->Application->HandleEvent($nevent);
			$field_values = $nevent->getEventParam('options');

			$this->saveOptions($event, $field_values);

			if ( $this->verifyOptions($event) ) {
				if ( $this->_getExportSavePreset($object) ) {
					$name = $object->GetDBField('ExportPresetName');

					$export_settings = $this->Application->RecallPersistentVar('export_settings');
					$export_settings = $export_settings ? unserialize($export_settings) : array ();
					$export_settings[$event->Prefix][$name] = $field_values;
					$this->Application->StorePersistentVar('export_settings', serialize($export_settings));
				}

				$progress_t = $this->Application->RecallVar('export_progress_t');
				if ( $progress_t ) {
					$this->Application->RemoveVar('export_progress_t');
				}
				else {
					$progress_t = $this->getModuleName($event) . '/' . $event->Special . '_progress';
				}
				$event->redirect = $progress_t;

				if ( $event->Special == 'import' ) {
					$import_category = (int)$this->Application->RecallVar('ImportCategory');

					// in future could use module root category if import category will be unavailable :)
					$event->SetRedirectParam('m_cat_id', $import_category); // for template permission checking
					$this->Application->StoreVar('m_cat_id', $import_category); // for event permission checking
				}
			}
			else {
				// make uploaded file local & change source selection
				$filename = getArrayValue($field_values, 'ImportFilename');

				if ( $filename ) {
					$this->updateImportFiles($event);
					$object->SetDBField('ImportSource', 2);
					$field_values['ImportSource'] = 2;
					$object->SetDBField('ImportLocalFilename', $filename);
					$field_values['ImportLocalFilename'] = $filename;
					$this->saveOptions($event, $field_values);
				}

				$event->status = kEvent::erFAIL;
				$event->redirect = false;
			}
		}

		/**
		 * Returns export save preset name, when used at all
		 *
		 * @param kDBItem $object
		 * @return string
		 */
		function _getExportSavePreset(&$object)
		{
			if ( !$object->isField('ExportSavePreset') ) {
				return '';
			}

			return $object->GetDBField('ExportSavePreset');
		}

		/**
		 * set required fields based on import or export params
		 *
		 * @param kEvent $event
		 */
		function setRequiredFields($event)
		{
			$required_fields['common'] = Array('FieldsSeparatedBy', 'LineEndings', 'CategoryFormat');

			$required_fields['export'] = Array('ExportFormat', 'ExportFilename','ExportColumns');

			$object = $event->getObject();
			/* @var $object kDBItem */

			if ($this->_getExportSavePreset($object)) {
				$required_fields['export'][] = 'ExportPresetName';
			}

			$required_fields['import'] = Array('FieldTitles', 'ImportSource', 'CheckDuplicatesMethod'); // ImportFilename, ImportLocalFilename

			if ($event->Special == 'import')
			{
				$import_source = Array(1 => 'ImportFilename', 2 => 'ImportLocalFilename');
				$used_field = $import_source[ $object->GetDBField('ImportSource') ];

				$required_fields[$event->Special][] = $used_field;
				$object->SetFieldOption($used_field, 'error_field', 'ImportSource');

				if ($object->GetDBField('FieldTitles') == 2) $required_fields[$event->Special][] = 'ExportColumns'; // manual field titles
			}

			$required_fields = array_merge($required_fields['common'], $required_fields[$event->Special]);
			$object->setRequired($required_fields);
		}

	}