<?php
/**
* @version	$Id: kbase.php 16513 2017-01-20 14:10:53Z 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!');

/**
* Base
*
*/
class kBase {

	/**
	* Reference to global kApplication instance
	*
	* @var kApplication
	* @access protected
	*/
	protected $Application = null;

	/**
	* Connection to database
	*
	* @var IDBConnection
	* @access protected
	*/
	protected $Conn = null;

	/**
	 * Prefix, used during object creation
	 *
	 * @var string
	 * @access public
	 */
	public $Prefix = '';

	/**
	 * Special, used during object creation
	 *
	 * @var string
	 * @access public
	 */
	public $Special = '';

	/**
	 * Joined prefix and special, usually taken directly from
	 * tag beeing processed, to use in kApplication::recallObject method
	 *
	 * @var string
	 * @access protected
	 * @see kApplication::recallObject
	 */
	protected $prefixSpecial = '';

	/**
	 * Set's references to kApplication and DBConnection interface class instances
	 *
	 * @access public
	 * @see kApplication
	 * @see IDBConnection
	 */
	public function __construct()
	{
		$this->Application =& kApplication::Instance();
		$this->Conn =& $this->Application->GetADODBConnection();
	}

	/**
	 * Set's prefix and special
	 *
	 * @param string $prefix
	 * @param string $special
	 * @access public
	 */
	public function Init($prefix, $special)
	{
		$prefix = explode('_', $prefix, 2);
		$this->Prefix = $prefix[0];
		$this->Special = $special;

		$this->prefixSpecial = rtrim($this->Prefix . '.' . $this->Special, '.');
	}

	/**
	 * Returns prefix and special (when present) joined by a "."
	 *
	 * @return string
	 * @access public
	 */
	public function getPrefixSpecial()
	{
		return $this->prefixSpecial;
	}

	/**
	 * Creates string representation of a class (for logging)
	 *
	 * @return string
	 * @access public
	 */
	public function __toString()
	{
		$ret = 'ClassName: ' . get_class($this);

		try {
			$ret .= '; PrefixSpecial: ' . $this->getPrefixSpecial();
		} catch (Exception $e) {}

		return $ret;
	}
}


class kHelper extends kBase {

	/**
	 * Performs helper initialization
	 *
	 * @access public
	 */
	public function InitHelper()
	{

	}

	/**
	 * Append prefix and special to tag
	 * params (get them from tagname) like
	 * they were really passed as params
	 *
	 * @param string $prefix_special
	 * @param Array $tag_params
	 * @return Array
	 * @access protected
	 */
	protected function prepareTagParams($prefix_special, $tag_params = Array())
	{
		$parts = explode('.', $prefix_special);

		$ret = $tag_params;
		$ret['Prefix'] = $parts[0];
		$ret['Special'] = count($parts) > 1 ? $parts[1] : '';
		$ret['PrefixSpecial'] = $prefix_special;

		return $ret;
	}
}


abstract class kDBBase extends kBase {

	/**
	* Name of primary key field for the unit
	*
	* @var string
	* @access public
	* @see kDBBase::TableName
	*/
	public $IDField = '';

	/**
	* Unit's database table name
	*
	* @var string
	* @access public
	*/
	public $TableName = '';

	/**
	 * Form name, used for validation
	 *
	 * @var string
	 */
	protected $formName = '';

	/**
	 * Final form configuration
	 *
	 * @var Array
	 */
	protected $formConfig = Array ();

	/**
	* SELECT, FROM, JOIN parts of SELECT query (no filters, no limit, no ordering)
	*
	* @var string
	* @access protected
	*/
	protected $SelectClause = '';

	/**
	* Unit fields definitions (fields from database plus virtual fields)
	*
	* @var Array
	* @access protected
	*/
	protected $Fields = Array ();

	/**
	 * Fields, that have current time as their default value.
	 *
	 * @var array
	 */
	protected $currentTimeFields = array();

	/**
	 * Mapping between unit custom field IDs and their names
	 *
	 * @var Array
	 * @access protected
	 */
	protected $customFields = Array ();

	/**
	 * Unit virtual field definitions
	 *
	 * @var Array
	 * @access protected
	 * @see kDBBase::getVirtualFields()
	 * @see kDBBase::setVirtualFields()
	 */
	protected $VirtualFields = Array ();

	/**
	 * Fields that need to be queried using custom expression, e.g. IF(...) AS value
	 *
	 * @var Array
	 * @access protected
	 */
	protected $CalculatedFields = Array ();


	/**
	 * Fields that contain aggregated functions, e.g. COUNT, SUM, etc.
	 *
	 * @var Array
	 * @access protected
	 */
	protected $AggregatedCalculatedFields = Array ();

	/**
	 * Tells, that multilingual fields sould not be populated by default.
	 * Can be overriden from kDBBase::Configure method
	 *
	 * @var bool
	 * @access protected
	 */
	protected $populateMultiLangFields = false;

	/**
	 * Event, that was used to create this object
	 *
	 * @var kEvent
	 * @access protected
	 */
	protected $parentEvent = null;

	/**
	 * Sets new parent event to the object
	 *
	 * @param kEvent $event
	 * @return void
	 * @access public
	 */
	public function setParentEvent($event)
	{
		$this->parentEvent = $event;
	}

	/**
	 * Set object' TableName to LIVE table, defined in unit config
	 *
	 * @access public
	 */
	public function SwitchToLive()
	{
		$this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName');
	}

	/**
	 * Set object' TableName to TEMP table created based on table, defined in unit config
	 *
	 * @access public
	 */
	public function SwitchToTemp()
	{
		$table_name = $this->Application->getUnitOption($this->Prefix, 'TableName');
		$this->TableName = $this->Application->GetTempName($table_name, 'prefix:' . $this->Prefix);
	}

	/**
	 * Checks if object uses temp table
	 *
	 * @return bool
	 * @access public
	 */
	public function IsTempTable()
	{
		return $this->Application->IsTempTable($this->TableName);
	}

	/**
	* Sets SELECT part of list' query
	*
	* @param string $sql SELECT and FROM [JOIN] part of the query up to WHERE
	* @access public
	*/
	public function SetSelectSQL($sql)
	{
		$this->SelectClause = $sql;
	}

	/**
	 * Returns object select clause without any transformations
	 *
	 * @return string
	 * @access public
	 */
	public function GetPlainSelectSQL()
	{
		return $this->SelectClause;
	}

	/**
	 * Returns SELECT part of list' query.
	 * 1. Occurrences of "%1$s" and "%s" are replaced to kDBBase::TableName
	 * 2. Occurrences of "%3$s" are replaced to temp table prefix (only for table, using TABLE_PREFIX)
	 *
	 * @param string $base_query given base query will override unit default select query
	 * @param bool $replace_table replace all possible occurrences
	 * @return string
	 * @access public
	 * @see kDBBase::replaceModePrefix
	 */
	public function GetSelectSQL($base_query = null, $replace_table = true)
	{
		if (!isset($base_query)) {
			$base_query = $this->SelectClause;
		}

		if (!$replace_table) {
			return $base_query;
		}

		$query = str_replace(Array('%1$s', '%s'), $this->TableName, $base_query);

		return $this->replaceModePrefix($query);
	}

	/**
	 * Allows sub-stables to be in same mode as main item (e.g. LEFT JOINED ones)
	 *
	 * @param string $query
	 * @return string
	 * @access protected
	 */
	protected function replaceModePrefix($query)
	{
		$live_table = substr($this->Application->GetLiveName($this->TableName), strlen(TABLE_PREFIX));

		if (preg_match('/'.preg_quote(TABLE_PREFIX, '/').'(.*)'.preg_quote($live_table, '/').'/', $this->TableName, $rets)) {
			// will only happen, when table has a prefix (like in K4)
			return str_replace('%3$s', $rets[1], $query);
		}

		// will happen, when K3 table without prefix is used
		return $query;
	}

	/**
	 * Sets calculated fields
	 *
	 * @param Array $fields
	 * @access public
	 */
	public function setCalculatedFields($fields)
	{
		$this->CalculatedFields = $fields;
	}

	/**
	 * Adds calculated field declaration to object.
	 *
	 * @param string $name
	 * @param string $sql_clause
	 * @access public
	 */
	public function addCalculatedField($name, $sql_clause)
	{
		$this->CalculatedFields[$name] = $sql_clause;
	}

	/**
	 * Returns required mixing of aggregated & non-aggregated calculated fields
	 *
	 * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only
	 * @return Array
	 * @access public
	 */
	public function getCalculatedFields($aggregated = 1)
	{
		switch ($aggregated) {
			case 0:
				$fields = array_merge($this->CalculatedFields, $this->AggregatedCalculatedFields);
				break;

			case 1:
				$fields = $this->CalculatedFields;
				break;

			case 2:
				$fields = $this->AggregatedCalculatedFields; // TODO: never used
				break;

			default:
				$fields = Array();
				break;
		}

		return $fields;
	}

	/**
	 * Checks, that given field is a calculated field
	 *
	 * @param string $field
	 * @return bool
	 * @access public
	 */
	public function isCalculatedField($field)
	{
		return array_key_exists($field, $this->CalculatedFields);
	}

	/**
	 * Insert calculated fields sql into query in place of %2$s,
	 * return processed query.
	 *
	 * @param string $query
	 * @param int $aggregated 0 - having + aggregated, 1 - having only, 2 - aggregated only
	 * @return string
	 * @access protected
	 */
	protected function addCalculatedFields($query, $aggregated = 1)
	{
		$fields = $this->getCalculatedFields($aggregated);
		if ($fields) {
			$sql = Array ();
			$fields = str_replace('%2$s', $this->Application->GetVar('m_lang'), $fields);

			foreach ($fields as $field_name => $field_expression) {
				$sql[] = '('.$field_expression.') AS `'.$field_name.'`';
			}
			$sql = implode(',',$sql);

			return $this->Application->ReplaceLanguageTags( str_replace('%2$s', ','.$sql, $query) );
		}

		return str_replace('%2$s', '', $query);
	}

	/**
	 * Performs initial object configuration, which includes setting the following:
	 * - primary key and table name
	 * - field definitions (including field modifiers, formatters, default values)
	 *
	 * @param bool $populate_ml_fields create all ml fields from db in config or not
	 * @param string $form_name form name for validation
	 * @access public
	 */
	public function Configure($populate_ml_fields = null, $form_name = null)
	{
		if ( isset($populate_ml_fields) ) {
			$this->populateMultiLangFields = $populate_ml_fields;
		}

		$this->IDField = $this->Application->getUnitOption($this->Prefix, 'IDField');
		$this->TableName = $this->Application->getUnitOption($this->Prefix, 'TableName');

		$this->initForm($form_name);
		$this->defineFields();

		$this->ApplyFieldModifiers(null, true); // should be called only after all fields definitions been set
		$this->prepareConfigOptions(); // this should go last, but before setDefaultValues, order is significant!

		// only set on first call of method
		if ( isset($populate_ml_fields) ) {
			$this->SetDefaultValues();
		}
	}

	/**
	 * Adjusts object according to given form name
	 *
	 * @param string $form_name
	 * @return void
	 * @access protected
	 */
	protected function initForm($form_name = null)
	{
		$forms = $this->Application->getUnitOption($this->Prefix, 'Forms', Array ());

		$this->formName = $form_name;
		$this->formConfig = isset($forms['default']) ? $forms['default'] : Array ();

		if ( !$this->formName ) {
			return ;
		}

		if ( !array_key_exists($this->formName, $forms) ) {
			trigger_error('Form "<strong>' . $this->formName . '</strong>" isn\'t declared in "<strong>' . $this->Prefix . '</strong>" unit config.', E_USER_NOTICE);
		}
		else {
			$this->formConfig = kUtil::array_merge_recursive($this->formConfig, $forms[$this->formName]);
		}
	}

	/**
	 * Add field definitions from all possible sources
	 * Used field sources: database fields, custom fields, virtual fields, calculated fields, aggregated calculated fields
	 *
	 * @access protected
	 */
	protected function defineFields()
	{
		$this->Fields = $this->getFormOption('Fields', Array ());
		$this->customFields = $this->getFormOption('CustomFields', Array());

		$this->setVirtualFields( $this->getFormOption('VirtualFields', Array ()) );

		$calculated_fields = $this->getFormOption('CalculatedFields', Array());
		$this->CalculatedFields = $this->getFieldsBySpecial($calculated_fields);

		$aggregated_calculated_fields = $this->getFormOption('AggregatedCalculatedFields', Array());
		$this->AggregatedCalculatedFields = $this->getFieldsBySpecial($aggregated_calculated_fields);
	}

	/**
	 * Returns form name, used for validation
	 *
	 * @return string
	 */
	public function getFormName()
	{
		return $this->formName;
	}

	/**
	 * Reads unit (specified by $prefix) option specified by $option and applies form change to it
	 *
	 * @param string $option
	 * @param mixed $default
	 * @return string
	 * @access public
	 */
	public function getFormOption($option, $default = false)
	{
		$ret = $this->Application->getUnitOption($this->Prefix, $option, $default);

		if ( isset($this->formConfig[$option]) ) {
			$ret = kUtil::array_merge_recursive($ret, $this->formConfig[$option]);
		}

		return $ret;
	}

	/**
	 * Only exteracts fields, that match current object Special
	 *
	 * @param Array $fields
	 * @return Array
	 * @access protected
	 */
	protected function getFieldsBySpecial($fields)
	{
		if ( array_key_exists($this->Special, $fields) ) {
			return $fields[$this->Special];
		}

		return array_key_exists('', $fields) ? $fields[''] : Array();
	}

	/**
	 * Sets aggeregated calculated fields
	 *
	 * @param Array $fields
	 * @access public
	 */
	public function setAggregatedCalculatedFields($fields)
	{
		$this->AggregatedCalculatedFields = $fields;
	}

	/**
	 * Set's field names from table from config
	 *
	 * @param Array $fields
	 * @access public
	 */
	public function setCustomFields($fields)
	{
		$this->customFields = $fields;
	}

	/**
	 * Returns custom fields information from table from config
	 *
	 * @return Array
	 * @access public
	 */
	public function getCustomFields()
	{
		return $this->customFields;
	}

	/**
	 * Set's fields information from table from config
	 *
	 * @param Array $fields
	 * @access public
	 */
	public function setFields($fields)
	{
		$this->Fields = $fields;
	}

	/**
	 * Returns fields information from table from config
	 *
	 * @return Array
	 * @access public
	 */
	public function getFields()
	{
		return $this->Fields;
	}

	/**
	 * Checks, that given field exists
	 *
	 * @param string $field
	 * @return bool
	 * @access public
	 */
	public function isField($field)
	{
		return array_key_exists($field, $this->Fields);
	}

	/**
	 * Override field options with ones defined in submit via "field_modfiers" array (common for all prefixes)
	 *
	 * @param Array $field_modifiers
	 * @param bool $from_submit
	 * @return void
	 * @access public
	 * @author Alex
	 */
	public function ApplyFieldModifiers($field_modifiers = null, $from_submit = false)
	{
		$allowed_modifiers = Array ('required', 'multiple');

		if ( $this->Application->isAdminUser ) {
			// can change upload dir on the fly (admin only!)
			$allowed_modifiers[] = 'upload_dir';
		}

		if ( !isset($field_modifiers) ) {
			$field_modifiers = $this->Application->GetVar('field_modifiers');

			if ( !$field_modifiers ) {
				// no field modifiers
				return;
			}

			$field_modifiers = getArrayValue($field_modifiers, $this->getPrefixSpecial());
		}

		if ( !$field_modifiers ) {
			// no field modifiers for current prefix_special
			return;
		}

		$fields = $this->Application->getUnitOption($this->Prefix, 'Fields', Array ());
		$virtual_fields = $this->Application->getUnitOption($this->Prefix, 'VirtualFields', Array ());

		foreach ($field_modifiers as $field => $field_options) {
			foreach ($field_options as $option_name => $option_value) {
				if ( !in_array(strtolower($option_name), $allowed_modifiers) ) {
					continue;
				}

				if ( $from_submit ) {
					// there are no "lN_FieldName" fields, since ApplyFieldModifiers is
					// called before PrepareOptions method, which creates them
					$field = preg_replace('/^l[\d]+_(.*)/', '\\1', $field);
				}

				if ( $this->isVirtualField($field) ) {
					$virtual_fields[$field][$option_name] = $option_value;
					$this->SetFieldOption($field, $option_name, $option_value, true);
				}

				$fields[$field][$option_name] = $option_value;
				$this->SetFieldOption($field, $option_name, $option_value);
			}
		}

		$this->Application->setUnitOption($this->Prefix, 'Fields', $fields);
		$this->Application->setUnitOption($this->Prefix, 'VirtualFields', $virtual_fields);
	}

	/**
	 * Set fields (+options) for fields that physically doesn't exist in database
	 *
	 * @param Array $fields
	 * @access public
	 */
	public function setVirtualFields($fields)
	{
		if ($fields) {
			$this->VirtualFields = $fields;
			$this->Fields = array_merge($this->VirtualFields, $this->Fields);
		}
	}

	/**
	 * Returns virtual fields
	 *
	 * @return Array
	 * @access public
	 */
	public function getVirtualFields()
	{
		return $this->VirtualFields;
	}

	/**
	 * Checks, that given field is a virtual field
	 *
	 * @param string $field
	 * @return bool
	 * @access public
	 */
	public function isVirtualField($field)
	{
		return array_key_exists($field, $this->VirtualFields);
	}

	/**
	 * Performs additional initialization for field default values
	 *
	 * @access protected
	 */
	protected function SetDefaultValues()
	{
		foreach ( $this->Fields as $field => $options ) {
			if ( array_key_exists('default', $options) ) {
				if ( $options['default'] === '#NOW#' ) {
					$this->currentTimeFields[] = $field;
				}

				if ( in_array($field, $this->currentTimeFields) ) {
					$this->Fields[$field]['default'] = adodb_mktime();
				}
			}
		}
	}

	/**
	 * Overwrites field definition in unit config
	 *
	 * @param string $field
	 * @param Array $options
	 * @param bool $is_virtual
	 * @access public
	 */
	public function SetFieldOptions($field, $options, $is_virtual = false)
	{
		if ($is_virtual) {
			$this->VirtualFields[$field] = $options;
			$this->Fields = array_merge($this->VirtualFields, $this->Fields);
		}
		else {
			$this->Fields[$field] = $options;
		}
	}

	/**
	 * Changes/sets given option's value in given field definiton
	 *
	 * @param string $field
	 * @param string $option_name
	 * @param mixed $option_value
	 * @param bool $is_virtual
	 * @access public
	 */
	public function SetFieldOption($field, $option_name, $option_value, $is_virtual = false)
	{
		if ($is_virtual) {
			$this->VirtualFields[$field][$option_name] = $option_value;
		}

		$this->Fields[$field][$option_name] = $option_value;
	}

	/**
	 * Returns field definition from unit config.
	 * Also executes sql from "options_sql" field option to form "options" field option
	 *
	 * @param string $field
	 * @param bool $is_virtual
	 * @return Array
	 * @access public
	 */
	public function GetFieldOptions($field, $is_virtual = false)
	{
		$property_name = $is_virtual ? 'VirtualFields' : 'Fields';

		if ( !array_key_exists($field, $this->$property_name) ) {
			return Array ();
		}

		if (!$is_virtual) {
			if (!array_key_exists('options_prepared', $this->Fields[$field]) || !$this->Fields[$field]['options_prepared']) {
				// executes "options_sql" from field definition, only when field options are accessed (late binding)
				$this->PrepareFieldOptions($field);
				$this->Fields[$field]['options_prepared'] = true;
			}
		}

		return $this->{$property_name}[$field];
	}

	/**
	 * Returns field option
	 *
	 * @param string $field
	 * @param string $option_name
	 * @param bool $is_virtual
	 * @param mixed $default
	 * @return mixed
	 * @access public
	 */
	public function GetFieldOption($field, $option_name, $is_virtual = false, $default = false)
	{
		$field_options = $this->GetFieldOptions($field, $is_virtual);

		if ( !$field_options && strpos($field, '#') === false ) {
			// we use "#FIELD_NAME#" as field for InputName tag in JavaScript, so ignore it
			$form_name = $this->getFormName();
			trigger_error('Field "<strong>' . $field . '</strong>" is not defined' . ($form_name ? ' on "<strong>' . $this->getFormName() . '</strong>" form' : '') . ' in "<strong>' . $this->Prefix . '</strong>" unit config', E_USER_WARNING);

			return false;
		}

		return array_key_exists($option_name, $field_options) ? $field_options[$option_name] : $default;
	}

	/**
	 * Returns formatted field value
	 *
	 * @param string $name
	 * @param string $format
	 *
	 * @return string
	 * @access protected
	 */
	public function GetField($name, $format = null)
	{
		$formatter_class = $this->GetFieldOption($name, 'formatter');

		if ( $formatter_class ) {
			$value = ($formatter_class == 'kMultiLanguage') && !preg_match('/^l[0-9]+_/', $name) ? '' : $this->GetDBField($name);

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

			return $formatter->Format($value, $name, $this, $format);
		}

		return $this->GetDBField($name);
	}

	/**
	 * Returns unformatted field value
	 *
	 * @param string $field
	 * @return string
	 * @access public
	 */
	abstract public function GetDBField($field);

	/**
	 * Checks of object has given field
	 *
	 * @param string $name
	 * @return bool
	 * @access public
	 */
	abstract public function HasField($name);

	/**
	 * Returns field values
	 *
	 * @return Array
	 * @access public
	 */
	abstract public function GetFieldValues();

	/**
	 * Populates values of sub-fields, based on formatters, set to mater fields
	 *
	 * @param Array $fields
	 * @access public
	 * @todo Maybe should not be publicly accessible
	 */
	public function UpdateFormattersSubFields($fields = null)
	{
		if ( !is_array($fields) ) {
			$fields = array_keys($this->Fields);
		}

		foreach ($fields as $field) {
			if ( isset($this->Fields[$field]['formatter']) ) {
				/** @var kFormatter $formatter */
				$formatter = $this->Application->recallObject($this->Fields[$field]['formatter']);

				$formatter->UpdateSubFields($field, $this->GetDBField($field), $this->Fields[$field], $this);
			}
		}
	}

	/**
	 * Use formatters, specified in field declarations to perform additional field initialization in unit config
	 *
	 * @access protected
	 */
	protected function prepareConfigOptions()
	{
		$field_names = array_keys($this->Fields);

		foreach ($field_names as $field_name) {
			if ( !array_key_exists('formatter', $this->Fields[$field_name]) ) {
				continue;
			}

			/** @var kFormatter $formatter */
			$formatter = $this->Application->recallObject( $this->Fields[$field_name]['formatter'] );

			$formatter->PrepareOptions($field_name, $this->Fields[$field_name], $this);
		}
	}

	/**
	 * Escapes fields only, not expressions
	 *
	 * @param string $field_expr
	 * @return string
	 * @access protected
	 */
	protected function escapeField($field_expr)
	{
		return preg_match('/[.(]/', $field_expr) ? $field_expr : '`' . $field_expr . '`';
	}

	/**
	 * Replaces current language id in given field options
	 *
	 * @param string $field_name
	 * @param Array $field_option_names
	 * @access protected
	 */
	protected function _replaceLanguageId($field_name, $field_option_names)
	{
		// don't use GetVar('m_lang') since it's always equals to default language on editing form in admin
		$current_language_id = $this->Application->Phrases->LanguageId;
		$primary_language_id = $this->Application->GetDefaultLanguageId();

		$field_options =& $this->Fields[$field_name];
		foreach ($field_option_names as $option_name) {
			$field_options[$option_name] = str_replace('%2$s', $current_language_id, $field_options[$option_name]);
			$field_options[$option_name] = str_replace('%3$s', $primary_language_id, $field_options[$option_name]);
		}
	}

	/**
	 * Transforms "options_sql" field option into valid "options" array for given field
	 *
	 * @param string $field_name
	 * @access protected
	 */
	protected function PrepareFieldOptions($field_name)
	{
		$field_options =& $this->Fields[$field_name];
		if (array_key_exists('options_sql', $field_options) ) {
			// get options based on given sql
			$replace_options = Array ('option_title_field', 'option_key_field', 'options_sql');
			$this->_replaceLanguageId($field_name, $replace_options);

			$select_clause = $this->escapeField($field_options['option_title_field']) . ',' . $this->escapeField($field_options['option_key_field']);
			$sql = sprintf($field_options['options_sql'], $select_clause);

			if (array_key_exists('serial_name', $field_options)) {
				// try to cache option sql on serial basis
				$cache_key = 'sql_' . crc32($sql) . '[%' . $field_options['serial_name'] . '%]';
				$dynamic_options = $this->Application->getCache($cache_key);

				if ($dynamic_options === false) {
					$this->Conn->nextQueryCachable = true;
					$dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field']));
					$this->Application->setCache($cache_key, $dynamic_options);
				}
			}
			else {
				// don't cache options sql
				$dynamic_options = $this->Conn->GetCol($sql, preg_replace('/^.*?\./', '', $field_options['option_key_field']));
			}

			$options_hash = array_key_exists('options', $field_options) ? $field_options['options'] : Array ();
			$field_options['options'] = kUtil::array_merge_recursive($options_hash, $dynamic_options); // because of numeric keys
		}
	}

	/**
	 * Returns ID of currently processed record
	 *
	 * @return int
	 * @access public
	 */
	public function GetID()
	{
		return $this->GetDBField($this->IDField);
	}

	/**
	 * Allows kDBTagProcessor.SectionTitle to detect if it's editing or new item creation
	 *
	 * @return bool
	 * @access public
	 */
	public function IsNewItem()
	{
		return $this->GetID() ? false : true;
	}

	/**
	 * Returns parent table information
	 *
	 * @param string $special special of main item
	 * @param bool $guess_special if object retrieved with specified special is not loaded, then try not to use special
	 * @return Array
	 * @access public
	 */
	public function getLinkedInfo($special = '', $guess_special = false)
	{
		$parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix');
		if ($parent_prefix) {
			// if this is linked table, then set id from main table
			$table_info = Array (
				'TableName' => $this->Application->getUnitOption($this->Prefix,'TableName'),
				'IdField' => $this->Application->getUnitOption($this->Prefix,'IDField'),
				'ForeignKey' => $this->Application->getUnitOption($this->Prefix,'ForeignKey'),
				'ParentTableKey' => $this->Application->getUnitOption($this->Prefix,'ParentTableKey'),
				'ParentPrefix' => $parent_prefix
			);

			if (is_array($table_info['ForeignKey'])) {
					$table_info['ForeignKey'] = getArrayValue($table_info, 'ForeignKey', $parent_prefix);
			}

			if (is_array($table_info['ParentTableKey'])) {
				$table_info['ParentTableKey'] = getArrayValue($table_info, 'ParentTableKey', $parent_prefix);
			}

			/** @var kDBItem $main_object */
			$main_object = $this->Application->recallObject($parent_prefix.'.'.$special, null, Array ('raise_warnings' => 0));

			if (!$main_object->isLoaded() && $guess_special) {
				$main_object = $this->Application->recallObject($parent_prefix);
			}

			return array_merge($table_info, Array('ParentId'=> $main_object->GetDBField( $table_info['ParentTableKey'] ) ) );
		}

		return false;
	}

	/**
	 * Returns true, when list/item was queried/loaded
	 *
	 * @return bool
	 * @access public
	 */
	abstract public function isLoaded();

	/**
	 * Returns specified field value from all selected rows.
	 * Don't affect current record index
	 *
	 * @param string $field
	 * @return Array
	 * @access public
	 */
	abstract public function GetCol($field);
}


/**
 * Base class for exceptions, that trigger redirect action once thrown
 */
class kRedirectException extends Exception {

	/**
	 * Redirect template
	 *
	 * @var string
	 * @access protected
	 */
	protected $template = '';

	/**
	 * Redirect params
	 *
	 * @var Array
	 * @access protected
	 */
	protected $params = Array ();

	/**
	 * Creates redirect exception
	 *
	 * @param string $message
	 * @param int $code
	 * @param Exception $previous
	 */
	public function __construct($message = '', $code = 0, $previous = NULL)
	{
		parent::__construct($message, $code, $previous);
	}

	/**
	 * Initializes exception
	 *
	 * @param string $template
	 * @param Array $params
	 * @return void
	 * @access public
	 */
	public function setup($template, $params = Array ())
	{
		$this->template = $template;
		$this->params = $params;
	}

	/**
	 * Display exception details in debugger (only useful, when DBG_REDIRECT is enabled) and performs redirect
	 *
	 * @return void
	 * @access public
	 */
	public function run()
	{
		$application =& kApplication::Instance();

		if ( $application->isDebugMode() ) {
			$application->Debugger->appendException($this);
		}

		$application->Redirect($this->template, $this->params);
	}
}


/**
 * Exception, that is thrown when user don't have permission to perform requested action
 */
class kNoPermissionException extends kRedirectException {

}
