Index: branches/5.0.x/core/kernel/db/dbitem.php
===================================================================
--- branches/5.0.x/core/kernel/db/dbitem.php	(revision 13536)
+++ branches/5.0.x/core/kernel/db/dbitem.php	(revision 13537)
@@ -1,1306 +1,1311 @@
 <?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
 *
 * Desciption
 * @package kernel4
 */
 class kDBItem extends kDBBase {
 
 	/**
 	* Description
 	*
 	* @var array Associative array of current item' field values
 	* @access public
 	*/
 	var $FieldValues;
 
 	/**
 	 * Unformatted field values, before parse
 	 *
 	 * @var Array
 	 * @access private
 	 */
 	var $DirtyFieldValues = Array();
 
 	/**
 	 * Holds item values after loading (not affected by submit)
 	 *
 	 * @var Array
 	 * @access private
 	 */
 	var $OriginalFieldValues = Array ();
 
 	var $FieldErrors;
 
 	var $ErrorMsgs = Array();
 
 	/**
 	* If set to true, Update will skip Validation before running
 	*
 	* @var array Associative array of current item' field values
 	* @access public
 	*/
 	var $IgnoreValidation = false;
 
 	var $Loaded = false;
 
 	/**
 	* Holds item' primary key value
 	*
 	* @var int Value of primary key field for current item
 	* @access public
 	*/
 	var $ID;
 
 	/**
 	 * This object is used in cloning operations
 	 *
 	 * @var bool
 	 */
 	var $inCloning = false;
 
 	function kDBItem()
 	{
 		parent::kDBBase();
 		$this->ErrorMsgs['required'] = '!la_err_required!'; //'Field is required';
 		$this->ErrorMsgs['unique'] = '!la_err_unique!'; //'Field value must be unique';
 		$this->ErrorMsgs['value_out_of_range'] = '!la_err_value_out_of_range!'; //'Field is out of range, possible values from %s to %s';
 		$this->ErrorMsgs['length_out_of_range'] = '!la_err_length_out_of_range!'; //'Field is out of range';
 		$this->ErrorMsgs['bad_type'] = '!la_err_bad_type!'; //'Incorrect data format, please use %s';
 		$this->ErrorMsgs['invalid_format'] = '!la_err_invalid_format!'; //'Incorrect data format, please use %s';
 		$this->ErrorMsgs['bad_date_format'] = '!la_err_bad_date_format!'; //'Incorrect date format, please use (%s) ex. (%s)';
 		$this->ErrorMsgs['primary_lang_required'] = '!la_err_primary_lang_required!';
 	}
 
 	function SetDirtyField($field_name, $field_value)
 	{
 		$this->DirtyFieldValues[$field_name] = $field_value;
 	}
 
 	function GetDirtyField($field_name)
 	{
 		return $this->DirtyFieldValues[$field_name];
 	}
 
 	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;
 		}
 
 		$options = $this->GetFieldOptions($field_name);
 		$res = $value;
 		if (array_key_exists('formatter', $options)) {
 			$formatter =& $this->Application->recallObject($options['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
 	 */
 	function SetOriginalField($field_name, $field_value)
 	{
 		$this->OriginalFieldValues[$field_name] = $field_value;
 	}
 
 	/**
 	 * Set's default values for all fields
 	 *
 	 * @param bool $populate_ml_fields create all ml fields from db in config or not
 	 *
 	 * @access public
 	 */
 	function SetDefaultValues($populate_ml_fields = false)
 	{
 		parent::SetDefaultValues($populate_ml_fields);
 		if ($populate_ml_fields) {
 			$this->PopulateMultiLangFields();
 		}
 
 		foreach ($this->Fields as $field => $params) {
 			if ( isset($params['default']) ) {
 				$this->SetDBField($field, $params['default']);
 			}
 			else {
 				$this->SetDBField($field, NULL);
 			}
 		}
 	}
 
 	/**
 	* 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
 	*/
 	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');
 		$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
 	*/
 	function SetDBField($name,$value)
 	{
 		$this->FieldValues[$name] = $value;
 		/*if (isset($this->Fields[$name]['formatter'])) {
 			$formatter =& $this->Application->recallObject($this->Fields[$name]['formatter']);
 			$formatter->UpdateSubFields($name, $value, $this->Fields[$name], $this);
 		}*/
 	}
 
 	/**
 	 * Set's field error, if pseudo passed not found then create it with message text supplied.
 	 * Don't owerrite existing pseudo translation.
 	 *
 	 * @param string $field
 	 * @param string $pseudo
 	 * @param string $error_label
 	 *
 	 * @return bool
 	 */
 	function SetError($field, $pseudo, $error_label = null, $error_params = null)
 	{
 		$error_field = isset($this->Fields[$field]['error_field']) ? $this->Fields[$field]['error_field'] : $field;
 		if (isset($this->FieldErrors[$error_field]['pseudo'])) {
 			// don't set more then one error on field
 			return false;
 		}
 
 		$this->FieldErrors[$error_field]['pseudo'] = $pseudo;
 
 		if (isset($error_params)) {
 			if (array_key_exists('value', $error_params)) {
 				$this->FieldErrors[$error_field]['value'] = $error_params['value'];
 				unset($error_params['value']);
 			}
 
 			// additional params, that helps to determine error sources
 			$this->FieldErrors[$error_field]['params'] = $error_params;
 		}
 
 		if (isset($error_label) && !isset($this->ErrorMsgs[$pseudo])) {
 			// label for error (only when not already set)
 			$this->ErrorMsgs[$pseudo] = (substr($error_label, 0, 1) == '+') ? substr($error_label, 1) : '!'.$error_label.'!';
 		}
 
 		return true;
 	}
 
 	/**
 	* Return current item' field value by field name
 	* (doesn't apply formatter)
 	*
 	* @access public
 	* @param string $name field name to return
 	* @return mixed
 	*/
 	function GetDBField($name)
 	{
 		/*if (!array_key_exists($name, $this->FieldValues) && defined('DEBUG_MODE') && DEBUG_MODE) {
 			$this->Application->Debugger->appendTrace();
 		}*/
 
 		return $this->FieldValues[$name];
 	}
 
 	function HasField($name)
 	{
 		return isset($this->FieldValues[$name]);
 	}
 
 	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 acutally setting the fields
 	*
 	* @access public
 	* @param Array $hash
 	* @param Array $set_fields Optional param, field names in target object to set, other fields will be skipped
 	* @return void
 	*/
 	function SetFieldsFromHash($hash, $set_fields = null)
 	{
 		// used in formatter which work with multiple fields together
 		foreach($hash as $field_name => $field_value) {
 			if (is_numeric($field_name) || !array_key_exists($field_name, $this->Fields)) {
 				continue;
 			}
 
 			if (is_array($set_fields) && !in_array($field_name, $set_fields)) {
 				continue;
 			}
 
 			$this->SetDirtyField($field_name, $field_value);
 		}
 
 		// formats all fields using associated formatters
 		foreach ($hash as $field_name => $field_value)
 		{
 			if (is_numeric($field_name) || !array_key_exists($field_name, $this->Fields)) {
 				continue;
 			}
 
 			if (is_array($set_fields) && !in_array($field_name, $set_fields)) {
 				continue;
 			}
 
 			$this->SetField($field_name,$field_value);
 		}
 	}
 
 	function SetDBFieldsFromHash($hash, $set_fields = null)
 	{
 		foreach ($hash as $field_name => $field_value) {
 			if (is_numeric($field_name) || !array_key_exists($field_name, $this->Fields)) {
 				continue;
 			}
 
 			if (is_array($set_fields) && !in_array($field_name, $set_fields)) {
 				continue;
 			}
 
 			$this->SetDBField($field_name, $field_value);
 		}
 	}
 
 	/**
 	* Returns part of SQL WHERE clause identifing the record, ex. id = 25
 	*
 	* @access public
 	* @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
 	* @return void
 	* @see kDBItem::Load()
 	* @see kDBItem::Update()
 	* @see kDBItem::Delete()
 	*/
 	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) {
 			if (!preg_match('/\./', $field)) {
 				$ret .= '(`' . $this->TableName . '`.' . $field . ' = ' . $this->Conn->qstr($value) . ') AND ';
 			}
 			else {
 				$ret .= '(' . $field . ' = ' . $this->Conn->qstr($value) . ') 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
 	* @return bool True if item has been loaded, false otherwise
 	*/
 	function Load($id, $id_field_name = null)
 	{
 		if ( isset($id_field_name) ) {
 			$this->SetIDField($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->setIDField( $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;
 		$field_values = $this->Conn->GetRow($q);
 
 		if ($field_values) {
 			$this->FieldValues = array_merge_recursive2($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;
 	}
 
 	/**
 	* Builds select sql, SELECT ... FROM parts only
 	*
 	* @access public
 	* @return string
 	*/
 	function GetSelectSQL()
 	{
 		$sql = $this->addCalculatedFields($this->SelectClause);
 		return parent::GetSelectSQL($sql);
 	}
 
 	function UpdateFormattersMasterFields()
 	{
 		foreach ($this->Fields as $field => $options) {
 			if (isset($options['formatter'])) {
 				$formatter =& $this->Application->recallObject($options['formatter']);
 				$formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this);
 			}
 		}
 	}
 
 	/**
 	 * Allows to skip certain fields from getting into sql queries
 	 *
 	 * @param string $field_name
 	 * @param mixed $force_id
 	 * @return bool
 	 */
 	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 Primery Key Id to update
 	* @return bool
 	*/
 	function Update($id = 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 = '';
 
 	    foreach ($this->FieldValues as $field_name => $field_value) {
 	    	if ($this->skipField($field_name)) {
 	    		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
 	    			$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
 	    	return false;
 	    }
 
-		$affected = $this->Conn->getAffectedRows();
-		if (!$system_update && $affected == 1) {
+		$affected_rows = $this->Conn->getAffectedRows();
+
+		if (!$system_update && ($affected_rows > 0)) {
 			$this->setModifiedFlag(clUPDATE);
 		}
 
 		$this->saveCustomFields();
-	    $this->raiseEvent('OnAfterItemUpdate');
+
+		if ($affected_rows > 0) {
+	    	$this->raiseEvent('OnAfterItemUpdate');
+		}
+
 	    $this->Loaded = true;
 
 	    if ($this->mode != 't') {
 			$this->Application->resetCounters($this->TableName);
 		}
 
 		return true;
 	}
 
 	function ValidateField($field)
 	{
 		$options = $this->Fields[$field];
 
 		/*if (isset($options['formatter'])) {
 			$formatter =& $this->Application->recallObject($options['formatter']);
 			$formatter->UpdateMasterFields($field, $this->GetDBField($field), $options, $this);
 		}*/
 
 		$error_field = isset($options['error_field']) ? $options['error_field'] : $field;
 		$res = !isset($this->FieldErrors[$error_field]['pseudo']) || !$this->FieldErrors[$error_field]['pseudo'];
 
 		$res = $res && $this->ValidateRequired($field, $options);
 		$res = $res && $this->ValidateType($field, $options);
 		$res = $res && $this->ValidateRange($field, $options);
 		$res = $res && $this->ValidateUnique($field, $options);
 		$res = $res && $this->CustomValidation($field, $options);
 
 		return $res;
 	}
 
 	/**
 	 * Validate all item fields based on
 	 * constraints set in each field options
 	 * in config
 	 *
 	 * @return bool
 	 * @access private
 	 */
 	function Validate()
 	{
 		$this->UpdateFormattersMasterFields(); //order is critical - should be called BEFORE checking errors
 
 		if ($this->IgnoreValidation) {
 			return true;
 		}
 
 		// will apply any custom validation to the item
 		$this->raiseEvent('OnBeforeItemValidate');
 
 		$global_res = true;
 		foreach ($this->Fields as $field => $params) {
 			$res = $this->ValidateField($field);
 
 
 			$global_res = $global_res && $res;
 		}
 
 		if (!$global_res && $this->Application->isDebugMode()) {
 			$error_msg = '	Validation failed in prefix <strong>'.$this->Prefix.'</strong>,
 							FieldErrors follow (look at items with <strong>"pseudo"</strong> key set)<br />
 							You may ignore this notice if submitted data really has a validation error';
 			trigger_error(trim($error_msg), E_USER_NOTICE);
 			$this->Application->Debugger->dumpVars($this->FieldErrors);
 		}
 
 		if ($global_res) {
 			// no validation errors
 			$this->raiseEvent('OnAfterItemValidate');
 		}
 
 		return $global_res;
 	}
 
 	/**
 	 * Check field value by user-defined alghoritm
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 */
 	function CustomValidation($field, $params)
 	{
 		return true;
 	}
 
 	/**
 	 * Check if item has errors
 	 *
 	 * @param Array $skip_fields fields to skip during error checking
 	 * @return bool
 	 */
 	function HasErrors($skip_fields)
 	{
 		$global_res = false;
 
 		foreach ($this->Fields as $field => $field_params) {
 			// If Formatter has set some error messages during values parsing
 			if ( !( in_array($field, $skip_fields) ) &&
 				isset($this->FieldErrors[$field]['pseudo']) && $this->FieldErrors[$field] != '') {
 				$global_res = true;
 			}
 		}
 		return $global_res;
 	}
 
 	/**
 	 * Check if value in field matches field type specified in config
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 */
 	function ValidateType($field, $params)
 	{
 		$res = true;
 		$val = $this->FieldValues[$field];
 		if ( 	$val != '' &&
 					isset($params['type']) &&
 					preg_match("#int|integer|double|float|real|numeric|string#", $params['type'])
 				) {
 			if ($params['type'] == 'numeric') {
 				trigger_error('Invalid field type <strong>'.$params['type'].'</strong> (in ValidateType method), please use <strong>float</strong> instead', E_USER_NOTICE);
 				$params['type'] = 'float';
 			}
 			$res = is_numeric($val);
 			if ($params['type']=='string' || $res) {
 				$f = 'is_'.$params['type'];
 				settype($val, $params['type']);
 				$res = $f($val) && ($val == $this->FieldValues[$field]);
 			}
 			if (!$res) {
 				$this->SetError($field, 'bad_type', null, $params['type']);
 			}
 		}
 		return $res;
 	}
 
 	/**
 	 * Check if value is set for required field
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 * @access private
 	 */
 	function ValidateRequired($field, $params)
 	{
 		$res = true;
 		if (isset($params['required']) && $params['required']) {
 			$check_value = $this->FieldValues[$field];
 			if ($this->Application->ConfigValue('TrimRequiredFields')) {
 				$check_value = trim($check_value);
 			}
 			$res = ((string)$check_value != '');
 		}
 
 		if (!$res) {
 			$this->SetError($field, 'required');
 		}
 
 		return $res;
 	}
 
 	/**
 	 * Validates that current record has unique field combination among other table records
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 * @access private
 	 */
 	function ValidateUnique($field, $params)
 	{
 		$res = true;
 		$unique_fields = getArrayValue($params,'unique');
 		if($unique_fields !== false)
 		{
 			$where = Array();
 			array_push($unique_fields,$field);
 			foreach($unique_fields as $unique_field)
 			{
 				// if field is not empty or if it is required - we add where condition
 				if ((string)$this->GetDBField($unique_field) != '' || (isset($this->Fields[$unique_field]['required']) && $this->Fields[$unique_field]['required'])) {
 					$where[] = '`'.$unique_field.'` = '.$this->Conn->qstr( $this->GetDBField($unique_field) );
 				}
 				else {
 					// not good if we check by less fields than indicated
 					return true;
 				}
 			}
 			// This can ONLY happen if all unique fields are empty and not required.
 			// In such case we return true, because if unique field is not required there may be numerous empty values
 //			if (!$where) return true;
 
 			$sql = 'SELECT COUNT(*) FROM %s WHERE ('.implode(') AND (',$where).') AND ('.$this->IDField.' <> '.(int)$this->ID.')';
 
 			$res_temp = $this->Conn->GetOne( str_replace('%s', $this->TableName, $sql) );
 
 			$current_table_only = getArrayValue($params, 'current_table_only'); // check unique record only in current table
 			$res_live = $current_table_only ? 0 : $this->Conn->GetOne( str_replace('%s', $this->Application->GetLiveName($this->TableName), $sql) );
 
 			$res = ($res_temp == 0) && ($res_live == 0);
 
 			if (!$res) {
 				$this->SetError($field, 'unique');
 			}
 		}
 		return $res;
 	}
 
 	/**
 	 * Check if field value is in range specified in config
 	 *
 	 * @param string $field field name
 	 * @param Array $params field options from config
 	 * @return bool
 	 * @access private
 	 */
 	function ValidateRange($field, $params)
 	{
 		$res = true;
 		$val = $this->FieldValues[$field];
 
 		if ( isset($params['type']) && preg_match("#int|integer|double|float|real#", $params['type']) && strlen($val) > 0 ) {
 			if ( isset($params['max_value_inc'])) {
 				$res = $res && $val <= $params['max_value_inc'];
 				$max_val = $params['max_value_inc'].' (inclusive)';
 			}
 			if ( isset($params['min_value_inc'])) {
 				$res = $res && $val >= $params['min_value_inc'];
 				$min_val = $params['min_value_inc'].' (inclusive)';
 			}
 			if ( isset($params['max_value_exc'])) {
 				$res = $res && $val < $params['max_value_exc'];
 				$max_val = $params['max_value_exc'].' (exclusive)';
 			}
 			if ( isset($params['min_value_exc'])) {
 				$res = $res && $val > $params['min_value_exc'];
 				$min_val = $params['min_value_exc'].' (exclusive)';
 			}
 		}
 		if (!$res) {
 			if ( !isset($min_val) ) $min_val = '-&infin;';
 			if ( !isset($max_val) ) $max_val = '&infin;';
 
 			$this->SetError($field, 'value_out_of_range', null, Array ($min_val, $max_val));
 			return $res;
 		}
 		if ( isset($params['max_len'])) {
 			$res = $res && mb_strlen($val) <= $params['max_len'];
 		}
 		if ( isset($params['min_len'])) {
 			$res = $res && mb_strlen($val) >= $params['min_len'];
 		}
 		if (!$res) {
 			$error_params = Array (getArrayValue($params, 'min_len'), getArrayValue($params, 'max_len'));
 			$this->SetError($field, 'length_out_of_range', null, $error_params);
 			return $res;
 		}
 		return $res;
 	}
 
 	/**
 	 * Return error message for field
 	 *
 	 * @param string $field
 	 * @return string
 	 * @access public
 	 */
 	function GetErrorMsg($field, $force_escape = null)
 	{
 		if( !isset($this->FieldErrors[$field]) ) return '';
 
 		$err = getArrayValue($this->FieldErrors[$field], 'pseudo');
 		if (!$err) return '';
 		// if special error msg defined in config
 		if( isset($this->Fields[$field]['error_msgs'][$err]) )
 		{
 			$msg = $this->Fields[$field]['error_msgs'][$err];
 		}
 		else //fall back to defaults
 		{
 			if( !isset($this->ErrorMsgs[$err]) ) {
 				trigger_error('No user message is defined for pseudo error <b>'.$err.'</b><br>', E_USER_WARNING);
 				return $err; //return the pseudo itself
 			}
 			$msg = $this->ErrorMsgs[$err];
 		}
 		$msg = $this->Application->ReplaceLanguageTags($msg, $force_escape);
 
 		if ( isset($this->FieldErrors[$field]['params']) )
 		{
 			return vsprintf($msg, $this->FieldErrors[$field]['params']);
 		}
 		return $msg;
 	}
 
 	/**
 	* 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!
 	* @access public
 	* @return bool
 	*/
 	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)) {
 					// 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) {
 			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];
 		}
 		$this->setID($insert_id);
 
 		if (!$system_create){
 			$this->setModifiedFlag(clCREATE);
 		}
 
 		$this->saveCustomFields();
 		if ($this->mode != 't') {
 			$this->Application->resetCounters($this->TableName);
 		}
 		$this->raiseEvent('OnAfterItemCreate');
 		$this->Loaded = true;
 
 		return true;
 	}
 
 	/**
 	* Deletes the record from databse
 	*
 	* @access public
 	* @return bool
 	*/
 	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();
 
 		$this->setModifiedFlag(clDELETE); // will change affected rows, so get it before this line
 
 		if ($affected_rows > 0) {
 			// something was actually deleted
 			$this->raiseEvent('OnAfterItemDelete');
 		}
 
 		if ($this->mode != 't') {
 			$this->Application->resetCounters($this->TableName);
 		}
 
 		return $ret;
 	}
 
 	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') && array_key_exists('master_field', $options) && array_key_exists('error_field', $options)) {
 				// 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 will not be displayed
 				unset($this->Fields[$field]['error_field']);
 			}
 		}
 	}
 
 	/**
 	 * Sets new name for item in case if it is beeing 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 private
 	 */
 	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);
 	}
 
 	function raiseEvent($name, $id = null, $additional_params = Array())
 	{
 		if( !isset($id) ) $id = $this->GetID();
 		$event = new kEvent( Array('name'=>$name,'prefix'=>$this->Prefix,'special'=>$this->Special) );
 		$event->setEventParam('id', $id);
 
 		if ($additional_params) {
 			foreach ($additional_params as $ap_name => $ap_value) {
 				$event->setEventParam($ap_name, $ap_value);
 			}
 		}
 
 		$this->Application->HandleEvent($event);
 		return $event->status == erSUCCESS ? true : false;
 	}
 
 	/**
 	 * Set's new ID for item
 	 *
 	 * @param int $new_id
 	 * @access public
 	 */
 	function setID($new_id)
 	{
 		$this->ID = $new_id;
 		$this->SetDBField($this->IDField, $new_id);
 	}
 
 	/**
 	 * Generate and set new temporary id
 	 *
 	 * @access private
 	 */
 	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()) {
 			// 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()) {
 						$changes[$key]['ItemId'] = $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
 	 *
 	 * @access private
 	 * @author Alexey
 	 */
 	function setModifiedFlag($mode = null)
 	{
 		$main_prefix = $this->Application->GetTopmostPrefix($this->Prefix);
 		$this->Application->StoreVar($main_prefix.'_modified', '1', !$this->Application->isAdmin);
 
 		if ($this->ShouldLogChanges()) {
 			$this->LogChanges($main_prefix, $mode);
 			if (!$this->IsTempTable()) {
 				$handler =& $this->Application->recallObject($this->Prefix.'_EventHandler');
 				$ses_var_name = $main_prefix.'_changes_'.$this->Application->GetTopmostWid($this->Prefix);
 				$handler->SaveLoggedChanges($ses_var_name);
 			}
 		}
 	}
 
 	/**
 	 * Determines, that changes made to this item should be written to change log
 	 *
 	 * @return bool
 	 */
 	function ShouldLogChanges()
 	{
 		$log_changes = $this->Application->getUnitOption($this->Prefix, 'LogChanges') || $this->Application->ConfigValue('UseChangeLog');
 
 		return $log_changes && !$this->Application->getUnitOption($this->Prefix, 'ForceDontLogChanges');
 	}
 
 	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();
 
 		$general = array(
 			'Prefix' => $this->Prefix,
 			'ItemId' => $this->GetID(),
 			'OccuredOn' => adodb_mktime(),
 			'MasterPrefix' => $main_prefix,
 			'MasterId' => $this->Prefix == $main_prefix ? $this->GetID() : $this->Application->GetVar($main_prefix.'_id'), // is that correct (Kostja)??
 			'Action' => $mode,
 		);
 		switch ($mode) {
 			case clUPDATE:
 				$changes[] = array_merge($general, Array(
 					'Changes' => serialize(array_merge($this->GetTitleField(), $this->GetChangedFields())),
 				));
 				break;
 			case clCREATE:
 				$changes[] = array_merge($general, Array(
 					'Changes' => serialize($this->GetTitleField()),
 				));
 				break;
 			case clDELETE:
 				$changes[] = array_merge($general, Array(
 					'Changes' => serialize(array_merge($this->GetTitleField(), $this->GetRealFields())),
 				));
 		}
 
 		$this->Application->StoreVar($ses_var_name, serialize($changes));
 	}
 
 	function GetTitleField()
 	{
 		$title_field = $this->Application->getUnitOption($this->Prefix, 'TitleField');
 		if ($title_field && $this->GetField($title_field)) {
 			return Array($title_field => $this->GetField($title_field));
 		}
 	}
 
 	function GetRealFields()
 	{
 		if (function_exists('array_diff_key')) {
 			$db_fields = array_diff_key($this->FieldValues, $this->VirtualFields, $this->CalculatedFields);
 		}
 		else {
 			$db_fields = array();
 			foreach ($this->FieldValues as $key => $value) {
 				if (array_key_exists($key, $this->VirtualFields) || array_key_exists($key, $this->CalculatedFields)) continue;
 				$db_fields[$key] = $value;
 			}
 		}
 		return $db_fields;
 	}
 
 	function GetChangedFields()
 	{
 		$changes = array();
 
 		$diff = array_diff_assoc($this->GetRealFields(), $this->OriginalFieldValues);
 		foreach ($diff as $field => $new_value) {
 			$changes[$field] = array('old' => $this->GetOriginalField($field, true), 'new' => $this->GetField($field));
 		}
 		return $changes;
 	}
 
 	/**
 	 * Returns ID of currently processed record
 	 *
 	 * @return int
 	 * @access public
 	 */
 	function GetID()
 	{
 		return $this->ID;
 	}
 
 	/**
 	 * Generates ID for new items before inserting into database
 	 *
 	 * @return int
 	 * @access private
 	 */
 	function generateID()
 	{
 		return 0;
 	}
 
 	/**
 	 * Returns true if item was loaded successfully by Load method
 	 *
 	 * @return bool
 	 */
 	function isLoaded()
 	{
 		return $this->Loaded;
 	}
 
 	/**
 	 * Checks if field is required
 	 *
 	 * @param string $field
 	 * @return bool
 	 */
 	function isRequired($field)
 	{
 		return getArrayValue( $this->Fields[$field], 'required' );
 	}
 
 	/**
 	 * Sets new required flag to field
 	 *
 	 * @param string $field
 	 * @param bool $is_required
 	 */
 	function setRequired($field, $is_required = true)
 	{
 		$this->Fields[$field]['required'] = $is_required;
 	}
 
 	function Clear($new_id = null)
 	{
 		$this->Loaded = false;
 		$this->FieldValues = Array();
 		$this->OriginalFieldValues = Array ();
 		$this->SetDefaultValues(); // will wear off kDBItem::setID effect, so set it later
 		$this->FieldErrors = Array();
 
 		$this->setID($new_id);
 
 		return $this->Loaded;
 	}
 
 	function Query($force = false)
 	{
 		if ($this->Application->isDebugMode()) {
 			$this->Application->Debugger->appendTrace();
 		}
 
 		trigger_error('<b>Query</b> method is called in class <b>'.get_class($this).'</b> for prefix <b>'.$this->getPrefixSpecial().'</b>', E_USER_ERROR);
 	}
 
 	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, 'populate_ml_fields' => true));
 
 		$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 */
 
 		foreach ($this->customFields as $custom_id => $custom_name) {
 			$force_primary = isset($cdata->Fields['cust_'.$custom_id]['force_primary']) && $cdata->Fields['cust_'.$custom_id]['force_primary'];
 			$cdata->SetDBField($ml_formatter->LangFieldName('cust_'.$custom_id, $force_primary), $this->GetDBField('cust_'.$custom_name));
 		}
 
 		if ($cdata->isLoaded()) {
 			$ret = $cdata->Update();
 		}
 		else {
 			 $ret = $cdata->Create();
 			 if ($cdata->mode == 't') $cdata->setTempID();
 		}
 
 		return $ret;
 	}
 
 	/**
 	 * Returns specified field value from all selected rows.
 	 * Don't affect current record index
 	 *
 	 * @param string $field
 	 * @return Array
 	 */
 	function GetCol($field)
 	{
 		return Array (0 => $this->GetDBField($field));
 	}
 
 }
\ No newline at end of file