Index: branches/5.2.x/core/kernel/db/dbitem.php
===================================================================
--- branches/5.2.x/core/kernel/db/dbitem.php	(revision 16017)
+++ branches/5.2.x/core/kernel/db/dbitem.php	(revision 16018)
@@ -1,1597 +1,1599 @@
 <?php
 /**
 * @version	$Id$
 * @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!');
 
 /**
 * DBItem
 *
 */
 class kDBItem extends kDBBase {
 
 	/**
 	* Description
 	*
 	* @var array Associative array of current item' field values
 	* @access protected
 	*/
 	protected $FieldValues = Array ();
 
 	/**
 	 * Unformatted field values, before parse
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected $DirtyFieldValues = Array ();
 
 	/**
 	 * Holds item values after loading (not affected by submit)
 	 *
 	 * @var Array
 	 * @access protected
 	 */
 	protected $OriginalFieldValues = Array ();
 
 	/**
 	* If set to true, Update will skip Validation before running
 	*
 	* @var array Associative array of current item' field values
 	* @access public
 	*/
 	public $IgnoreValidation = false;
 
 	/**
 	 * Remembers if object was loaded
 	 *
 	 * @var bool
 	 * @access protected
 	 */
 	protected $Loaded = false;
 
 	/**
 	* Holds item' primary key value
 	*
 	* @var int Value of primary key field for current item
 	* @access protected
 	*/
 	protected $ID;
 
 	/**
 	 * This object is used in cloning operations
 	 *
 	 * @var bool
 	 * @access public
 	 */
 	public $inCloning = false;
 
 	/**
 	 * Validator object reference
 	 *
 	 * @var kValidator
 	 */
 	protected $validator = null;
 
 	/**
 	 * Creates validator object, only when required
 	 *
 	 */
 	public function initValidator()
 	{
 		if ( !is_object($this->validator) ) {
 			$validator_class = $this->Application->getUnitOption($this->Prefix, 'ValidatorClass', 'kValidator');
 
 			$this->validator = $this->Application->makeClass($validator_class);
 		}
 
 		$this->validator->setDataSource($this);
 	}
 
 	public function SetDirtyField($field_name, $field_value)
 	{
 		$this->DirtyFieldValues[$field_name] = $field_value;
 	}
 
 	public function GetDirtyField($field_name)
 	{
 		return $this->DirtyFieldValues[$field_name];
 	}
 
 	public function GetOriginalField($field_name, $formatted = false, $format=null)
 	{
 		if (array_key_exists($field_name, $this->OriginalFieldValues)) {
 			// item was loaded before
 			$value = $this->OriginalFieldValues[$field_name];
 		}
 		else {
 			// no original fields -> use default field value
 			$value = $this->Fields[$field_name]['default'];
 		}
 
 		if (!$formatted) {
 			return $value;
 		}
 
 		$res = $value;
 		$formatter = $this->GetFieldOption($field_name, 'formatter');
 
 		if ( $formatter ) {
 			$formatter = $this->Application->recallObject($formatter);
 			/* @var $formatter kFormatter */
 
 			$res = $formatter->Format($value, $field_name, $this, $format);
 		}
 
 		return $res;
 	}
 
 	/**
 	 * Sets original field value (useful for custom virtual fields)
 	 *
 	 * @param string $field_name
 	 * @param string $field_value
 	 */
 	public function SetOriginalField($field_name, $field_value)
 	{
 		$this->OriginalFieldValues[$field_name] = $field_value;
 	}
 
 	/**
 	 * Set's default values for all fields
 	 *
 	 * @access public
 	 */
 	public function SetDefaultValues()
 	{
 		parent::SetDefaultValues();
 
 		if ($this->populateMultiLangFields) {
 			$this->PopulateMultiLangFields();
 		}
 
 		foreach ($this->Fields as $field => $field_options) {
 			$default_value = isset($field_options['default']) ? $field_options['default'] : NULL;
 			$this->SetDBField($field, $default_value);
 		}
 	}
 
 	/**
 	* Sets current item field value
 	* (applies formatting)
 	*
 	* @access public
 	* @param string $name Name of the field
 	* @param mixed $value Value to set the field to
 	* @return void
 	*/
 	public function SetField($name,$value)
 	{
 		$options = $this->GetFieldOptions($name);
 		$parsed = $value;
 		if ($value == '') {
 			$parsed = NULL;
 		}
 
 		// kFormatter is always used, to make sure, that numeric value is converted to normal representation
 		// according to regional format, even when formatter is not set (try seting format to 1.234,56 to understand why)
 		$formatter = $this->Application->recallObject(isset($options['formatter']) ? $options['formatter'] : 'kFormatter');
 		/* @var $formatter kFormatter */
 
 		$parsed = $formatter->Parse($value, $name, $this);
 
 		$this->SetDBField($name,$parsed);
 	}
 
 	/**
 	* Sets current item field value
 	* (doesn't apply formatting)
 	*
 	* @access public
 	* @param string $name Name of the field
 	* @param mixed $value Value to set the field to
 	* @return void
 	*/
 	public function SetDBField($name,$value)
 	{
 		$this->FieldValues[$name] = $value;
 	}
 
 	/**
 	 * Set's field error, if pseudo passed not found then create it with message text supplied.
 	 * Don't overwrite existing pseudo translation.
 	 *
 	 * @param string $field
 	 * @param string $pseudo
 	 * @param string $error_label
 	 * @param Array $error_params
 	 *
 	 * @return bool
 	 * @access public
 	 */
 	public function SetError($field, $pseudo, $error_label = null, $error_params = null)
 	{
 		$this->initValidator();
 
 		return $this->validator->SetError($field, $pseudo, $error_label, $error_params);
 	}
 
 	/**
 	 * Removes error on field
 	 *
 	 * @param string $field
 	 * @access public
 	 */
 	public function RemoveError($field)
 	{
 		if ( !is_object($this->validator) ) {
 			return ;
 		}
 
 		$this->validator->RemoveError($field);
 	}
 
 	/**
 	 * Returns error pseudo
 	 *
 	 * @param string $field
 	 * @return string
 	 */
 	public function GetErrorPseudo($field)
 	{
 		if ( !is_object($this->validator) ) {
 			return '';
 		}
 
 		return $this->validator->GetErrorPseudo($field);
 	}
 
 	/**
 	* Return current item' field value by field name
 	* (doesn't apply formatter)
 	*
 	* @param string $name field name to return
 	* @return mixed
 	* @access public
 	*/
 	public function GetDBField($name)
 	{
 		/*if (!array_key_exists($name, $this->FieldValues) && defined('DEBUG_MODE') && DEBUG_MODE) {
 			$this->Application->Debugger->appendTrace();
 		}*/
 
 		return $this->FieldValues[$name];
 	}
 
 	public function HasField($name)
 	{
 		return array_key_exists($name, $this->FieldValues);
 	}
 
 	public function GetFieldValues()
 	{
 		return $this->FieldValues;
 	}
 
 	/**
 	 * Sets item' fields corresponding to elements in passed $hash values.
 	 * The function sets current item fields to values passed in $hash, by matching $hash keys with field names
 	 * of current item. If current item' fields are unknown {@link kDBItem::PrepareFields()} is called before actually setting the fields
 	 *
 	 * @param Array $hash       Fields hash.
 	 * @param Array $set_fields Optional param, field names in target object to set, other fields will be skipped
 	 *
 	 * @return void
 	 */
 	public function SetFieldsFromHash($hash, $set_fields = Array ())
 	{
 		if ( !$set_fields ) {
 			$set_fields = array_keys($hash);
 		}
 
 		$skip_fields = $this->getRequestProtectedFields($hash);
 
 		if ( $skip_fields ) {
 			$set_fields = array_diff($set_fields, $skip_fields);
 		}
 
 		$set_fields = array_intersect($set_fields, array_keys($this->Fields));
 
 		// used in formatter which work with multiple fields together
 		foreach ($set_fields as $field_name) {
 			$this->SetDirtyField($field_name, $hash[$field_name]);
 		}
 
 		// formats all fields using associated formatters
 		foreach ($set_fields as $field_name) {
 			$this->SetField($field_name, $hash[$field_name]);
 		}
 	}
 
 	/**
 	 * Returns fields, that are not allowed to be changed from request.
 	 *
 	 * @param array $fields_hash Fields hash.
 	 *
 	 * @return array
 	 */
 	protected function getRequestProtectedFields(array $fields_hash)
 	{
 		// don't allow changing ID
 		$fields = Array ();
 		$fields[] = $this->Application->getUnitOption($this->Prefix, 'IDField');
 
 		$parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix');
 
 		if ( $parent_prefix && $this->isLoaded() && !$this->Application->isAdmin ) {
 			// don't allow changing foreign key of existing item from request
 			$foreign_key = $this->Application->getUnitOption($this->Prefix, 'ForeignKey');
 			$fields[] = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key;
 		}
 
 		return $fields;
 	}
 
 	/**
 	 * Sets object fields from $hash array
 	 * @param Array $hash
 	 * @param Array|null $set_fields
 	 * @return void
 	 * @access public
 	 */
 	public function SetDBFieldsFromHash($hash, $set_fields = Array ())
 	{
 		if ( !$set_fields ) {
 			$set_fields = array_keys($hash);
 		}
 
 		$set_fields = array_intersect($set_fields, array_keys($this->Fields));
 
 		foreach ($set_fields as $field_name) {
 			$this->SetDBField($field_name, $hash[$field_name]);
 		}
 	}
 
 	/**
 	 * Returns part of SQL WHERE clause identifying the record, ex. id = 25
 	 *
 	 * @param string $method Child class may want to know who called GetKeyClause, Load(), Update(), Delete() send its names as method
 	 * @param Array $keys_hash alternative, then item id, keys hash to load item by
 	 * @see kDBItem::Load()
 	 * @see kDBItem::Update()
 	 * @see kDBItem::Delete()
 	 * @return string
 	 * @access protected
 	 */
 	protected function GetKeyClause($method = null, $keys_hash = null)
 	{
 		if ( !isset($keys_hash) ) {
 			$keys_hash = Array ($this->IDField => $this->ID);
 		}
 
 		$ret = '';
 
 		foreach ($keys_hash as $field => $value) {
 			$value_part = is_null($value) ? ' IS NULL' : ' = ' . $this->Conn->qstr($value);
 
 			$ret .= '(' . (strpos($field, '.') === false ? '`' . $this->TableName . '`.' : '') . $field . $value_part . ') AND ';
 		}
 
 		return substr($ret, 0, -5);
 	}
 
 	/**
 	 * Loads item from the database by given id
 	 *
 	 * @access public
 	 * @param mixed $id item id of keys->values hash to load item by
 	 * @param string $id_field_name Optional parameter to load item by given Id field
 	 * @param bool $cachable cache this query result based on it's prefix serial
 	 * @return bool True if item has been loaded, false otherwise
 	 */
 	public function Load($id, $id_field_name = null, $cachable = false)
 	{
 		if ( isset($id_field_name) ) {
 			$this->IDField = $id_field_name; // set new IDField
 		}
 
 		$keys_sql = '';
 		if (is_array($id)) {
 			$keys_sql = $this->GetKeyClause('load', $id);
 		}
 		else {
 			$this->setID($id);
 			$keys_sql = $this->GetKeyClause('load');
 		}
 
 		if ( isset($id_field_name) ) {
 			// restore original IDField from unit config
 			$this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField');
 		}
 
 		if (($id === false) || !$keys_sql) {
 			return $this->Clear();
 		}
 
 		if (!$this->raiseEvent('OnBeforeItemLoad', $id)) {
 			return false;
 		}
 
 		$q = $this->GetSelectSQL() . ' WHERE ' . $keys_sql;
 
 		if ($cachable && $this->Application->isCachingType(CACHING_TYPE_MEMORY)) {
 			$serial_name = $this->Application->incrementCacheSerial($this->Prefix == 'st' ? 'c' : $this->Prefix, isset($id_field_name) ? null : $id, false);
 			$cache_key = 'kDBItem::Load_' . crc32(serialize($id) . '-' . $this->IDField) . '[%' . $serial_name . '%]';
 			$field_values = $this->Application->getCache($cache_key, false);
 
 			if ($field_values === false) {
 				$field_values = $this->Conn->GetRow($q);
 
 				if ($field_values !== false) {
 					// only cache, when data was retrieved
 					$this->Application->setCache($cache_key, $field_values);
 				}
 			}
 		}
 		else {
 			$field_values = $this->Conn->GetRow($q);
 		}
 
 		if ($field_values) {
 			$this->FieldValues = array_merge($this->FieldValues, $field_values);
 			$this->OriginalFieldValues = $this->FieldValues;
 		}
 		else {
 			return $this->Clear();
 		}
 
 		if (is_array($id) || isset($id_field_name)) {
 			$this->setID($this->FieldValues[$this->IDField]);
 		}
 
 		$this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example)
 
 		$this->raiseEvent('OnAfterItemLoad', $this->GetID());
 		$this->Loaded = true;
 
 		return true;
 	}
 
 	/**
 	 * Loads object from hash (not db)
 	 *
 	 * @param Array $fields_hash
 	 * @param string $id_field
 	 */
 	public function LoadFromHash($fields_hash, $id_field = null)
 	{
 		if (!isset($id_field)) {
 			$id_field = $this->IDField;
 		}
 
 		$this->Clear();
 
 		if (!$fields_hash || !array_key_exists($id_field, $fields_hash)) {
 			// no data OR id field missing
 			return false;
 		}
 
 		$id = $fields_hash[$id_field];
 
 		if ( !$this->raiseEvent('OnBeforeItemLoad', $id) ) {
 			return false;
 		}
 
 		$this->FieldValues = array_merge($this->FieldValues, $fields_hash);
 		$this->OriginalFieldValues = $this->FieldValues;
 
 		$this->setID($id);
 		$this->UpdateFormattersSubFields(); // used for updating separate virtual date/time fields from DB timestamp (for example)
 
 		$this->raiseEvent('OnAfterItemLoad', $id);
 
 		$this->Loaded = true;
 
 		return true;
 	}
 
 	/**
 	* Builds select sql, SELECT ... FROM parts only
 	*
 	* @access public
 	* @return string
 	*/
 
 	/**
 	 * Returns SELECT part of list' query
 	 *
 	 * @param string $base_query
 	 * @param bool $replace_table
 	 * @return string
 	 * @access public
 	 */
 	public function GetSelectSQL($base_query = null, $replace_table = true)
 	{
 		if (!isset($base_query)) {
 			$base_query = $this->SelectClause;
 		}
 
 		$base_query = $this->addCalculatedFields($base_query);
 
 		return parent::GetSelectSQL($base_query, $replace_table);
 	}
 
 	public function UpdateFormattersMasterFields()
 	{
 		$this->initValidator(); // used, when called not from kValidator::Validate method
 
 		foreach ($this->Fields as $field => $options) {
 			if ( isset($options['formatter']) ) {
 				$formatter = $this->Application->recallObject($options['formatter']);
 				/* @var $formatter kFormatter */
 
 				$formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this);
 			}
 		}
 	}
 
 	/**
 	 * Returns variable name, used to store pending file actions
 	 *
 	 * @return string
 	 * @access protected
 	 */
 	protected function _getPendingActionVariableName()
 	{
 		$window_id = $this->Application->GetTopmostWid($this->Prefix);
 
 		return $this->Prefix . '_file_pending_actions' . $window_id;
 	}
 
 	/**
 	 * Returns pending actions
 	 *
 	 * @param mixed $id
 	 * @return Array
 	 * @access public
 	 */
 	public function getPendingActions($id = null)
 	{
 		if ( !isset($id) ) {
 			$id = $this->GetID();
 		}
 
 		$pending_actions = $this->Application->RecallVar($this->_getPendingActionVariableName());
 		$pending_actions = $pending_actions ? unserialize($pending_actions) : Array ();
 
 		if ( is_numeric($id) ) {
 			// filter by given/current id
 			$ret = Array ();
 
 			foreach ($pending_actions as $pending_action) {
 				if ( $pending_action['id'] == $id ) {
 					$ret[] = $pending_action;
 				}
 			}
 
 			return $ret;
 		}
 
 		return $pending_actions;
 	}
 
 	/**
 	 * Sets new pending actions
 	 *
 	 * @param Array|null $new_pending_actions
 	 * @param mixed $id
 	 * @return void
 	 * @access public
 	 */
 	public function setPendingActions($new_pending_actions = null, $id = null)
 	{
 		if ( !isset($new_pending_actions) ) {
 			$new_pending_actions = Array ();
 		}
 
 		if ( !isset($id) ) {
 			$id = $this->GetID();
 		}
 
 		$pending_actions = Array ();
 		$old_pending_actions = $this->getPendingActions(true);
 
 		if ( is_numeric($id) ) {
 			// remove old actions for this id
 			foreach ($old_pending_actions as $pending_action) {
 				if ( $pending_action['id'] != $id ) {
 					$pending_actions[] = $pending_action;
 				}
 			}
 
 			// add new actions for this id
 			$pending_actions = array_merge($pending_actions, $new_pending_actions);
 		}
 		else {
 			$pending_actions = $new_pending_actions;
 		}
 
 		// save changes
 		$var_name = $this->_getPendingActionVariableName();
 
 		if ( !$pending_actions ) {
 			$this->Application->RemoveVar($var_name);
 		}
 		else {
 			$this->Application->StoreVar($var_name, serialize($this->sortPendingActions($pending_actions)));
 		}
 	}
 
 	/**
 	 * Sorts pending actions the way, that `delete` action will come before other actions.
 	 *
 	 * @param array $pending_actions Pending actions.
 	 *
 	 * @return array
 	 */
 	protected function sortPendingActions(array $pending_actions)
 	{
 		usort($pending_actions, array($this, 'comparePendingActions'));
 
 		return $pending_actions;
 	}
 
 	protected function comparePendingActions($pending_action_a, $pending_action_b)
 	{
 		if ( $pending_action_a['action'] == $pending_action_b['action'] ) {
 			return 0;
 		}
 
 		return $pending_action_a['action'] == 'delete' ? -1 : 1;
 	}
 
 	/**
 	 * Allows to skip certain fields from getting into sql queries
 	 *
 	 * @param string $field_name
 	 * @param mixed $force_id
 	 * @return bool
 	 */
 	public function skipField($field_name, $force_id = false)
 	{
 		$skip = false;
 
 		// 1. skipping 'virtual' field
 		$skip = $skip || array_key_exists($field_name, $this->VirtualFields);
 
 		// 2. don't write empty field value to db, when "skip_empty" option is set
 		$field_value = array_key_exists($field_name, $this->FieldValues) ? $this->FieldValues[$field_name] : false;
 
 		if (array_key_exists($field_name, $this->Fields)) {
 			$skip_empty = array_key_exists('skip_empty', $this->Fields[$field_name]) ? $this->Fields[$field_name]['skip_empty'] : false;
 		}
 		else {
 			// field found in database, but not declared in unit config
 			$skip_empty = false;
 		}
 
 		$skip = $skip || (!$field_value && $skip_empty);
 
 		// 3. skipping field not in Fields (nor virtual, nor real)
 		$skip = $skip || !array_key_exists($field_name, $this->Fields);
 
 		return $skip;
 	}
 
 	/**
 	 * Updates previously loaded record with current item' values
 	 *
 	 * @access public
 	 * @param int $id Primary Key Id to update
 	 * @param Array $update_fields
 	 * @param bool $system_update
 	 * @return bool
 	 * @access public
 	 */
 	public function Update($id = null, $update_fields = null, $system_update = false)
 	{
 		if ( isset($id) ) {
 			$this->setID($id);
 		}
 
 		if ( !$this->raiseEvent('OnBeforeItemUpdate') ) {
 			return false;
 		}
 
 		if ( !isset($this->ID) ) {
 			// ID could be set inside OnBeforeItemUpdate event, so don't combine this check with previous one
 			return false;
 		}
 
 		// validate before updating
 		if ( !$this->Validate() ) {
 			return false;
 		}
 
 		if ( !$this->FieldValues ) {
 			// nothing to update
 			return true;
 		}
 
 		$sql = '';
 
 		$set_fields = isset($update_fields) ? $update_fields : array_keys($this->FieldValues);
 
 		foreach ($set_fields as $field_name) {
 			if ( $this->skipField($field_name) ) {
 				continue;
 			}
 
 			$field_value = $this->FieldValues[$field_name];
 
 			if ( is_null($field_value) ) {
 				if ( array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null'] ) {
 					// "kFormatter::Parse" methods converts empty values to NULL and for
 					// not-null fields they are replaced with default value here
 					$field_value = $this->Fields[$field_name]['default'];
 				}
 			}
 
 			$sql .= '`' . $field_name . '` = ' . $this->Conn->qstr($field_value) . ', ';
 		}
 
 		$sql = 'UPDATE ' . $this->TableName . '
 				SET ' . substr($sql, 0, -2) . '
 				WHERE ' . $this->GetKeyClause('update');
 
 		if ( $this->Conn->ChangeQuery($sql) === false ) {
 			// there was and sql error
 			$this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg());
 			return false;
 		}
 
 		$affected_rows = $this->Conn->getAffectedRows();
 
 		if ( !$system_update && ($affected_rows > 0) ) {
 			$this->setModifiedFlag(ChangeLog::UPDATE);
 		}
 
 		$this->saveCustomFields();
 		$this->raiseEvent('OnAfterItemUpdate');
 
 		if ( !isset($update_fields) ) {
 			$this->OriginalFieldValues = $this->FieldValues;
 		}
 		else {
 			foreach ($update_fields as $update_field) {
 				$this->OriginalFieldValues[$update_field] = $this->FieldValues[$update_field];
 			}
 		}
 
 		$this->Loaded = true;
 
 		if ( !$this->IsTempTable() ) {
 			$this->Application->resetCounters($this->TableName);
 		}
 
 		return true;
 	}
 
 	/**
 	 * Validates given field
 	 *
 	 * @param string $field
 	 * @return bool
 	 * @access public
 	 */
 	public function ValidateField($field)
 	{
 		$this->initValidator();
 
 		return $this->validator->ValidateField($field);
 	}
 
 	/**
 	 * Validate all item fields based on
 	 * constraints set in each field options
 	 * in config
 	 *
 	 * @return bool
 	 * @access private
 	 */
 	public function Validate()
 	{
 		if ( $this->IgnoreValidation ) {
 			return true;
 		}
 
 		$this->initValidator();
 
 		// will apply any custom validation to the item
 		$this->raiseEvent('OnBeforeItemValidate');
 
 		if ( $this->validator->Validate() ) {
 			// no validation errors
 			$this->raiseEvent('OnAfterItemValidate');
 
 			return true;
 		}
 
 		return false;
 	}
 
 	/**
 	 * Check if item has errors
 	 *
 	 * @param Array $skip_fields fields to skip during error checking
 	 * @return bool
 	 */
 	public function HasErrors($skip_fields = Array ())
 	{
 		if ( !is_object($this->validator) ) {
 			return false;
 		}
 
 		return $this->validator->HasErrors($skip_fields);
 	}
 
 	/**
 	 * Check if value is set for required field
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 * @access public
 	 * @todo Find a way to get rid of direct call from kMultiLanguage::UpdateMasterFields method
 	 */
 	public function ValidateRequired($field, $params)
 	{
 		return $this->validator->ValidateRequired($field, $params);
 	}
 
 	/**
 	 * Return error message for field
 	 *
 	 * @param string $field
 	 * @param bool $force_escape
 	 * @return string
 	 * @access public
 	 */
 	public function GetErrorMsg($field, $force_escape = null)
 	{
 		if ( !is_object($this->validator) ) {
 			return '';
 		}
 
 		return $this->validator->GetErrorMsg($field, $force_escape);
 	}
 
 	/**
 	 * Returns field errors
 	 *
 	 * @return Array
 	 * @access public
 	 */
 	public function GetFieldErrors()
 	{
 		if ( !is_object($this->validator) ) {
 			return Array ();
 		}
 
 		return $this->validator->GetFieldErrors();
 	}
 
 	/**
 	 * Creates a record in the database table with current item' values
 	 *
 	 * @param mixed $force_id Set to TRUE to force creating of item's own ID or to value to force creating of passed id. Do not pass 1 for true, pass exactly TRUE!
 	 * @param bool $system_create
 	 * @return bool
 	 * @access public
 	 */
 	public function Create($force_id = false, $system_create = false)
 	{
 		if (!$this->raiseEvent('OnBeforeItemCreate')) {
 			return false;
 		}
 
 		// Validating fields before attempting to create record
 		if (!$this->Validate()) {
 			return false;
 		}
 
 		if (is_int($force_id)) {
 			$this->FieldValues[$this->IDField] = $force_id;
 		}
 		elseif (!$force_id || !is_bool($force_id)) {
 			$this->FieldValues[$this->IDField] = $this->generateID();
 		}
 
 		$fields_sql = '';
 		$values_sql = '';
 		foreach ($this->FieldValues as $field_name => $field_value) {
 			if ($this->skipField($field_name, $force_id)) {
 				continue;
 			}
 
 			if (is_null($field_value)) {
 				if (array_key_exists('not_null', $this->Fields[$field_name]) && $this->Fields[$field_name]['not_null']) {
 					// "kFormatter::Parse" methods converts empty values to NULL and for
 					// not-null fields they are replaced with default value here
 					$values_sql .= $this->Conn->qstr($this->Fields[$field_name]['default']);
 				}
 				else {
 					$values_sql .= $this->Conn->qstr($field_value);
 				}
 			}
 			else {
 				if (($field_name == $this->IDField) && ($field_value == 0) && !is_int($force_id)) {
 					// don't skip IDField in INSERT statement, just use DEFAULT keyword as it's value
 					$values_sql .= 'DEFAULT';
 				}
 				else {
 					$values_sql .= $this->Conn->qstr($field_value);
 				}
 			}
 
 			$fields_sql .= '`' . $field_name . '`, '; //Adding field name to fields block of Insert statement
 			$values_sql .= ', ';
 		}
 
 		$sql = 'INSERT INTO ' . $this->TableName . ' (' . substr($fields_sql, 0, -2) . ')
 				VALUES (' . substr($values_sql, 0, -2) . ')';
 
 		//Executing the query and checking the result
 		if ($this->Conn->ChangeQuery($sql) === false) {
 			$this->SetError($this->IDField, 'sql_error', '#' . $this->Conn->getErrorCode() . ': ' . $this->Conn->getErrorMsg());
 			return false;
 		}
 
 		$insert_id = $this->Conn->getInsertID();
 		if ($insert_id == 0) {
 			// insert into temp table (id is not auto-increment field)
 			$insert_id = $this->FieldValues[$this->IDField];
 		}
+
+		$temp_id = $this->GetID();
 		$this->setID($insert_id);
 
 		$this->OriginalFieldValues = $this->FieldValues;
 
 		if (!$system_create){
 			$this->setModifiedFlag(ChangeLog::CREATE);
 		}
 
 		$this->saveCustomFields();
 		if (!$this->IsTempTable()) {
 			$this->Application->resetCounters($this->TableName);
 		}
 
 		if ($this->IsTempTable() && ($this->Application->GetTopmostPrefix($this->Prefix) != $this->Prefix) && !is_int($force_id)) {
 			// temp table + subitem = set negative id
 			$this->setTempID();
 		}
 
-		$this->raiseEvent('OnAfterItemCreate');
+		$this->raiseEvent('OnAfterItemCreate', null, array('temp_id' => $temp_id));
 		$this->Loaded = true;
 
 		return true;
 	}
 
 	/**
 	 * Deletes the record from database
 	 *
 	 * @param int $id
 	 * @return bool
 	 * @access public
 	 */
 	public function Delete($id = null)
 	{
 		if ( isset($id) ) {
 			$this->setID($id);
 		}
 
 		if ( !$this->raiseEvent('OnBeforeItemDelete') ) {
 			return false;
 		}
 
 		$sql = 'DELETE FROM ' . $this->TableName . '
 				WHERE ' . $this->GetKeyClause('Delete');
 
 		$ret = $this->Conn->ChangeQuery($sql);
 		$affected_rows = $this->Conn->getAffectedRows();
 
 		if ( $affected_rows > 0 ) {
 			$this->setModifiedFlag(ChangeLog::DELETE); // will change affected rows, so get it before this line
 
 			// something was actually deleted
 			$this->raiseEvent('OnAfterItemDelete');
 		}
 
 		if ( !$this->IsTempTable() ) {
 			$this->Application->resetCounters($this->TableName);
 		}
 
 		return $ret;
 	}
 
 	public function PopulateMultiLangFields()
 	{
 		foreach ($this->Fields as $field => $options) {
 			// master field is set only for CURRENT language
 			$formatter = array_key_exists('formatter', $options) ? $options['formatter'] : false;
 
 			if ( ($formatter == 'kMultiLanguage') && isset($options['master_field']) && isset($options['error_field']) ) {
 				// MuliLanguage formatter sets error_field to master_field, but in PopulateMlFields mode,
 				// we display ML fields directly so we set it back to itself, otherwise error won't be displayed
 				unset( $this->Fields[$field]['error_field'] );
 			}
 		}
 	}
 
 	/**
 	 * Sets new name for item in case if it is being copied in same table
 	 *
 	 * @param array $master Table data from TempHandler
 	 * @param int $foreign_key ForeignKey value to filter name check query by
 	 * @param string $title_field FieldName to alter, by default - TitleField of the prefix
 	 * @param string $format sprintf-style format of renaming pattern, by default Copy %1$s of %2$s which makes it Copy [Number] of Original Name
 	 * @access public
 	 */
 	public function NameCopy($master=null, $foreign_key=null, $title_field=null, $format='Copy %1$s of %2$s')
 	{
 		if (!isset($title_field)) {
 			$title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField');
 			if (!$title_field || isset($this->CalculatedFields[$title_field]) ) return;
 		}
 
 		$new_name = $this->GetDBField($title_field);
 		$original_checked = false;
 		do {
 			if ( preg_match('/'.sprintf($format, '([0-9]*) *', '(.*)').'/', $new_name, $regs) ) {
 				$new_name = sprintf($format, ($regs[1]+1), $regs[2]);
 			}
 			elseif ($original_checked) {
 				$new_name = sprintf($format, '', $new_name);
 			}
 
 			// if we are cloning in temp table this will look for names in temp table,
 			// since object' TableName contains correct TableName (for temp also!)
 			// if we are cloning live - look in live
 			$query = 'SELECT '.$title_field.' FROM '.$this->TableName.'
 								WHERE '.$title_field.' = '.$this->Conn->qstr($new_name);
 
 			$foreign_key_field = getArrayValue($master, 'ForeignKey');
 			$foreign_key_field = is_array($foreign_key_field) ? $foreign_key_field[ $master['ParentPrefix'] ] : $foreign_key_field;
 
 			if ($foreign_key_field && isset($foreign_key)) {
 				$query .= ' AND '.$foreign_key_field.' = '.$foreign_key;
 			}
 
 			$res = $this->Conn->GetOne($query);
 
 			/*// if not found in live table, check in temp table if applicable
 			if ($res === false && $object->Special == 'temp') {
 				$query = 'SELECT '.$name_field.' FROM '.$this->GetTempName($master['TableName']).'
 									WHERE '.$name_field.' = '.$this->Conn->qstr($new_name);
 				$res = $this->Conn->GetOne($query);
 			}*/
 
 			$original_checked = true;
 		} while ($res !== false);
 		$this->SetDBField($title_field, $new_name);
 	}
 
 	protected function raiseEvent($name, $id = null, $additional_params = Array())
 	{
 		$additional_params['id'] = isset($id) ? $id : $this->GetID();
 		$event = new kEvent($this->getPrefixSpecial() . ':' . $name, $additional_params);
 
 		if ( is_object($this->parentEvent) ) {
 			$event->MasterEvent = $this->parentEvent;
 		}
 
 		$this->Application->HandleEvent($event);
 
 		return $event->status == kEvent::erSUCCESS;
 	}
 
 	/**
 	 * Set's new ID for item
 	 *
 	 * @param int $new_id
 	 * @access public
 	 */
 	public function setID($new_id)
 	{
 		$this->ID = $new_id;
 		$this->SetDBField($this->IDField, $new_id);
 	}
 
 	/**
 	 * Generate and set new temporary id
 	 *
 	 * @access private
 	 */
 	public function setTempID()
 	{
 		$new_id = (int)$this->Conn->GetOne('SELECT MIN('.$this->IDField.') FROM '.$this->TableName);
 		if($new_id > 0) $new_id = 0;
 		--$new_id;
 
 		$this->Conn->Query('UPDATE '.$this->TableName.' SET `'.$this->IDField.'` = '.$new_id.' WHERE `'.$this->IDField.'` = '.$this->GetID());
 
 		if ($this->ShouldLogChanges(true)) {
 			// Updating TempId in ChangesLog, if changes are disabled
 			$ses_var_name = $this->Application->GetTopmostPrefix($this->Prefix) . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
 			$changes = $this->Application->RecallVar($ses_var_name);
 			$changes = $changes ? unserialize($changes) : Array ();
 
 			if ($changes) {
 				foreach ($changes as $key => $rec) {
 					if ($rec['Prefix'] == $this->Prefix && $rec['ItemId'] == $this->GetID()) {
 						// change log for record, that's ID was just updated -> update in change log record too
 						$changes[$key]['ItemId'] = $new_id;
 					}
 
 					if ($rec['MasterPrefix'] == $this->Prefix && $rec['MasterId'] == $this->GetID()) {
 						// master item id was changed
 						$changes[$key]['MasterId'] = $new_id;
 					}
 
 					if (in_array($this->Prefix, $rec['ParentPrefix']) && $rec['ParentId'][$this->Prefix] == $this->GetID()) {
 						// change log record of given item's sub item -> update changed id's in dependent fields
 						$changes[$key]['ParentId'][$this->Prefix] = $new_id;
 
 						if (array_key_exists('DependentFields', $rec)) {
 							// these are fields from table of $rec['Prefix'] table!
 							// when one of dependent fields goes into idfield of it's parent item, that was changed
 							$parent_table_key = $this->Application->getUnitOption($rec['Prefix'], 'ParentTableKey');
 							$parent_table_key = is_array($parent_table_key) ? $parent_table_key[$this->Prefix] : $parent_table_key;
 
 							if ($parent_table_key == $this->IDField) {
 								$foreign_key = $this->Application->getUnitOption($rec['Prefix'], 'ForeignKey');
 								$foreign_key = is_array($foreign_key) ? $foreign_key[$this->Prefix] : $foreign_key;
 
 								$changes[$key]['DependentFields'][$foreign_key] = $new_id;
 							}
 						}
 					}
 				}
 			}
 
 			$this->Application->StoreVar($ses_var_name, serialize($changes));
 		}
 
 		$this->SetID($new_id);
 	}
 
 	/**
 	 * Set's modification flag for main prefix of current prefix to true
 	 *
 	 * @param int $mode
 	 * @access private
 	 */
 	public function setModifiedFlag($mode = null)
 	{
 		$main_prefix = $this->Application->GetTopmostPrefix($this->Prefix);
 		$this->Application->StoreVar($main_prefix . '_modified', '1', true); // true for optional
 
 		if ($this->ShouldLogChanges(true)) {
 			$this->LogChanges($main_prefix, $mode);
 
 			if (!$this->IsTempTable()) {
 				$handler = $this->Application->recallObject($this->Prefix . '_EventHandler');
 				/* @var $handler kDBEventHandler */
 
 				$ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
 				$handler->SaveLoggedChanges($ses_var_name, $this->ShouldLogChanges());
 			}
 		}
 	}
 
 	/**
 	 * Determines, that changes made to this item should be written to change log
 	 *
 	 * @param bool $log_changes
 	 * @return bool
 	 */
 	public function ShouldLogChanges($log_changes = null)
 	{
 		if (!isset($log_changes)) {
 			// specific logging mode no forced -> use global logging settings
 			$log_changes = $this->Application->getUnitOption($this->Prefix, 'LogChanges') || $this->Application->ConfigValue('UseChangeLog');
 		}
 
 		return $log_changes && !$this->Application->getUnitOption($this->Prefix, 'ForceDontLogChanges');
 	}
 
 	protected function LogChanges($main_prefix, $mode)
 	{
 		if ( !$mode ) {
 			return ;
 		}
 
 		$ses_var_name = $main_prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
 		$changes = $this->Application->RecallVar($ses_var_name);
 		$changes = $changes ? unserialize($changes) : Array ();
 
 		$fields_hash = Array (
 			'Prefix' => $this->Prefix,
 			'ItemId' => $this->GetID(),
 			'OccuredOn' => adodb_mktime(),
 			'MasterPrefix' => $main_prefix,
 			'Action' => $mode,
 		);
 
 		if ( $this->Prefix == $main_prefix ) {
 			// main item
 			$fields_hash['MasterId'] = $this->GetID();
 			$fields_hash['ParentPrefix'] = Array ($main_prefix);
 			$fields_hash['ParentId'] = Array ($main_prefix => $this->GetID());
 		}
 		else {
 			// sub item
 			// collect foreign key values (for serial reset)
 			$foreign_keys = $this->Application->getUnitOption($this->Prefix, 'ForeignKey', Array ());
 			$dependent_fields = $fields_hash['ParentId'] = $fields_hash['ParentPrefix'] = Array ();
 			/* @var $foreign_keys Array */
 
 			if ( is_array($foreign_keys) ) {
 				foreach ($foreign_keys as $prefix => $field_name) {
 					$dependent_fields[$field_name] = $this->GetDBField($field_name);
 					$fields_hash['ParentPrefix'][] = $prefix;
 					$fields_hash['ParentId'][$prefix] = $this->getParentId($prefix);
 				}
 			}
 			else {
 				$dependent_fields[$foreign_keys] = $this->GetDBField($foreign_keys);
 				$fields_hash['ParentPrefix'] = Array ( $this->Application->getUnitOption($this->Prefix, 'ParentPrefix') );
 				$fields_hash['ParentId'][ $fields_hash['ParentPrefix'][0] ] = $this->getParentId('auto');
 			}
 
 			$fields_hash['DependentFields'] = $dependent_fields;
 
 
 			// works only, when main item is present in url, when sub-item is changed
 			$master_id = $this->Application->GetVar($main_prefix . '_id');
 
 			if ( $master_id === false ) {
 				// works in case of we are not editing topmost item, when sub-item is created/updated/deleted
 				$master_id = $this->getParentId('auto', true);
 			}
 
 			$fields_hash['MasterId'] = $master_id;
 		}
 
 		switch ( $mode ) {
 			case ChangeLog::UPDATE:
 				$to_save = array_merge($this->GetTitleField(), $this->GetChangedFields());
 				break;
 
 			case ChangeLog::CREATE:
 				$to_save = $this->GetTitleField();
 				break;
 
 			case ChangeLog::DELETE:
 				$to_save = array_merge($this->GetTitleField(), $this->GetRealFields());
 				break;
 
 			default:
 				$to_save = Array ();
 				break;
 		}
 
 		$fields_hash['Changes'] = serialize($to_save);
 		$changes[] = $fields_hash;
 
 		$this->Application->StoreVar($ses_var_name, serialize($changes));
 	}
 
 	/**
 	 * Returns current item parent's ID
 	 *
 	 * @param string $parent_prefix
 	 * @param bool $top_most return topmost parent, when used
 	 * @return int
 	 * @access public
 	 */
 	public function getParentId($parent_prefix, $top_most = false)
 	{
 		$current_id = $this->GetID();
 		$current_prefix = $this->Prefix;
 
 		if ($parent_prefix == 'auto') {
 			$parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix');
 		}
 
 		if (!$parent_prefix) {
 			return $current_id;
 		}
 
 		do {
 			// field in this table
 			$foreign_key = $this->Application->getUnitOption($current_prefix, 'ForeignKey');
 			$foreign_key = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key;
 
 			// get foreign key value for $current_prefix
 			if ($current_prefix == $this->Prefix) {
 				$foreign_key_value = $this->GetDBField($foreign_key);
 			}
 			else {
 				$id_field = $this->Application->getUnitOption($current_prefix, 'IDField');
 				$table_name = $this->Application->getUnitOption($current_prefix, 'TableName');
 
 				if ($this->IsTempTable()) {
 					$table_name = $this->Application->GetTempName($table_name, 'prefix:' . $current_prefix);
 				}
 
 				$sql = 'SELECT ' . $foreign_key . '
 						FROM ' . $table_name . '
 						WHERE ' . $id_field . ' = ' . $current_id;
 				$foreign_key_value = $this->Conn->GetOne($sql);
 			}
 
 			// field in parent table
 			$parent_table_key = $this->Application->getUnitOption($current_prefix, 'ParentTableKey');
 			$parent_table_key = is_array($parent_table_key) ? $parent_table_key[$parent_prefix] : $parent_table_key;
 
 			$parent_id_field = $this->Application->getUnitOption($parent_prefix, 'IDField');
 			$parent_table_name = $this->Application->getUnitOption($parent_prefix, 'TableName');
 
 			if ($this->IsTempTable()) {
 				$parent_table_name = $this->Application->GetTempName($parent_table_name, 'prefix:' . $current_prefix);
 			}
 
 			if ($parent_id_field == $parent_table_key) {
 				// sub-item is related by parent item idfield
 				$current_id = $foreign_key_value;
 			}
 			else {
 				// sub-item is related by other parent item field
 				$sql = 'SELECT ' . $parent_id_field . '
 						FROM ' . $parent_table_name . '
 						WHERE ' . $parent_table_key . ' = ' . $foreign_key_value;
 				$current_id = $this->Conn->GetOne($sql);
 			}
 
 			$current_prefix = $parent_prefix;
 
 			if (!$top_most) {
 				break;
 			}
 		} while ( $parent_prefix = $this->Application->getUnitOption($current_prefix, 'ParentPrefix') );
 
 		return $current_id;
 	}
 
 	/**
 	 * Returns title field (if any)
 	 *
 	 * @return Array
 	 */
 	public function GetTitleField()
 	{
 		$title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField');
 
 		if ($title_field) {
 			$value = $this->GetField($title_field);
 			return $value ? Array ($title_field => $value) : Array ();
 		}
 
 		return Array ();
 	}
 
 	/**
 	 * Returns only fields, that are present in database (no virtual and no calculated fields)
 	 *
 	 * @return Array
 	 */
 	public function GetRealFields()
 	{
 		return array_diff_key($this->FieldValues, $this->VirtualFields, $this->CalculatedFields);
 	}
 
 	/**
 	 * Returns only changed database field
 	 *
 	 * @param bool $include_virtual_fields
 	 * @return Array
 	 */
 	public function GetChangedFields($include_virtual_fields = false)
 	{
 		$changes = Array ();
 		$fields = $include_virtual_fields ? $this->FieldValues : $this->GetRealFields();
 		$diff = array_diff_assoc($fields, $this->OriginalFieldValues);
 
 		foreach ($diff as $field => $new_value) {
 			$old_value = $this->GetOriginalField($field, true);
 			$new_value = $this->GetField($field);
 
 			if ($old_value != $new_value) {
 				// "0.00" and "0.0000" are stored as strings and will differ. Double check to prevent that.
 				$changes[$field] = Array ('old' => $old_value, 'new' => $new_value);
 			}
 		}
 
 		return $changes;
 	}
 
 	/**
 	 * Returns ID of currently processed record
 	 *
 	 * @return int
 	 * @access public
 	 */
 	public function GetID()
 	{
 		return $this->ID;
 	}
 
 	/**
 	 * Generates ID for new items before inserting into database
 	 *
 	 * @return int
 	 * @access private
 	 */
 	protected function generateID()
 	{
 		return 0;
 	}
 
 	/**
 	 * Returns true if item was loaded successfully by Load method
 	 *
 	 * @return bool
 	 */
 	public function isLoaded()
 	{
 		return $this->Loaded;
 	}
 
 	/**
 	 * Checks if field is required
 	 *
 	 * @param string $field
 	 * @return bool
 	 */
 	public function isRequired($field)
 	{
 		return isset($this->Fields[$field]['required']) && $this->Fields[$field]['required'];
 	}
 
 	/**
 	 * Sets new required flag to field
 	 *
 	 * @param mixed $fields
 	 * @param bool $is_required
 	 */
 	public function setRequired($fields, $is_required = true)
 	{
 		if ( !is_array($fields) ) {
 			$fields = explode(',', $fields);
 		}
 
 		foreach ($fields as $field) {
 			$this->Fields[$field]['required'] = $is_required;
 		}
 	}
 
 	/**
 	 * Removes all data from an object
 	 *
 	 * @param int $new_id
 	 * @return bool
 	 * @access public
 	 */
 	public function Clear($new_id = null)
 	{
 		$this->Loaded = false;
 		$this->FieldValues = $this->OriginalFieldValues = Array ();
 		$this->SetDefaultValues(); // will wear off kDBItem::setID effect, so set it later
 
 		if ( is_object($this->validator) ) {
 			$this->validator->reset();
 		}
 
 		$this->setID($new_id);
 
 		return $this->Loaded;
 	}
 
 	public function Query($force = false)
 	{
 		throw new Exception('<b>Query</b> method is called in class <strong>' . get_class($this) . '</strong> for prefix <strong>' . $this->getPrefixSpecial() . '</strong>');
 	}
 
 	protected function saveCustomFields()
 	{
 		if ( !$this->customFields || $this->inCloning ) {
 			return true;
 		}
 
 		$cdata_key = rtrim($this->Prefix . '-cdata.' . $this->Special, '.');
 
 		$cdata = $this->Application->recallObject($cdata_key, null, Array ('skip_autoload' => true));
 		/* @var $cdata kDBItem */
 
 		$resource_id = $this->GetDBField('ResourceId');
 		$cdata->Load($resource_id, 'ResourceId');
 		$cdata->SetDBField('ResourceId', $resource_id);
 
 		$ml_formatter = $this->Application->recallObject('kMultiLanguage');
 		/* @var $ml_formatter kMultiLanguage */
 
 		$ml_helper = $this->Application->recallObject('kMultiLanguageHelper');
 		/* @var $ml_helper kMultiLanguageHelper */
 
 		$languages = $ml_helper->getLanguages();
 
 		foreach ($this->customFields as $custom_id => $custom_name) {
 			$force_primary = $cdata->GetFieldOption('cust_' . $custom_id, 'force_primary');
 
 			if ( $force_primary ) {
 				$cdata->SetDBField($ml_formatter->LangFieldName('cust_' . $custom_id, true), $this->GetDBField('cust_' . $custom_name));
 			}
 			else {
 				foreach ($languages as $language_id) {
 					$cdata->SetDBField('l' . $language_id . '_cust_' . $custom_id, $this->GetDBField('l' . $language_id . '_cust_' . $custom_name));
 				}
 			}
 		}
 
 		return $cdata->isLoaded() ? $cdata->Update() : $cdata->Create();
 	}
 
 	/**
 	 * Returns specified field value from all selected rows.
 	 * Don't affect current record index
 	 *
 	 * @param string $field
 	 * @param bool $formatted
 	 * @param string $format
 	 * @return Array
 	 */
 	public function GetCol($field, $formatted = false, $format = null)
 	{
 		if ($formatted) {
 			return Array (0 => $this->GetField($field, $format));
 		}
 
 		return Array (0 => $this->GetDBField($field));
 	}
 
 	/**
 	 * Set's loaded status of object
 	 *
 	 * @param bool $is_loaded
 	 * @access public
 	 * @todo remove this method, since item can't be marked as loaded externally
 	 */
 	public function setLoaded($is_loaded = true)
 	{
 		$this->Loaded = $is_loaded;
 	}
 
 	/**
 	 * Returns item's first status field
 	 *
 	 * @return string
 	 * @access public
 	 */
 	public function getStatusField()
 	{
 		$status_fields = $this->Application->getUnitOption($this->Prefix, 'StatusField');
 
 		return array_shift($status_fields);
 	}
 
 }