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

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

	define('EH_CUSTOM_PROCESSING_BEFORE',1);
	define('EH_CUSTOM_PROCESSING_AFTER',2);

	/**
	 * Note:
	 *   1. When addressing variables from submit containing
	 *	 	Prefix_Special as part of their name use
	 *	 	$event->getPrefixSpecial(true) instead of
	 *	 	$event->getPrefixSpecial() as usual. This is due PHP
	 *	 	is converting "." symbols in variable names during
	 *	 	submit info "_". $event->getPrefixSpecial optional
	 *	 	1st parameter returns correct current Prefix_Special
	 *	 	for variables being submitted such way (e.g. variable
	 *	 	name that will be converted by PHP: "users.read_only_id"
	 *	 	will be submitted as "users_read_only_id".
	 *
	 *	 2.	When using $this->Application-LinkVar on variables submitted
	 *		from form which contain $Prefix_Special then note 1st item. Example:
	 *		LinkVar($event->getPrefixSpecial(true).'_varname',$event->getPrefixSpecial().'_varname')
	 *
	 */


	/**
	 * EventHandler that is used to process
	 * any database related events
	 *
	 */
	class kDBEventHandler extends kEventHandler {

		/**
		 * Checks permissions of user
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function CheckPermission(kEvent &$event)
		{
			$section = $event->getSection();

			if ( !$this->Application->isAdmin ) {
				$allow_events = Array ('OnSearch', 'OnSearchReset', 'OnNew');
				if ( in_array($event->Name, $allow_events) ) {
					// allow search on front
					return true;
				}
			}
			elseif ( ($event->Name == 'OnPreSaveAndChangeLanguage') && !$this->UseTempTables($event) ) {
				// allow changing language in grids, when not in editing mode
				return $this->Application->CheckPermission($section . '.view', 1);
			}

			if ( !preg_match('/^CATEGORY:(.*)/', $section) ) {
				// only if not category item events
				if ( (substr($event->Name, 0, 9) == 'OnPreSave') || ($event->Name == 'OnSave') ) {
					if ( $this->isNewItemCreate($event) ) {
						return $this->Application->CheckPermission($section . '.add', 1);
					}
					else {
						return $this->Application->CheckPermission($section . '.add', 1) || $this->Application->CheckPermission($section . '.edit', 1);
					}
				}
			}

			if ( $event->Name == 'OnPreCreate' ) {
				// save category_id before item create (for item category selector not to destroy permission checking category)
				$this->Application->LinkVar('m_cat_id');
			}

			if ( $event->Name == 'OnSaveWidths' ) {
				return $this->Application->isAdminUser;
			}

			return parent::CheckPermission($event);
		}

		/**
		 * Allows to override standard permission mapping
		 *
		 * @return void
		 * @access protected
		 * @see kEventHandler::$permMapping
		 */
		protected function mapPermissions()
		{
			parent::mapPermissions();

			$permissions = Array (
				'OnLoad' => Array ('self' => 'view', 'subitem' => 'view'),
				'OnItemBuild' => Array ('self' => 'view', 'subitem' => 'view'),
				'OnSuggestValues' => Array ('self' => 'view', 'subitem' => 'view'),

				'OnBuild' => Array ('self' => true),

				'OnNew' => Array ('self' => 'add', 'subitem' => 'add|edit'),
				'OnCreate' => Array ('self' => 'add', 'subitem' => 'add|edit'),
				'OnUpdate' => Array ('self' => 'edit', 'subitem' => 'add|edit'),
				'OnSetPrimary' => Array ('self' => 'add|edit', 'subitem' => 'add|edit'),
				'OnDelete' => Array ('self' => 'delete', 'subitem' => 'add|edit'),
				'OnDeleteAll' => Array ('self' => 'delete', 'subitem' => 'add|edit'),
				'OnMassDelete' => Array ('self' => 'delete', 'subitem' => 'add|edit'),
				'OnMassClone' => Array ('self' => 'add', 'subitem' => 'add|edit'),

				'OnCut' => Array ('self'=>'edit', 'subitem' => 'edit'),
				'OnCopy' => Array ('self'=>'edit', 'subitem' => 'edit'),
				'OnPaste' => Array ('self'=>'edit', 'subitem' => 'edit'),

				'OnSelectItems' => Array ('self' => 'add|edit', 'subitem' => 'add|edit'),
				'OnProcessSelected' => Array ('self' => 'add|edit', 'subitem' => 'add|edit'),
				'OnStoreSelected' => Array ('self' => 'add|edit', 'subitem' => 'add|edit'),
				'OnSelectUser' => Array ('self' => 'add|edit', 'subitem' => 'add|edit'),

				'OnMassApprove' => Array ('self' => 'advanced:approve|edit', 'subitem' => 'advanced:approve|add|edit'),
				'OnMassDecline' => Array ('self' => 'advanced:decline|edit', 'subitem' => 'advanced:decline|add|edit'),
				'OnMassMoveUp' => Array ('self' => 'advanced:move_up|edit', 'subitem' => 'advanced:move_up|add|edit'),
				'OnMassMoveDown' => Array ('self' => 'advanced:move_down|edit', 'subitem' => 'advanced:move_down|add|edit'),

				'OnPreCreate' => Array ('self' => 'add|add.pending', 'subitem' => 'edit|edit.pending'),
				'OnEdit' => Array ('self' => 'edit|edit.pending', 'subitem' => 'edit|edit.pending'),

				'OnExport' => Array ('self' => 'view|advanced:export'),
				'OnExportBegin' => Array ('self' => 'view|advanced:export'),
				'OnExportProgress' => Array ('self' => 'view|advanced:export'),


				'OnSetAutoRefreshInterval' => Array ('self' => true, 'subitem' => true),
				'OnAutoRefreshToggle' => Array ('self' => true, 'subitem' => true),

				// theese event do not harm, but just in case check them too :)
				'OnCancelEdit' => Array ('self' => true, 'subitem' => true),
				'OnCancel' => Array ('self' => true, 'subitem' => true),
				'OnReset' => Array ('self' => true, 'subitem' => true),

				'OnSetSorting' => Array ('self' => true, 'subitem' => true),
				'OnSetSortingDirect' => Array ('self' => true, 'subitem' => true),
				'OnResetSorting' => Array ('self' => true, 'subitem' => true),

				'OnSetFilter' => Array ('self' => true, 'subitem' => true),
				'OnApplyFilters' => Array ('self' => true, 'subitem' => true),
				'OnRemoveFilters' => Array ('self' => true, 'subitem' => true),
				'OnSetFilterPattern' => Array ('self' => true, 'subitem' => true),

				'OnSetPerPage' => Array ('self' => true, 'subitem' => true),
				'OnSetPage' => Array ('self' => true, 'subitem' => true),

				'OnSearch' => Array ('self' => true, 'subitem' => true),
				'OnSearchReset' => Array ('self' => true, 'subitem' => true),

				'OnGoBack' => Array ('self' => true, 'subitem' => true),

				// it checks permission itself since flash uploader does not send cookies
				'OnUploadFile' => Array ('self' => true, 'subitem' => true),
				'OnDeleteFile' => Array ('self' => true, 'subitem' => true),

				'OnViewFile' => Array ('self' => true, 'subitem' => true),
				'OnSaveWidths' => Array ('self' => true, 'subitem' => true),

				'OnValidateMInputFields' => Array ('self' => 'view'),
				'OnValidateField' => Array ('self' => true, 'subitem' => true),
			);

			$this->permMapping = array_merge($this->permMapping, $permissions);
		}

		/**
		 * Define alternative event processing method names
		 *
		 * @return void
		 * @see kEventHandler::$eventMethods
		 * @access protected
		 */
		protected function mapEvents()
		{
			$events_map = Array (
				'OnRemoveFilters' => 'FilterAction',
				'OnApplyFilters' => 'FilterAction',
				'OnMassApprove' => 'iterateItems',
				'OnMassDecline' => 'iterateItems',
				'OnMassMoveUp' => 'iterateItems',
				'OnMassMoveDown' => 'iterateItems',
			);

			$this->eventMethods = array_merge($this->eventMethods, $events_map);
		}

		/**
		 * Returns ID of current item to be edited
		 * by checking ID passed in get/post as prefix_id
		 * or by looking at first from selected ids, stored.
		 * Returned id is also stored in Session in case
		 * it was explicitly passed as get/post
		 *
		 * @param kEvent $event
		 * @return int
		 * @access public
		 */
		public function getPassedID(kEvent &$event)
		{
			if ( $event->getEventParam('raise_warnings') === false ) {
				$event->setEventParam('raise_warnings', 1);
			}

			if ( $event->Special == 'previous' || $event->Special == 'next' ) {
				$object =& $this->Application->recallObject($event->getEventParam('item'));
				/* @var $object kDBItem */

				$list_helper =& $this->Application->recallObject('ListHelper');
				/* @var $list_helper ListHelper */

				$select_clause = $this->Application->getUnitOption($object->Prefix, 'NavigationSelectClause', null);

				return $list_helper->getNavigationResource($object, $event->getEventParam('list'), $event->Special == 'next', $select_clause);
			}
			elseif ( $event->Special == 'filter' ) {
				// temporary object, used to print filter options only
				return 0;
			}

			if ( preg_match('/^auto-(.*)/', $event->Special, $regs) && $this->Application->prefixRegistred($regs[1]) ) {
				// <inp2:lang.auto-phrase_Field name="DateFormat"/> - returns field DateFormat value from language (LanguageId is extracted from current phrase object)
				$main_object =& $this->Application->recallObject($regs[1]);
				/* @var $main_object kDBItem */

				$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
				return $main_object->GetDBField($id_field);
			}

			// 1. get id from post (used in admin)
			$ret = $this->Application->GetVar($event->getPrefixSpecial(true) . '_id');
			if ( ($ret !== false) && ($ret != '') ) {
				return $ret;
			}

			// 2. get id from env (used in front)
			$ret = $this->Application->GetVar($event->getPrefixSpecial() . '_id');
			if ( ($ret !== false) && ($ret != '') ) {
				return $ret;
			}

			// recall selected ids array and use the first one
			$ids = $this->Application->GetVar($event->getPrefixSpecial() . '_selected_ids');
			if ( $ids != '' ) {
				$ids = explode(',', $ids);
				if ( $ids ) {
					$ret = array_shift($ids);
				}
			}
			else { // if selected ids are not yet stored
				$this->StoreSelectedIDs($event);
				return $this->Application->GetVar($event->getPrefixSpecial() . '_id'); // StoreSelectedIDs sets this variable
			}

			return $ret;
		}

		/**
		 * Prepares and stores selected_ids string
		 * in Session and Application Variables
		 * by getting all checked ids from grid plus
		 * id passed in get/post as prefix_id
		 *
		 * @param kEvent $event
		 * @param Array $direct_ids
		 * @return Array
		 * @access protected
		 */
		protected function StoreSelectedIDs(kEvent &$event, $direct_ids = null)
		{
			$wid = $this->Application->GetTopmostWid($event->Prefix);
			$session_name = rtrim($event->getPrefixSpecial() . '_selected_ids_' . $wid, '_');

			$ids = $event->getEventParam('ids');
			if ( isset($direct_ids) || ($ids !== false) ) {
				// save ids directly if they given + reset array indexes
				$resulting_ids = $direct_ids ? array_values($direct_ids) : ($ids ? array_values($ids) : false);
				if ( $resulting_ids ) {
					$this->Application->SetVar($event->getPrefixSpecial() . '_selected_ids', implode(',', $resulting_ids));
					$this->Application->LinkVar($event->getPrefixSpecial() . '_selected_ids', $session_name, '', true);
					$this->Application->SetVar($event->getPrefixSpecial() . '_id', $resulting_ids[0]);

					return $resulting_ids;
				}

				return Array ();
			}

			$ret = Array ();

			// May be we don't need this part: ?
			$passed = $this->Application->GetVar($event->getPrefixSpecial(true) . '_id');
			if ( $passed !== false && $passed != '' ) {
				array_push($ret, $passed);
			}

			$ids = Array ();

			// get selected ids from post & save them to session
			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
			if ( $items_info ) {
				$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
				foreach ($items_info as $id => $field_values) {
					if ( getArrayValue($field_values, $id_field) ) {
						array_push($ids, $id);
					}
				}
				//$ids = array_keys($items_info);
			}

			$ret = array_unique(array_merge($ret, $ids));

			$this->Application->SetVar($event->getPrefixSpecial() . '_selected_ids', implode(',', $ret));
			$this->Application->LinkVar($event->getPrefixSpecial() . '_selected_ids', $session_name, '', !$ret); // optional when IDs are missing

			// This is critical - otherwise getPassedID will return last ID stored in session! (not exactly true)
			// this smells... needs to be refactored
			$first_id = getArrayValue($ret, 0);
			if ( ($first_id === false) && ($event->getEventParam('raise_warnings') == 1) ) {
				if ( $this->Application->isDebugMode() ) {
					$this->Application->Debugger->appendTrace();
				}

				trigger_error('Requested ID for prefix <strong>' . $event->getPrefixSpecial() . '</strong> <span class="debug_error">not passed</span>', E_USER_NOTICE);
			}

			$this->Application->SetVar($event->getPrefixSpecial() . '_id', $first_id);
			return $ret;
		}

		/**
		 * Returns stored selected ids as an array
		 *
		 * @param kEvent $event
		 * @param bool $from_session return ids from session (written, when editing was started)
		 * @return Array
		 * @access protected
		 */
		protected function getSelectedIDs(kEvent &$event, $from_session = false)
		{
			if ( $from_session ) {
				$wid = $this->Application->GetTopmostWid($event->Prefix);
				$var_name = rtrim($event->getPrefixSpecial() . '_selected_ids_' . $wid, '_');
				$ret = $this->Application->RecallVar($var_name);
			}
			else {
				$ret = $this->Application->GetVar($event->getPrefixSpecial() . '_selected_ids');
			}

			return explode(',', $ret);
		}

		/**
		 * Stores IDs, selected in grid in session
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnStoreSelected(kEvent &$event)
		{
			$this->StoreSelectedIDs($event);

			$id = $this->Application->GetVar($event->getPrefixSpecial() . '_id');

			if ( $id !== false ) {
				$event->SetRedirectParam($event->getPrefixSpecial() . '_id', $id);
				$event->SetRedirectParam('pass', 'all,' . $event->getPrefixSpecial());
			}
		}

		/**
		 * Returns associative array of submitted fields for current item
		 * Could be used while creating/editing single item -
		 * meaning on any edit form, except grid edit
		 *
		 * @param kEvent $event
		 * @return Array
		 * @access protected
		 */
		protected function getSubmittedFields(kEvent &$event)
		{
			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
			$field_values = $items_info ? array_shift($items_info) : Array ();

			return $field_values;
		}

		/**
		 * Returns fields, that are not allowed to be changed from request
		 *
		 * @param Array $hash
		 * @return Array
		 * @access protected
		 */
		protected function getRequestProtectedFields($hash)
		{
			// by default don't allow changing ID or foreign key from request
			$fields = Array ();
			$fields[] = $this->Application->getUnitOption($this->Prefix, 'IDField');

			$parent_prefix = $this->Application->getUnitOption($this->Prefix, 'ParentPrefix');

			if ( $parent_prefix ) {
				$foreign_key = $this->Application->getUnitOption($this->Prefix, 'ForeignKey');
				$fields[] = is_array($foreign_key) ? $foreign_key[$parent_prefix] : $foreign_key;
			}

			return $fields;
		}

		/**
		 * Removes any information about current/selected ids
		 * from Application variables and Session
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function clearSelectedIDs(kEvent &$event)
		{
			$prefix_special = $event->getPrefixSpecial();

			$ids = implode(',', $this->getSelectedIDs($event, true));
			$event->setEventParam('ids', $ids);

			$wid = $this->Application->GetTopmostWid($event->Prefix);
			$session_name = rtrim($prefix_special . '_selected_ids_' . $wid, '_');

			$this->Application->RemoveVar($session_name);
			$this->Application->SetVar($prefix_special . '_selected_ids', '');

			$this->Application->SetVar($prefix_special . '_id', ''); // $event->getPrefixSpecial(true) . '_id' too may be
		}

		/**
		 * Common builder part for Item & List
		 *
		 * @param kDBBase|kDBItem|kDBList $object
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function dbBuild(&$object, kEvent &$event)
		{
			// for permission checking inside item/list build events
			$event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true));

			if ( $event->getEventParam('form_name') !== false ) {
				$form_name = $event->getEventParam('form_name');
			}
			else {
				$request_forms = $this->Application->GetVar('forms', Array ());
				$form_name = (string)getArrayValue($request_forms, $object->getPrefixSpecial());
			}

			$object->Configure($event->getEventParam('populate_ml_fields') || $this->Application->getUnitOption($event->Prefix, 'PopulateMlFields'), $form_name);
			$this->PrepareObject($object, $event);

			$parent_event = $event->getEventParam('parent_event');

			if ( is_object($parent_event) ) {
				$object->setParentEvent($parent_event);
			}

			// force live table if specified or is original item
			$live_table = $event->getEventParam('live_table') || $event->Special == 'original';

			if ( $this->UseTempTables($event) && !$live_table ) {
				$object->SwitchToTemp();
			}

			$this->Application->setEvent($event->getPrefixSpecial(), '');

			$save_event = $this->UseTempTables($event) && $this->Application->GetTopmostPrefix($event->Prefix) == $event->Prefix ? 'OnSave' : 'OnUpdate';
			$this->Application->SetVar($event->getPrefixSpecial() . '_SaveEvent', $save_event);
		}

		/**
		 * Checks, that currently loaded item is allowed for viewing (non permission-based)
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access protected
		 */
		protected function checkItemStatus(kEvent &$event)
		{
			$status_fields = $this->Application->getUnitOption($event->Prefix, 'StatusField');
			if ( !$status_fields ) {
				return true;
			}

			$status_field = array_shift($status_fields);

			if ( $status_field == 'Status' || $status_field == 'Enabled' ) {
				$object =& $event->getObject();
				/* @var $object kDBItem */

				if ( !$object->isLoaded() ) {
					return true;
				}

				return $object->GetDBField($status_field) == STATUS_ACTIVE;
			}

			return true;
		}

		/**
		 * Shows not found template content
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function _errorNotFound(kEvent &$event)
		{
			if ( $event->getEventParam('raise_warnings') === 0 ) {
				// when it's possible, that autoload fails do nothing
				return;
			}

			if ( $this->Application->isDebugMode() ) {
				$this->Application->Debugger->appendTrace();
			}

			trigger_error('ItemLoad Permission Failed for prefix [' . $event->getPrefixSpecial() . '] in <strong>checkItemStatus</strong>, leading to "404 Not Found"', E_USER_NOTICE);

			$vars = $this->Application->UrlManager->prepare404();

			foreach ($vars as $var_name => $var_value) {
				$this->Application->SetVar($var_name, $var_value);
			}

			// in case if missing item is recalled first from event (not from template)
			$this->Application->QuickRun();
			$this->Application->Done();
			exit;
		}

		/**
		 * Builds item (loads if needed)
		 *
		 * Pattern: Prototype Manager
		 *
		 * @param kEvent $event
		 * @access protected
		 */
		protected function OnItemBuild(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$this->dbBuild($object, $event);

			$sql = $this->ItemPrepareQuery($event);
			$sql = $this->Application->ReplaceLanguageTags($sql);
			$object->setSelectSQL($sql);

			// 2. loads if allowed
			$auto_load = $this->Application->getUnitOption($event->Prefix,'AutoLoad');
			$skip_autoload = $event->getEventParam('skip_autoload');

			if ( $auto_load && !$skip_autoload ) {
				$perm_status = true;
				$user_id = $this->Application->InitDone ? $this->Application->RecallVar('user_id') : USER_ROOT;
				$event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true));
				$status_checked = false;
				if ( $user_id == USER_ROOT || $this->CheckPermission($event) ) {
					// don't autoload item, when user doesn't have view permission
					$this->LoadItem($event);

					$status_checked = true;
					$editing_mode = defined('EDITING_MODE') ? EDITING_MODE : false;

					if ( $user_id != USER_ROOT && !$this->Application->isAdmin && !($editing_mode || $this->checkItemStatus($event)) ) {
						// non-root user AND on front-end AND (not editing mode || incorrect status)
						$perm_status = false;
					}
				}
				else {
					$perm_status = false;
				}

				if ( !$perm_status ) {
					// when no permission to view item -> redirect to no permission template
					if ( $this->Application->isDebugMode() ) {
						$this->Application->Debugger->appendTrace();
					}

					trigger_error('ItemLoad Permission Failed for prefix [' . $event->getPrefixSpecial() . '] in <strong>' . ($status_checked ? 'checkItemStatus' : 'CheckPermission') . '</strong>', E_USER_NOTICE);
					$template = $this->Application->isAdmin ? 'no_permission' : $this->Application->ConfigValue('NoPermissionTemplate');

					if ( $this->Application->GetVar('t') != $template ) {
						// don't perform "no_permission" redirect if already on a "no_permission" template
						if ( MOD_REWRITE ) {
							$redirect_params = Array (
								'm_cat_id' => 0,
								'next_template' => urlencode('external:' . $_SERVER['REQUEST_URI']),
							);
						}
						else {
							$redirect_params = Array (
								'next_template' => $this->Application->GetVar('t'),
							);
						}

						$this->Application->Redirect($template, $redirect_params);
					}
				}
			}

			$actions =& $this->Application->recallObject('kActions');
			/* @var $actions Params */

			$actions->Set($event->getPrefixSpecial() . '_GoTab', '');
			$actions->Set($event->getPrefixSpecial() . '_GoId', '');
			$actions->Set('forms[' . $event->getPrefixSpecial() . ']', $object->getFormName());
		}

		/**
		 * Build sub-tables array from configs
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnTempHandlerBuild(kEvent &$event)
		{
			$object =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler');
			/* @var $object kTempTablesHandler */

			$parent_event = $event->getEventParam('parent_event');

			if ( is_object($parent_event) ) {
				$object->setParentEvent($parent_event);
			}

			$object->BuildTables($event->Prefix, $this->getSelectedIDs($event));
		}

		/**
		 * Checks, that object used in event should use temp tables
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access protected
		 */
		protected function UseTempTables(kEvent &$event)
		{
			$top_prefix = $this->Application->GetTopmostPrefix($event->Prefix); // passed parent, not always actual
			$special = ($top_prefix == $event->Prefix) ? $event->Special : $this->getMainSpecial($event);

			return $this->Application->IsTempMode($event->Prefix, $special);
		}

		/**
		 * Load item if id is available
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function LoadItem(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$id = $this->getPassedID($event);

			if ( $object->isLoaded() && !is_array($id) && ($object->GetID() == $id) ) {
				// object is already loaded by same id
				return ;
			}

			if ( $object->Load($id) ) {
				$actions =& $this->Application->recallObject('kActions');
				/* @var $actions Params */

				$actions->Set($event->getPrefixSpecial() . '_id', $object->GetID());
			}
			else {
				$object->setID( is_array($id) ? false : $id );
			}
		}

		/**
		 * Builds list
		 *
		 * Pattern: Prototype Manager
		 *
		 * @param kEvent $event
		 * @access protected
		 */
		protected function OnListBuild(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBList */

			/*if ( $this->Application->isDebugMode() ) {
				$event_params = http_build_query($event->getEventParams());
				$this->Application->Debugger->appendHTML('InitList "<strong>' . $event->getPrefixSpecial() . '</strong>" (' . $event_params . ')');
			}*/

			$this->dbBuild($object, $event);

			if ( !$object->isMainList() && $event->getEventParam('main_list') ) {
				// once list is set to main, then even "requery" parameter can't remove that
				/*$passed = $this->Application->GetVar('passed');
				$this->Application->SetVar('passed', $passed . ',' . $event->Prefix);*/

				$object->becameMain();
			}

			$object->setGridName($event->getEventParam('grid'));

			$sql = $this->ListPrepareQuery($event);
			$sql = $this->Application->ReplaceLanguageTags($sql);
			$object->setSelectSQL($sql);

			$object->reset();

			if ( $event->getEventParam('skip_parent_filter') === false ) {
				$object->linkToParent($this->getMainSpecial($event));
			}

			$this->AddFilters($event);
			$this->SetCustomQuery($event); // new!, use this for dynamic queries based on specials for ex.
			$this->SetPagination($event);
			$this->SetSorting($event);

			$actions =& $this->Application->recallObject('kActions');
			/* @var $actions Params */

			$actions->Set('remove_specials[' . $event->getPrefixSpecial() . ']', '0');
			$actions->Set($event->getPrefixSpecial() . '_GoTab', '');
		}

		/**
		 * Returns special of main item for linking with sub-item
		 *
		 * @param kEvent $event
		 * @return string
		 * @access protected
		 */
		protected function getMainSpecial(kEvent &$event)
		{
			$main_special = $event->getEventParam('main_special');

			if ( $main_special === false ) {
				// main item's special not passed

				if ( substr($event->Special, -5) == '-item' ) {
					// temp handler added "-item" to given special -> process that here
					return substr($event->Special, 0, -5);
				}

				// by default subitem's special is used for main item searching
				return $event->Special;
			}

			return $main_special;
		}

		/**
		 * Apply any custom changes to list's sql query
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetCustomQuery(kEvent &$event)
		{

		}

		/**
		 * Set's new per-page for grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetPerPage(kEvent &$event)
		{
			$per_page = $this->Application->GetVar($event->getPrefixSpecial(true) . '_PerPage');
			$event->SetRedirectParam($event->getPrefixSpecial() . '_PerPage', $per_page);
			$event->SetRedirectParam('pass', 'all,' . $event->getPrefixSpecial());

			if ( !$this->Application->isAdminUser ) {
				$list_helper =& $this->Application->recallObject('ListHelper');
				/* @var $list_helper ListHelper */

				$this->_passListParams($event, 'per_page');
			}
		}

		/**
		 * Occurs when page is changed (only for hooking)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetPage(kEvent &$event)
		{
			$page = $this->Application->GetVar($event->getPrefixSpecial(true) . '_Page');
			$event->SetRedirectParam($event->getPrefixSpecial() . '_Page', $page);
			$event->SetRedirectParam('pass', 'all,' . $event->getPrefixSpecial());

			if ( !$this->Application->isAdminUser ) {
				$this->_passListParams($event, 'page');
			}
		}

		/**
		 * Passes through main list pagination and sorting
		 *
		 * @param kEvent $event
		 * @param string $skip_var
		 * @return void
		 * @access protected
		 */
		protected function _passListParams(&$event, $skip_var)
		{
			$param_names = array_diff(Array ('page', 'per_page', 'sort_by'), Array ($skip_var));

			$list_helper =& $this->Application->recallObject('ListHelper');
			/* @var $list_helper ListHelper */

			foreach ($param_names as $param_name) {
				$value = $this->Application->GetVar($param_name);

				switch ($param_name) {
					case 'page':
						if ( $value > 1 ) {
							$event->SetRedirectParam('page', $value);
						}
						break;

					case 'per_page':
						if ( $value > 0 ) {
							if ( $value != $list_helper->getDefaultPerPage($event->Prefix) ) {
								$event->SetRedirectParam('per_page', $value);
							}
						}
						break;

					case 'sort_by':
						$event->setPseudoClass('_List');
						$object =& $event->getObject(Array ('main_list' => 1));
						/* @var $object kDBList */

						if ( $list_helper->hasUserSorting($object) ) {
							$event->SetRedirectParam('sort_by', $value);
						}
						break;
				}
			}
		}

		/**
		 * Set's correct page for list based on data provided with event
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetPagination(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBList */

			// get PerPage (forced -> session -> config -> 10)
			$object->SetPerPage($this->getPerPage($event));

			// main lists on Front-End have special get parameter for page
			$page = $object->isMainList() ? $this->Application->GetVar('page') : false;

			if ( !$page ) {
				// page is given in "env" variable for given prefix
				$page = $this->Application->GetVar($event->getPrefixSpecial() . '_Page');
			}

			if ( !$page && $event->Special ) {
				// when not part of env, then variables like "prefix.special_Page" are
				// replaced (by PHP) with "prefix_special_Page", so check for that too
				$page = $this->Application->GetVar($event->getPrefixSpecial(true) . '_Page');
			}

			if ( !$object->isMainList() ) {
				// main lists doesn't use session for page storing
				$this->Application->StoreVarDefault($event->getPrefixSpecial() . '_Page', 1, true); // true for optional

				if ( $page ) {
					// page found in request -> store in session
					$this->Application->StoreVar($event->getPrefixSpecial() . '_Page', $page, true); //true for optional
				}
				else {
					// page not found in request -> get from session
					$page = $this->Application->RecallVar($event->getPrefixSpecial() . '_Page');
				}

				if ( !$event->getEventParam('skip_counting') ) {
					// when stored page is larger, then maximal list page number
					// (such case is also processed in kDBList::Query method)
					$pages = $object->GetTotalPages();

					if ( $page > $pages ) {
						$page = 1;
						$this->Application->StoreVar($event->getPrefixSpecial() . '_Page', 1, true);
					}
				}
			}

			$object->SetPage($page);
		}

		/**
		 * Returns current per-page setting for list
		 *
		 * @param kEvent $event
		 * @return int
		 * @access protected
		 */
		protected function getPerPage(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBList */

			$per_page = $event->getEventParam('per_page');

			if ( $per_page ) {
				// per-page is passed as tag parameter to PrintList, InitList, etc.
				$config_mapping = $this->Application->getUnitOption($event->Prefix, 'ConfigMapping');

				// 2. per-page setting is stored in configuration variable
				if ( $config_mapping ) {
					// such pseudo per-pages are only defined in templates directly
					switch ($per_page) {
						case 'short_list':
							$per_page = $this->Application->ConfigValue($config_mapping['ShortListPerPage']);
							break;

						case 'default':
							$per_page = $this->Application->ConfigValue($config_mapping['PerPage']);
							break;
					}
				}

				return $per_page;
			}

			if ( !$per_page && $object->isMainList() ) {
				// main lists on Front-End have special get parameter for per-page
				$per_page = $this->Application->GetVar('per_page');
			}

			if ( !$per_page ) {
				// per-page is given in "env" variable for given prefix
				$per_page = $this->Application->GetVar($event->getPrefixSpecial() . '_PerPage');
			}

			if ( !$per_page && $event->Special ) {
				// when not part of env, then variables like "prefix.special_PerPage" are
				// replaced (by PHP) with "prefix_special_PerPage", so check for that too
				$per_page = $this->Application->GetVar($event->getPrefixSpecial(true) . '_PerPage');
			}

			if ( !$object->isMainList() ) {
				// per-page given in env and not in main list
				$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');

				if ( $per_page ) {
					// per-page found in request -> store in session and persistent session
					$this->setListSetting($event, 'PerPage', $per_page);
				}
				else {
					// per-page not found in request -> get from pesistent session (or session)
					$per_page = $this->getListSetting($event, 'PerPage');
				}
			}

			if ( !$per_page ) {
				// per page wan't found in request/session/persistent session
				$list_helper =& $this->Application->recallObject('ListHelper');
				/* @var $list_helper ListHelper */

				// allow to override default per-page value from tag
				$default_per_page = $event->getEventParam('default_per_page');

				if ( !is_numeric($default_per_page) ) {
					$default_per_page = 10;
				}

				$per_page = $list_helper->getDefaultPerPage($event->Prefix, $default_per_page);
			}

			return $per_page;
		}

		/**
		 * Set's correct sorting for list based on data provided with event
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetSorting(kEvent &$event)
		{
			$event->setPseudoClass('_List');

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

			if ( $object->isMainList() ) {
				$sort_by = $this->Application->GetVar('sort_by');
				$cur_sort1 = $cur_sort1_dir = $cur_sort2 = $cur_sort2_dir = false;

				if ( $sort_by ) {
					list ($cur_sort1, $cur_sort1_dir) = explode(',', $sort_by);
				}
			}
			else {
				$sorting_settings = $this->getListSetting($event, 'Sortings');

				$cur_sort1 = getArrayValue($sorting_settings, 'Sort1');
				$cur_sort1_dir = getArrayValue($sorting_settings, 'Sort1_Dir');
				$cur_sort2 = getArrayValue($sorting_settings, 'Sort2');
				$cur_sort2_dir = getArrayValue($sorting_settings, 'Sort2_Dir');
			}

			$tag_sort_by = $event->getEventParam('sort_by');

			if ( $tag_sort_by ) {
				if ( $tag_sort_by == 'random' ) {
					$object->AddOrderField('RAND()', '');
				}
				else {
					// multiple sortings could be specified at once
					$tag_sort_by = explode('|', $tag_sort_by);

					foreach ($tag_sort_by as $sorting_element) {
						list ($by, $dir) = explode(',', $sorting_element);
						$object->AddOrderField($by, $dir);
					}
				}
			}

			$list_sortings = $this->Application->getUnitOption($event->Prefix, 'ListSortings', Array ());
			$sorting_prefix = array_key_exists($event->Special, $list_sortings) ? $event->Special : '';
			$sorting_configs = $this->Application->getUnitOption($event->Prefix, 'ConfigMapping');

			if ( $sorting_configs && array_key_exists('DefaultSorting1Field', $sorting_configs) ) {
				// sorting defined in configuration variables overrides one from unit config
				$list_sortings[$sorting_prefix]['Sorting'] = Array (
					$this->Application->ConfigValue($sorting_configs['DefaultSorting1Field']) => $this->Application->ConfigValue($sorting_configs['DefaultSorting1Dir']),
					$this->Application->ConfigValue($sorting_configs['DefaultSorting2Field']) => $this->Application->ConfigValue($sorting_configs['DefaultSorting2Dir']),
				);

				// TODO: lowercase configuration variable values in db, instead of here
				$list_sortings[$sorting_prefix]['Sorting'] = array_map('strtolower', $list_sortings[$sorting_prefix]['Sorting']);
			}

			// use default if not specified in session
			if ( !$cur_sort1 || !$cur_sort1_dir ) {
				$sorting = getArrayValue($list_sortings, $sorting_prefix, 'Sorting');

				if ( $sorting ) {
					reset($sorting);
					$cur_sort1 = key($sorting);
					$cur_sort1_dir = current($sorting);

					if ( next($sorting) ) {
						$cur_sort2 = key($sorting);
						$cur_sort2_dir = current($sorting);
					}
				}
			}

			// always add forced sorting before any user sorting fields
			$forced_sorting = getArrayValue($list_sortings, $sorting_prefix, 'ForcedSorting');
			/* @var $forced_sorting Array */

			if ( $forced_sorting ) {
				foreach ($forced_sorting as $field => $dir) {
					$object->AddOrderField($field, $dir);
				}
			}

			// add user sorting fields
			if ( $cur_sort1 != '' && $cur_sort1_dir != '' ) {
				$object->AddOrderField($cur_sort1, $cur_sort1_dir);
			}

			if ( $cur_sort2 != '' && $cur_sort2_dir != '' ) {
				$object->AddOrderField($cur_sort2, $cur_sort2_dir);
			}
		}

		/**
		 * Gets list setting by name (persistent or real session)
		 *
		 * @param kEvent $event
		 * @param string $variable_name
		 * @return string|Array
		 * @access protected
		 */
		protected function getListSetting(kEvent &$event, $variable_name)
		{
			$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');
			$storage_prefix = $event->getEventParam('same_special') ? $event->Prefix : $event->getPrefixSpecial();

			// get sorting from persistent session
			$variable_value = $this->Application->RecallPersistentVar($storage_prefix . '_' . $variable_name . '.' . $view_name, ALLOW_DEFAULT_SETTINGS);

			/*if ( !$variable_value ) {
				// get sorting from session
				$variable_value = $this->Application->RecallVar($storage_prefix . '_' . $variable_name);
			}*/

			if ( kUtil::IsSerialized($variable_value) ) {
				$variable_value = unserialize($variable_value);
			}

			return $variable_value;
		}

		/**
		 * Sets list setting by name (persistent and real session)
		 *
		 * @param kEvent $event
		 * @param string $variable_name
		 * @param string|Array $variable_value
		 * @return void
		 * @access protected
		 */
		protected function setListSetting(kEvent &$event, $variable_name, $variable_value = null)
		{
			$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');
//			$this->Application->StoreVar($event->getPrefixSpecial() . '_' . $variable_name, $variable_value, true); //true for optional

			if ( isset($variable_value) ) {
				if ( is_array($variable_value) ) {
					$variable_value = serialize($variable_value);
				}

				$this->Application->StorePersistentVar($event->getPrefixSpecial() . '_' . $variable_name . '.' . $view_name, $variable_value, true); //true for optional
			}
			else {
				$this->Application->RemovePersistentVar($event->getPrefixSpecial() . '_' . $variable_name . '.' . $view_name);
			}
		}

		/**
		 * Add filters found in session
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function AddFilters(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBList */

			$edit_mark = rtrim($this->Application->GetSID() . '_' . $this->Application->GetTopmostWid($event->Prefix), '_');

			// add search filter
			$filter_data = $this->Application->RecallVar($event->getPrefixSpecial() . '_search_filter');

			if ( $filter_data ) {
				$filter_data = unserialize($filter_data);

				foreach ($filter_data as $filter_field => $filter_params) {
					$filter_type = ($filter_params['type'] == 'having') ? kDBList::HAVING_FILTER : kDBList::WHERE_FILTER;
					$filter_value = str_replace(EDIT_MARK, $edit_mark, $filter_params['value']);
					$object->addFilter($filter_field, $filter_value, $filter_type, kDBList::FLT_SEARCH);
				}
			}

			// add custom filter
			$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');
			$custom_filters = $this->Application->RecallPersistentVar($event->getPrefixSpecial() . '_custom_filter.' . $view_name);

			if ( $custom_filters ) {
				$grid_name = $event->getEventParam('grid');
				$custom_filters = unserialize($custom_filters);

				if ( isset($custom_filters[$grid_name]) ) {
					foreach ($custom_filters[$grid_name] as $field_name => $field_options) {
						list ($filter_type, $field_options) = each($field_options);

						if ( isset($field_options['value']) && $field_options['value'] ) {
							$filter_type = ($field_options['sql_filter_type'] == 'having') ? kDBList::HAVING_FILTER : kDBList::WHERE_FILTER;
							$filter_value = str_replace(EDIT_MARK, $edit_mark, $field_options['value']);
							$object->addFilter($field_name, $filter_value, $filter_type, kDBList::FLT_CUSTOM);
						}
					}
				}
			}

			// add view filter
			$view_filter = $this->Application->RecallVar($event->getPrefixSpecial() . '_view_filter');

			if ( $view_filter ) {
				$view_filter = unserialize($view_filter);

				$temp_filter =& $this->Application->makeClass('kMultipleFilter');
				/* @var $temp_filter kMultipleFilter */

				$filter_menu = $this->Application->getUnitOption($event->Prefix, 'FilterMenu');

				$group_key = 0;
				$group_count = count($filter_menu['Groups']);

				while ($group_key < $group_count) {
					$group_info = $filter_menu['Groups'][$group_key];

					$temp_filter->setType(constant('kDBList::FLT_TYPE_' . $group_info['mode']));
					$temp_filter->clearFilters();

					foreach ($group_info['filters'] as $flt_id) {
						$sql_key = getArrayValue($view_filter, $flt_id) ? 'on_sql' : 'off_sql';

						if ( $filter_menu['Filters'][$flt_id][$sql_key] != '' ) {
							$temp_filter->addFilter('view_filter_' . $flt_id, $filter_menu['Filters'][$flt_id][$sql_key]);
						}
					}

					$object->addFilter('view_group_' . $group_key, $temp_filter, $group_info['type'], kDBList::FLT_VIEW);
					$group_key++;
				}
			}

			// add item filter
			if ( $object->isMainList() ) {
				$this->applyItemFilters($event);
			}
		}

		/**
		 * Applies item filters
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function applyItemFilters(&$event)
		{
			$filter_values = $this->Application->GetVar('filters', Array ());

			if ( !$filter_values ) {
				return;
			}

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

			$where_clause = Array (
				'ItemPrefix = ' . $this->Conn->qstr($object->Prefix),
				'FilterField IN (' . implode(',', $this->Conn->qstrArray(array_keys($filter_values))) . ')',
				'Enabled = 1',
			);

			$sql = 'SELECT *
					FROM ' . $this->Application->getUnitOption('item-filter', 'TableName') . '
					WHERE (' . implode(') AND (', $where_clause) . ')';
			$filters = $this->Conn->Query($sql, 'FilterField');

			foreach ($filters as $filter_field => $filter_data) {
				$filter_value = $filter_values[$filter_field];

				if ( "$filter_value" === '' ) {
					// ListManager don't pass empty values, but check here just in case
					continue;
				}

				$table_name = $object->isVirtualField($filter_field) ? '' : '%1$s.';

				switch ($filter_data['FilterType']) {
					case 'radio':
						$filter_value = $table_name . '`' . $filter_field . '` = ' . $this->Conn->qstr($filter_value);
						break;

					case 'checkbox':
						$filter_value = explode('|', substr($filter_value, 1, -1));
						$filter_value = $this->Conn->qstrArray($filter_value, 'escape');

						if ( $object->GetFieldOption($filter_field, 'multiple') ) {
							$filter_value = $table_name . '`' . $filter_field . '` LIKE "%|' . implode('|%" OR ' . $table_name . '`' . $filter_field . '` LIKE "%|', $filter_value) . '|%"';
						}
						else {
							$filter_value = $table_name . '`' . $filter_field . '` IN (' . implode(',', $filter_value) . ')';
						}
						break;

					case 'range':
						$filter_value = $this->Conn->qstrArray(explode('-', $filter_value));
						$filter_value = $table_name . '`' . $filter_field . '` BETWEEN ' . $filter_value[0] . ' AND ' . $filter_value[1];
						break;
				}

				$object->addFilter('item_filter_' . $filter_field, $filter_value, $object->isVirtualField($filter_field) ? kDBList::HAVING_FILTER : kDBList::WHERE_FILTER);
			}
		}

		/**
		 * Set's new sorting for list
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetSorting(kEvent &$event)
		{
			$sorting_settings = $this->getListSetting($event, 'Sortings');
			$cur_sort1 = getArrayValue($sorting_settings, 'Sort1');
			$cur_sort1_dir = getArrayValue($sorting_settings, 'Sort1_Dir');

			$use_double_sorting = $this->Application->ConfigValue('UseDoubleSorting');

			if ( $use_double_sorting ) {
				$cur_sort2 = getArrayValue($sorting_settings, 'Sort2');
				$cur_sort2_dir = getArrayValue($sorting_settings, 'Sort2_Dir');
			}

			$passed_sort1 = $this->Application->GetVar($event->getPrefixSpecial(true) . '_Sort1');
			if ( $cur_sort1 == $passed_sort1 ) {
				$cur_sort1_dir = $cur_sort1_dir == 'asc' ? 'desc' : 'asc';
			}
			else {
				if ( $use_double_sorting ) {
					$cur_sort2 = $cur_sort1;
					$cur_sort2_dir = $cur_sort1_dir;
				}

				$cur_sort1 = $passed_sort1;
				$cur_sort1_dir = 'asc';
			}

			$sorting_settings = Array ('Sort1' => $cur_sort1, 'Sort1_Dir' => $cur_sort1_dir);

			if ( $use_double_sorting ) {
				$sorting_settings['Sort2'] = $cur_sort2;
				$sorting_settings['Sort2_Dir'] = $cur_sort2_dir;
			}

			$this->setListSetting($event, 'Sortings', $sorting_settings);
		}

		/**
		 * Set sorting directly to session (used for category item sorting (front-end), grid sorting (admin, view menu)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetSortingDirect(kEvent &$event)
		{
			// used on Front-End in category item lists
			$prefix_special = $event->getPrefixSpecial();
			$combined = $this->Application->GetVar($event->getPrefixSpecial(true) . '_CombinedSorting');

			if ( $combined ) {
				list ($field, $dir) = explode('|', $combined);

				if ( $this->Application->isAdmin || !$this->Application->GetVar('main_list') ) {
					$this->setListSetting($event, 'Sortings', Array ('Sort1' => $field, 'Sort1_Dir' => $dir));
				}
				else {
					$event->setPseudoClass('_List');
					$this->Application->SetVar('sort_by', $field . ',' . $dir);

					$object =& $event->getObject(Array ('main_list' => 1));
					/* @var $object kDBList */

					$list_helper =& $this->Application->recallObject('ListHelper');
					/* @var $list_helper ListHelper */

					$this->_passListParams($event, 'sort_by');

					if ( $list_helper->hasUserSorting($object) ) {
						$event->SetRedirectParam('sort_by', $field . ',' . strtolower($dir));
					}

					$event->SetRedirectParam('pass', 'm');
				}

				return;
			}

			// used in "View Menu -> Sort" menu in administrative console
			$field_pos = $this->Application->GetVar($event->getPrefixSpecial(true) . '_SortPos');
			$this->Application->LinkVar($event->getPrefixSpecial(true) . '_Sort' . $field_pos, $prefix_special . '_Sort' . $field_pos);
			$this->Application->LinkVar($event->getPrefixSpecial(true) . '_Sort' . $field_pos . '_Dir', $prefix_special . '_Sort' . $field_pos . '_Dir');
		}

		/**
		 * Reset grid sorting to default (from config)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnResetSorting(kEvent &$event)
		{
			$this->setListSetting($event, 'Sortings');
		}

		/**
		 * Sets grid refresh interval
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetAutoRefreshInterval(kEvent &$event)
		{
			$refresh_interval = $this->Application->GetVar('refresh_interval');

			$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');
			$this->Application->StorePersistentVar($event->getPrefixSpecial() . '_refresh_interval.' . $view_name, $refresh_interval);
		}

		/**
		 * Changes auto-refresh state for grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAutoRefreshToggle(kEvent &$event)
		{
			$refresh_intervals = $this->Application->ConfigValue('AutoRefreshIntervals');
			if ( !$refresh_intervals ) {
				return;
			}

			$view_name = $this->Application->RecallVar($event->getPrefixSpecial() . '_current_view');
			$auto_refresh = $this->Application->RecallPersistentVar($event->getPrefixSpecial() . '_auto_refresh.' . $view_name);

			if ( $auto_refresh === false ) {
				$refresh_intervals = explode(',', $refresh_intervals);
				$this->Application->StorePersistentVar($event->getPrefixSpecial() . '_refresh_interval.' . $view_name, $refresh_intervals[0]);
			}

			$this->Application->StorePersistentVar($event->getPrefixSpecial() . '_auto_refresh.' . $view_name, $auto_refresh ? 0 : 1);
		}

		/**
		 * Creates needed sql query to load item,
		 * if no query is defined in config for
		 * special requested, then use list query
		 *
		 * @param kEvent $event
		 * @return string
		 * @access protected
		 */
		protected function ItemPrepareQuery(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$sqls = $object->getFormOption('ItemSQLs', Array ());
			$special = isset($sqls[$event->Special]) ? $event->Special : '';

			// preferred special not found in ItemSQLs -> use analog from ListSQLs

			return isset($sqls[$special]) ? $sqls[$special] : $this->ListPrepareQuery($event);
		}

		/**
		 * Creates needed sql query to load list,
		 * if no query is defined in config for
		 * special requested, then use default
		 * query
		 *
		 * @param kEvent $event
		 * @return string
		 * @access protected
		 */
		protected function ListPrepareQuery(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$sqls = $object->getFormOption('ListSQLs', Array ());

			return $sqls[array_key_exists($event->Special, $sqls) ? $event->Special : ''];
		}

		/**
		 * Apply custom processing to item
		 *
		 * @param kEvent $event
		 * @param string $type
		 * @return void
		 * @access protected
		 */
		protected function customProcessing(kEvent &$event, $type)
		{

		}

		/* Edit Events mostly used in Admin */

		/**
		 * Creates new kDBItem
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnCreate(kEvent &$event)
		{
			$object =& $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

			if ( !$items_info ) {
				return;
			}

			list($id, $field_values) = each($items_info);
			$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));

			$this->customProcessing($event, 'before');

			// look at kDBItem' Create for ForceCreateId description, it's rarely used and is NOT set by default
			if ( $object->Create($event->getEventParam('ForceCreateId')) ) {
				$this->customProcessing($event, 'after');
				$event->SetRedirectParam('opener', 'u');
				return;
			}

			$event->redirect = false;
			$event->status = kEvent::erFAIL;
			$this->Application->SetVar($event->getPrefixSpecial() . '_SaveEvent', 'OnCreate');
			$object->setID($id);
		}

		/**
		 * Updates kDBItem
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnUpdate(kEvent &$event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$this->_update($event);

			$event->SetRedirectParam('opener', 'u');
		}

		/**
		 * Updates data in database based on request
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function _update(kEvent &$event)
		{
			$object =& $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );

			if ( $items_info ) {
				foreach ($items_info as $id => $field_values) {
					$object->Load($id);
					$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
					$this->customProcessing($event, 'before');

					if ( $object->Update($id) ) {
						$this->customProcessing($event, 'after');
						$event->status = kEvent::erSUCCESS;
					}
					else {
						$event->status = kEvent::erFAIL;
						$event->redirect = false;
						break;
					}
				}
			}
		}

		/**
		 * Delete's kDBItem object
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnDelete(kEvent &$event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$temp_handler->DeleteItems($event->Prefix, $event->Special, Array ($this->getPassedID($event)));
		}

		/**
		 * Deletes all records from table
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnDeleteAll(kEvent &$event)
		{
			$sql = 'SELECT ' . $this->Application->getUnitOption($event->Prefix, 'IDField') . '
					FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName');
			$ids = $this->Conn->GetCol($sql);

			if ( $ids ) {
				$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
				/* @var $temp_handler kTempTablesHandler */

				$temp_handler->DeleteItems($event->Prefix, $event->Special, $ids);
			}
		}

		/**
		 * Prepares new kDBItem object
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnNew(kEvent &$event)
		{
			$object =& $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$object->Clear(0);
			$this->Application->SetVar($event->getPrefixSpecial() . '_SaveEvent', 'OnCreate');

			if ( $event->getEventParam('top_prefix') != $event->Prefix ) {
				// this is subitem prefix, so use main item special
				$table_info = $object->getLinkedInfo($this->getMainSpecial($event));
			}
			else {
				$table_info = $object->getLinkedInfo();
			}

			$object->SetDBField($table_info['ForeignKey'], $table_info['ParentId']);

			$event->redirect = false;
		}

		/**
		 * Cancels kDBItem Editing/Creation
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnCancel(kEvent &$event)
		{
			$object =& $event->getObject(Array ('skip_autoload' => true));
			/* @var $object kDBItem */

			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

			if ( $items_info ) {
				$delete_ids = Array ();

				$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
				/* @var $temp_handler kTempTablesHandler */

				foreach ($items_info as $id => $field_values) {
					$object->Load($id);
					// record created for using with selector (e.g. Reviews->Select User), and not validated => Delete it
					if ( $object->isLoaded() && !$object->Validate() && ($id <= 0) ) {
						$delete_ids[] = $id;
					}
				}

				if ( $delete_ids ) {
					$temp_handler->DeleteItems($event->Prefix, $event->Special, $delete_ids);
				}
			}

			$event->SetRedirectParam('opener', 'u');
		}

		/**
		 * Deletes all selected items.
		 * Automatically recurse into sub-items using temp handler, and deletes sub-items
		 * by calling its Delete method if sub-item has AutoDelete set to true in its config file
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnMassDelete(kEvent &$event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return ;
			}

			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$ids = $this->StoreSelectedIDs($event);

			$event->setEventParam('ids', $ids);
			$this->customProcessing($event, 'before');
			$ids = $event->getEventParam('ids');

			if ( $ids ) {
				$temp_handler->DeleteItems($event->Prefix, $event->Special, $ids);
			}

			$this->clearSelectedIDs($event);
		}

		/**
		 * Sets window id (of first opened edit window) to temp mark in uls
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function setTempWindowID(kEvent &$event)
		{
			$prefixes = Array ($event->Prefix, $event->getPrefixSpecial(true));

			foreach ($prefixes as $prefix) {
				$mode = $this->Application->GetVar($prefix . '_mode');

				if ($mode == 't') {
					$wid = $this->Application->GetVar('m_wid');
					$this->Application->SetVar(str_replace('_', '.', $prefix) . '_mode', 't' . $wid);
					break;
				}
			}
		}

		/**
		 * Prepare temp tables and populate it
		 * with items selected in the grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnEdit(kEvent &$event)
		{
			$this->setTempWindowID($event);
			$ids = $this->StoreSelectedIDs($event);

			$this->Application->RemoveVar($this->_getPendingActionVariableName($event));

			$changes_var_name = $this->Prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
			$this->Application->RemoveVar($changes_var_name);

			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$temp_handler->PrepareEdit();

			$event->SetRedirectParam('m_lang', $this->Application->GetDefaultLanguageId());
			$event->SetRedirectParam($event->getPrefixSpecial() . '_id', array_shift($ids));
			$event->SetRedirectParam('pass', 'all,' . $event->getPrefixSpecial());
		}

		/**
		 * Saves content of temp table into live and
		 * redirects to event' default redirect (normally grid template)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSave(kEvent &$event)
		{
			$event->CallSubEvent('OnPreSave');

			if ( $event->status != kEvent::erSUCCESS ) {
				return;
			}

			$skip_master = false;
			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$changes_var_name = $this->Prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);

			if ( !$this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$live_ids = $temp_handler->SaveEdit($event->getEventParam('master_ids') ? $event->getEventParam('master_ids') : Array ());

				if ( $live_ids === false ) {
					// coping from table failed, because we have another coping process to same table, that wasn't finished
					$event->status = kEvent::erFAIL;
					return;
				}

				if ( $live_ids ) {
					// ensure, that newly created item ids are available as if they were selected from grid
					// NOTE: only works if main item has sub-items !!!
					$this->StoreSelectedIDs($event, $live_ids);
				}

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

				$this->SaveLoggedChanges($changes_var_name, $object->ShouldLogChanges());
			}
			else {
				$event->status = kEvent::erFAIL;
			}

			$this->clearSelectedIDs($event);

			$event->SetRedirectParam('opener', 'u');
			$this->Application->RemoveVar($event->getPrefixSpecial() . '_modified');

			// all temp tables are deleted here => all after hooks should think, that it's live mode now
			$this->Application->SetVar($event->Prefix . '_mode', '');
		}

		/**
		 * Saves changes made in temporary table to log
		 *
		 * @param string $changes_var_name
		 * @param bool $save
		 * @return void
		 * @access public
		 */
		public function SaveLoggedChanges($changes_var_name, $save = true)
		{
			// 1. get changes, that were made
			$changes = $this->Application->RecallVar($changes_var_name);
			$changes = $changes ? unserialize($changes) : Array ();
			$this->Application->RemoveVar($changes_var_name);

			if (!$changes) {
				// no changes, skip processing
				return ;
			}

			// TODO: 2. optimize change log records (replace multiple changes to same record with one change record)

			$to_increment = Array ();

			// 3. collect serials to reset based on foreign keys
			foreach ($changes as $index => $rec) {
				if (array_key_exists('DependentFields', $rec)) {

					foreach ($rec['DependentFields'] as $field_name => $field_value) {
						// will be "ci|ItemResourceId:345"
						$to_increment[] = $rec['Prefix'] . '|' . $field_name . ':' . $field_value;

						// also reset sub-item prefix general serial
						$to_increment[] = $rec['Prefix'];
					}

					unset($changes[$index]['DependentFields']);
				}

				unset($changes[$index]['ParentId'], $changes[$index]['ParentPrefix']);
			}

			// 4. collect serials to reset based on changed ids
			foreach ($changes as $change) {
				$to_increment[] = $change['MasterPrefix'] . '|' . $change['MasterId'];

				if ($change['MasterPrefix'] != $change['Prefix']) {
					// also reset sub-item prefix general serial
					$to_increment[] = $change['Prefix'];

					// will be "ci|ItemResourceId"
					$to_increment[] = $change['Prefix'] . '|' . $change['ItemId'];
				}
			}

			// 5. reset serials collected before
			$to_increment = array_unique($to_increment);
			$this->Application->incrementCacheSerial($this->Prefix);

			foreach ($to_increment as $to_increment_mixed) {
				if (strpos($to_increment_mixed, '|') !== false) {
					list ($to_increment_prefix, $to_increment_id) = explode('|', $to_increment_mixed, 2);
					$this->Application->incrementCacheSerial($to_increment_prefix, $to_increment_id);
				}
				else {
					$this->Application->incrementCacheSerial($to_increment_mixed);
				}
			}

			// save changes to database
			$sesion_log_id = $this->Application->RecallVar('_SessionLogId_');

			if (!$save || !$sesion_log_id) {
				// saving changes to database disabled OR related session log missing
				return ;
			}

			$add_fields = Array (
				'PortalUserId' => $this->Application->RecallVar('user_id'),
				'SessionLogId' => $sesion_log_id,
			);

			$change_log_table = $this->Application->getUnitOption('change-log', 'TableName');

			foreach ($changes as $rec) {
				$this->Conn->doInsert(array_merge($rec, $add_fields), $change_log_table);
			}

			$this->Application->incrementCacheSerial('change-log');

			$sql = 'UPDATE ' . $this->Application->getUnitOption('session-log', 'TableName') . '
					SET AffectedItems = AffectedItems + ' . count($changes) . '
					WHERE SessionLogId = ' . $sesion_log_id;
			$this->Conn->Query($sql);

			$this->Application->incrementCacheSerial('session-log');
		}

		/**
		 * Cancels edit
		 * Removes all temp tables and clears selected ids
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnCancelEdit(kEvent &$event)
		{
			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$temp_handler->CancelEdit();
			$this->clearSelectedIDs($event);

			$this->Application->RemoveVar($event->getPrefixSpecial() . '_modified');

			$changes_var_name = $this->Prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
			$this->Application->RemoveVar($changes_var_name);

			$event->SetRedirectParam('opener', 'u');
		}

		/**
		 * Allows to determine if we are creating new item or editing already created item
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function isNewItemCreate(kEvent &$event)
		{
			$object =& $event->getObject( Array ('raise_warnings' => 0) );
			/* @var $object kDBItem */

			return !$object->isLoaded();
		}

		/**
		 * Saves edited item into temp table
		 * If there is no id, new item is created in temp table
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSave(kEvent &$event)
		{
			// if there is no id - it means we need to create an item
			if ( is_object($event->MasterEvent) ) {
				$event->MasterEvent->setEventParam('IsNew', false);
			}

			if ( $this->isNewItemCreate($event) ) {
				$event->CallSubEvent('OnPreSaveCreated');

				if ( is_object($event->MasterEvent) ) {
					$event->MasterEvent->setEventParam('IsNew', true);
				}

				return ;
			}

			// don't just call OnUpdate event here, since it maybe overwritten to Front-End specific behavior
			$this->_update($event);
		}

		/**
		 * [HOOK] Saves sub-item
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSaveSubItem(kEvent &$event)
		{
			$not_created = $this->isNewItemCreate($event);

			$event->CallSubEvent($not_created ? 'OnCreate' : 'OnUpdate');
			if ( $event->status == kEvent::erSUCCESS ) {
				$object =& $event->getObject();
				/* @var $object kDBItem */

				$this->Application->SetVar($event->getPrefixSpecial() . '_id', $object->GetID());
			}
			else {
				$event->MasterEvent->status = $event->status;
			}

			$event->SetRedirectParam('opener', 's');
		}

		/**
		 * Saves edited item in temp table and loads
		 * item with passed id in current template
		 * Used in Prev/Next buttons
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSaveAndGo(kEvent &$event)
		{
			$event->CallSubEvent('OnPreSave');

			if ( $event->status == kEvent::erSUCCESS ) {
				$id = $this->Application->GetVar($event->getPrefixSpecial(true) . '_GoId');
				$event->SetRedirectParam($event->getPrefixSpecial() . '_id', $id);
			}
		}

		/**
		 * Saves edited item in temp table and goes
		 * to passed tabs, by redirecting to it with OnPreSave event
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSaveAndGoToTab(kEvent &$event)
		{
			$event->CallSubEvent('OnPreSave');

			if ( $event->status == kEvent::erSUCCESS ) {
				$event->redirect = $this->Application->GetVar($event->getPrefixSpecial(true) . '_GoTab');
			}
		}

		/**
		 * Saves editable list and goes to passed tab,
		 * by redirecting to it with empty event
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnUpdateAndGoToTab(kEvent &$event)
		{
			$event->setPseudoClass('_List');
			$event->CallSubEvent('OnUpdate');

			if ( $event->status == kEvent::erSUCCESS ) {
				$event->redirect = $this->Application->GetVar($event->getPrefixSpecial(true) . '_GoTab');
			}
		}

		/**
		 * Prepare temp tables for creating new item
		 * but does not create it. Actual create is
		 * done in OnPreSaveCreated
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreCreate(kEvent &$event)
		{
			$this->setTempWindowID($event);
			$this->clearSelectedIDs($event);
			$this->Application->SetVar('m_lang', $this->Application->GetDefaultLanguageId());

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

			$temp_handler =& $this->Application->recallObject($event->Prefix . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$temp_handler->PrepareEdit();

			$object->setID(0);
			$this->Application->SetVar($event->getPrefixSpecial() . '_id', 0);
			$this->Application->SetVar($event->getPrefixSpecial() . '_PreCreate', 1);

			$changes_var_name = $this->Prefix . '_changes_' . $this->Application->GetTopmostWid($this->Prefix);
			$this->Application->RemoveVar($changes_var_name);

			$event->redirect = false;
		}

		/**
		 * Creates a new item in temp table and
		 * stores item id in App vars and Session on success
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSaveCreated(kEvent &$event)
		{
			$object =& $event->getObject( Array('skip_autoload' => true) );
			/* @var $object kDBItem */

			$field_values = $this->getSubmittedFields($event);
			$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));

			$this->customProcessing($event, 'before');

			if ( $object->Create() ) {
				$this->customProcessing($event, 'after');
				$event->SetRedirectParam($event->getPrefixSpecial(true) . '_id', $object->GetID());
			}
			else {
				$event->status = kEvent::erFAIL;
				$event->redirect = false;
				$object->setID(0);
			}
		}

		/**
		 * Reloads form to loose all changes made during item editing
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnReset(kEvent &$event)
		{
			//do nothing - should reset :)
			if ( $this->isNewItemCreate($event) ) {
				// just reset id to 0 in case it was create
				$object =& $event->getObject( Array ('skip_autoload' => true) );
				/* @var $object kDBItem */

				$object->setID(0);
				$this->Application->SetVar($event->getPrefixSpecial() . '_id', 0);
			}
		}

		/**
		 * Apply same processing to each item being selected in grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function iterateItems(kEvent &$event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return ;
			}

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

			$ids = $this->StoreSelectedIDs($event);

			if ( $ids ) {
				$status_field = array_shift( $this->Application->getUnitOption($event->Prefix, 'StatusField') );
				$order_field = $this->Application->getUnitOption($event->Prefix, 'OrderField');

				if ( !$order_field ) {
					$order_field = 'Priority';
				}

				foreach ($ids as $id) {
					$object->Load($id);

					switch ( $event->Name ) {
						case 'OnMassApprove':
							$object->SetDBField($status_field, 1);
							break;

						case 'OnMassDecline':
							$object->SetDBField($status_field, 0);
							break;

						case 'OnMassMoveUp':
							$object->SetDBField($order_field, $object->GetDBField($order_field) + 1);
							break;

						case 'OnMassMoveDown':
							$object->SetDBField($order_field, $object->GetDBField($order_field) - 1);
							break;
					}

					if ( $object->Update() ) {
						$event->status = kEvent::erSUCCESS;
					}
					else {
						$event->status = kEvent::erFAIL;
						$event->redirect = false;
						break;
					}
				}
			}

			$this->clearSelectedIDs($event);
		}

		/**
		 * Clones selected items in list
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnMassClone(kEvent &$event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler', Array ('parent_event' => &$event));
			/* @var $temp_handler kTempTablesHandler */

			$ids = $this->StoreSelectedIDs($event);

			if ( $ids ) {
				$temp_handler->CloneItems($event->Prefix, $event->Special, $ids);
			}

			$this->clearSelectedIDs($event);
		}

		/**
		 * Checks if given value is present in given array
		 *
		 * @param Array $records
		 * @param string $field
		 * @param mixed $value
		 * @return bool
		 * @access protected
		 */
		protected function check_array($records, $field, $value)
		{
			foreach ($records as $record) {
				if ($record[$field] == $value) {
					return true;
				}
			}

			return false;
		}

		/**
		 * Saves data from editing form to database without checking required fields
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSavePopup(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$this->RemoveRequiredFields($object);
			$event->CallSubEvent('OnPreSave');

			$event->SetRedirectParam('opener', 'u');
		}


/* End of Edit events */

		// III. Events that allow to put some code before and after Update,Load,Create and Delete methods of item

		/**
		 * Occurs before loading item, 'id' parameter
		 * allows to get id of item being loaded
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemLoad(kEvent &$event)
		{

		}

		/**
		 * Occurs after loading item, 'id' parameter
		 * allows to get id of item that was loaded
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemLoad(kEvent &$event)
		{

		}

		/**
		 * Occurs before creating item
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemCreate(kEvent &$event)
		{

		}

		/**
		 * Occurs after creating item
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemCreate(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			if ( !$object->IsTempTable() ) {
				$this->_proccessPendingActions($event);
			}
		}

		/**
		 * Occurs before updating item
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemUpdate(kEvent &$event)
		{

		}

		/**
		 * Occurs after updating item
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemUpdate(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			if ( !$object->IsTempTable() ) {
				$this->_proccessPendingActions($event);
			}
		}

		/**
		 * Occurs before deleting item, id of item being
		 * deleted is stored as 'id' event param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemDelete(kEvent &$event)
		{

		}

		/**
		 * Occurs after deleting item, id of deleted item
		 * is stored as 'id' param of event
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemDelete(kEvent &$event)
		{

		}

		/**
		 * Occurs before validation attempt
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemValidate(kEvent &$event)
		{

		}

		/**
		 * Occurs after successful item validation
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemValidate(kEvent &$event)
		{

		}

		/**
		 * Occurs after an item has been copied to temp
		 * Id of copied item is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterCopyToTemp(kEvent &$event)
		{

		}

		/**
		 * Occurs before an item is deleted from live table when copying from temp
		 * (temp handler deleted all items from live and then copy over all items from temp)
		 * Id of item being deleted is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeDeleteFromLive(kEvent &$event)
		{

		}

		/**
		 * Occurs before an item is copied to live table (after all foreign keys have been updated)
		 * Id of item being copied is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeCopyToLive(kEvent &$event)
		{

		}

		/**
		 * Occurs after an item has been copied to live table
		 * Id of copied item is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterCopyToLive(kEvent &$event)
		{
			$this->_proccessPendingActions($event);
		}

		/**
		 * Processing file pending actions (e.g. delete scheduled files)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function _proccessPendingActions(kEvent &$event)
		{
			$var_name = $this->_getPendingActionVariableName($event);
			$schedule = $this->Application->RecallVar($var_name);

			if ( $schedule ) {
				$schedule = unserialize($schedule);

				foreach ($schedule as $data) {
					if ( $data['action'] == 'delete' ) {
						unlink($data['file']);
					}
				}

				$this->Application->RemoveVar($var_name);
			}
		}

		/**
		 * Returns variable name, used to store pending file actions
		 *
		 * @param kEvent $event
		 * @return string
		 * @access protected
		 */
		protected function _getPendingActionVariableName(kEvent &$event)
		{
			$window_id = $this->Application->GetTopmostWid($event->Prefix);

			return $event->Prefix . '_file_pending_actions' . $window_id;
		}

		/**
		 * Occurs before an item has been cloned
		 * Id of newly created item is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeClone(kEvent &$event)
		{

		}

		/**
		 * Occurs after an item has been cloned
		 * Id of newly created item is passed as event' 'id' param
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterClone(kEvent &$event)
		{

		}

		/**
		 * Occurs after list is queried
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterListQuery(kEvent &$event)
		{

		}

		/**
		 * Ensures that popup will be closed automatically
		 * and parent window will be refreshed with template
		 * passed
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @deprecated
		 */
		protected function finalizePopup(kEvent &$event)
		{
			$event->SetRedirectParam('opener', 'u');
		}

		/**
		 * Create search filters based on search query
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSearch(kEvent &$event)
		{
			$event->setPseudoClass('_List');

			$search_helper =& $this->Application->recallObject('SearchHelper');
			/* @var $search_helper kSearchHelper */

			$search_helper->performSearch($event);
		}

		/**
		 * Clear search keywords
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSearchReset(kEvent &$event)
		{
			$search_helper =& $this->Application->recallObject('SearchHelper');
			/* @var $search_helper kSearchHelper */

			$search_helper->resetSearch($event);
		}

		/**
		 * Set's new filter value (filter_id meaning from config)
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @deprecated
		 */
		protected function OnSetFilter(kEvent &$event)
		{
			$filter_id = $this->Application->GetVar('filter_id');
			$filter_value = $this->Application->GetVar('filter_value');

			$view_filter = $this->Application->RecallVar($event->getPrefixSpecial() . '_view_filter');
			$view_filter = $view_filter ? unserialize($view_filter) : Array ();

			$view_filter[$filter_id] = $filter_value;

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

		/**
		 * Sets view filter based on request
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSetFilterPattern(kEvent &$event)
		{
			$filters = $this->Application->GetVar($event->getPrefixSpecial(true) . '_filters');
			if ( !$filters ) {
				return;
			}

			$view_filter = $this->Application->RecallVar($event->getPrefixSpecial() . '_view_filter');
			$view_filter = $view_filter ? unserialize($view_filter) : Array ();

			$filters = explode(',', $filters);

			foreach ($filters as $a_filter) {
				list($id, $value) = explode('=', $a_filter);
				$view_filter[$id] = $value;
			}

			$this->Application->StoreVar($event->getPrefixSpecial() . '_view_filter', serialize($view_filter));
			$event->redirect = false;
		}

		/**
		 * Add/Remove all filters applied to list from "View" menu
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function FilterAction(kEvent &$event)
		{
			$view_filter = Array ();
			$filter_menu = $this->Application->getUnitOption($event->Prefix, 'FilterMenu');

			switch ($event->Name) {
				case 'OnRemoveFilters':
					$filter_value = 1;
					break;

				case 'OnApplyFilters':
					$filter_value = 0;
					break;

				default:
					$filter_value = 0;
					break;
			}

			foreach ($filter_menu['Filters'] as $filter_key => $filter_params) {
				if ( !$filter_params ) {
					continue;
				}

				$view_filter[$filter_key] = $filter_value;
			}

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

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 * @access protected
		 */
		protected function OnPreSaveAndOpenTranslator(kEvent &$event)
		{
			$this->Application->SetVar('allow_translation', true);

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

			$this->RemoveRequiredFields($object);
			$event->CallSubEvent('OnPreSave');

			if ( $event->status == kEvent::erSUCCESS ) {
				$resource_id = $this->Application->GetVar('translator_resource_id');

				if ( $resource_id ) {
					$t_prefixes = explode(',', $this->Application->GetVar('translator_prefixes'));

					$cdata =& $this->Application->recallObject($t_prefixes[1], null, Array ('skip_autoload' => true));
					/* @var $cdata kDBItem */

					$cdata->Load($resource_id, 'ResourceId');

					if ( !$cdata->isLoaded() ) {
						$cdata->SetDBField('ResourceId', $resource_id);
						$cdata->Create();
					}

					$this->Application->SetVar($cdata->getPrefixSpecial() . '_id', $cdata->GetID());
				}

				$event->redirect = $this->Application->GetVar('translator_t');

				$redirect_params = Array (
					'pass' => 'all,trans,' . $this->Application->GetVar('translator_prefixes'),
					'opener' => 's',
					$event->getPrefixSpecial(true) . '_id' => $object->GetID(),
					'trans_event'		=>	'OnLoad',
					'trans_prefix'		=>	$this->Application->GetVar('translator_prefixes'),
					'trans_field' 		=>	$this->Application->GetVar('translator_field'),
					'trans_multi_line'	=>	$this->Application->GetVar('translator_multi_line'),
				);

				$event->setRedirectParams($redirect_params);

				// 1. SAVE LAST TEMPLATE TO SESSION (really needed here, because of tweaky redirect)
				$last_template = $this->Application->RecallVar('last_template');
				preg_match('/index4\.php\|' . $this->Application->GetSID() . '-(.*):/U', $last_template, $rets);
				$this->Application->StoreVar('return_template', $this->Application->GetVar('t'));
			}
		}

		/**
		 * Makes all fields non-required
		 *
		 * @param kDBItem $object
		 * @return void
		 * @access protected
		 */
		protected function RemoveRequiredFields(&$object)
		{
			// making all field non-required to achieve successful presave
			$fields = array_keys( $object->getFields() );

			foreach ($fields as $field) {
				if ( $object->isRequired($field) ) {
					$object->setRequired($field, false);
				}
			}
		}

		/**
		 * Saves selected user in needed field
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSelectUser(kEvent &$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$items_info = $this->Application->GetVar('u');

			if ( $items_info ) {
				$user_id = array_shift(array_keys($items_info));

				$this->RemoveRequiredFields($object);

				$is_new = !$object->isLoaded();
				$is_main = substr($this->Application->GetVar($event->Prefix . '_mode'), 0, 1) == 't';

				if ( $is_new ) {
					$new_event = $is_main ? 'OnPreCreate' : 'OnNew';
					$event->CallSubEvent($new_event);
					$event->redirect = true;
				}

				$object->SetDBField($this->Application->RecallVar('dst_field'), $user_id);

				if ( $is_new ) {
					$object->Create();
				}
				else {
					$object->Update();
				}
			}

			$event->SetRedirectParam($event->getPrefixSpecial() . '_id', $object->GetID());
			$event->SetRedirectParam('opener', 'u');
		}


/** EXPORT RELATED **/

		/**
		 * Shows export dialog
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnExport(kEvent &$event)
		{
			$selected_ids = $this->StoreSelectedIDs($event);

			if ( implode(',', $selected_ids) == '' ) {
				// K4 fix when no ids found bad selected ids array is formed
				$selected_ids = false;
			}

			$this->Application->StoreVar($event->Prefix . '_export_ids', $selected_ids ? implode(',', $selected_ids) : '');

			$this->Application->LinkVar('export_finish_t');
			$this->Application->LinkVar('export_progress_t');
			$this->Application->StoreVar('export_oroginal_special', $event->Special);

			$export_helper =& $this->Application->recallObject('CatItemExportHelper');

			/*list ($index_file, $env) = explode('|', $this->Application->RecallVar('last_template'));
			$finish_url = $this->Application->BaseURL('/admin') . $index_file . '?' . ENV_VAR_NAME . '=' . $env;
			$this->Application->StoreVar('export_finish_url', $finish_url);*/

			$redirect_params = Array (
				$this->Prefix . '.export_event' => 'OnNew',
				'pass' => 'all,' . $this->Prefix . '.export'
			);

			$event->setRedirectParams($redirect_params);
		}

		/**
		 * Apply some special processing to object being
		 * recalled before using it in other events that
		 * call prepareObject
		 *
		 * @param kDBItem|kDBList $object
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function prepareObject(&$object, kEvent &$event)
		{
			if ( $event->Special == 'export' || $event->Special == 'import' ) {
				$export_helper =& $this->Application->recallObject('CatItemExportHelper');
				/* @var $export_helper kCatDBItemExportHelper */

				$export_helper->prepareExportColumns($event);
			}
		}

		/**
		 * Returns specific to each item type columns only
		 *
		 * @param kEvent $event
		 * @return Array
		 * @access public
		 */
		public function getCustomExportColumns(kEvent &$event)
		{
			return Array();
		}

		/**
		 * Export form validation & processing
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnExportBegin(kEvent &$event)
		{
			$export_helper =& $this->Application->recallObject('CatItemExportHelper');
			/* @var $export_helper kCatDBItemExportHelper */

			$export_helper->OnExportBegin($event);
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnExportCancel(kEvent &$event)
		{
			$this->OnGoBack($event);
		}

		/**
		 * Allows configuring export options
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeExportBegin(kEvent &$event)
		{

		}

		/**
		 * Deletes export preset
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnDeleteExportPreset(kEvent &$event)
		{
			$field_values = $this->getSubmittedFields($event);

			if ( !$field_values ) {
				return ;
			}

			$preset_key = $field_values['ExportPresets'];
			$export_settings = $this->Application->RecallPersistentVar('export_settings');

			if ( !$export_settings ) {
				return ;
			}

			$export_settings = unserialize($export_settings);

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

			$to_delete = '';

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

			if ( $to_delete ) {
				unset($export_settings[$event->Prefix][$to_delete]);
				$this->Application->StorePersistentVar('export_settings', serialize($export_settings));
			}
		}

		/**
		 * Saves changes & changes language
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreSaveAndChangeLanguage(kEvent &$event)
		{
			if ( $this->UseTempTables($event) ) {
				$event->CallSubEvent('OnPreSave');
			}

			if ( $event->status == kEvent::erSUCCESS ) {
				$this->Application->SetVar('m_lang', $this->Application->GetVar('language'));

				$data = $this->Application->GetVar('st_id');

				if ( $data ) {
					$event->SetRedirectParam('st_id', $data);
				}
			}
		}

		/**
		 * Used to save files uploaded via swfuploader
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnUploadFile(kEvent &$event)
		{
			$event->status = kEvent::erSTOP;
//			define('DBG_SKIP_REPORTING', 0);
			$default_msg = "Flash requires that we output something or it won't fire the uploadSuccess event";

			if ( !$this->Application->HttpQuery->Post ) {
				// Variables {field, id, flashsid} are always submitted through POST!
				// When file size is larger, then "upload_max_filesize" (in php.ini),
				// then these variables also are not submitted -> handle such case.
				header('HTTP/1.0 413 File size exceeds allowed limit');
				echo $default_msg;
				return;
			}

			if ( !$this->_checkFlashUploaderPermission($event) ) {
				// 403 Forbidden
				header('HTTP/1.0 403 You don\'t have permissions to upload');
				echo $default_msg;
				return;
			}

			$value = $this->Application->GetVar('Filedata');

			if ( !$value || ($value['error'] != UPLOAD_ERR_OK) ) {
				// 413 Request Entity Too Large (file uploads disabled OR uploaded file was
				// to large for web server to accept, see "upload_max_filesize" in php.ini)
				header('HTTP/1.0 413 File size exceeds allowed limit');
				echo $default_msg;
				return;
			}

			$tmp_path = WRITEABLE . '/tmp/';
			$fname = $value['name'];
			$id = $this->Application->GetVar('id');

			if ( $id ) {
				$fname = $id . '_' . $fname;
			}

			$field_name = $this->Application->GetVar('field');
			$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');
			$virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');
			$field_options = array_key_exists($field_name, $fields) ? $fields[$field_name] : $virtual_fields[$field_name];

			$upload_dir = $field_options['upload_dir'];
			$storage_format = array_key_exists('storage_format', $field_options) ? $field_options['storage_format'] : false;

			if ( !is_writable($tmp_path) ) {
				// 500 Internal Server Error
				// check both temp and live upload directory
				header('HTTP/1.0 500 Write permissions not set on the server');
				echo $default_msg;
				return;
			}

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

			$fname = $file_helper->ensureUniqueFilename($tmp_path, $fname);

			if ( $storage_format ) {
				$image_helper =& $this->Application->recallObject('ImageHelper');
				/* @var $image_helper ImageHelper */

				move_uploaded_file($value['tmp_name'], $value['tmp_name'] . '.jpg'); // add extension, so ResizeImage can work
				$url = $image_helper->ResizeImage($value['tmp_name'] . '.jpg', $storage_format);
				$tmp_name = preg_replace('/^' . preg_quote($this->Application->BaseURL(), '/') . '/', '/', $url);
				rename($tmp_name, $tmp_path . $fname);
			}
			else {
				move_uploaded_file($value['tmp_name'], $tmp_path . $fname);
			}

			echo preg_replace('/^' . preg_quote($id, '/') . '_/', '', $fname);

			$this->deleteTempFiles($tmp_path);
		}

		/**
		 * Delete temporary files, that won't be used for sure
		 *
		 * @param string $path
		 * @return void
		 * @access protected
		 */
		protected function deleteTempFiles($path)
		{
			$files = glob($path . '*.*');
			$max_file_date = strtotime('-1 day');

			foreach ($files as $file) {
				if (filemtime($file) < $max_file_date) {
					unlink($file);
				}
			}
		}

		/**
		 * Checks, that flash uploader is allowed to perform upload
		 *
		 * @param kEvent $event
		 * @return bool
		 */
		protected function _checkFlashUploaderPermission(kEvent &$event)
		{
			// Flash uploader does NOT send correct cookies, so we need to make our own check
			$cookie_name = 'adm_' . $this->Application->ConfigValue('SessionCookieName');
			$this->Application->HttpQuery->Cookie['cookies_on'] = 1;
			$this->Application->HttpQuery->Cookie[$cookie_name] = $this->Application->GetVar('flashsid');

			// this prevents session from auto-expiring when KeepSessionOnBrowserClose & FireFox is used
			$this->Application->HttpQuery->Cookie[$cookie_name . '_live'] = $this->Application->GetVar('flashsid');

			$admin_ses =& $this->Application->recallObject('Session.admin');
			/* @var $admin_ses Session */

			if ( $admin_ses->RecallVar('user_id') == USER_ROOT ) {
				return true;
			}

			// copy some data from given session to current session
			$backup_user_id = $this->Application->RecallVar('user_id');
			$this->Application->StoreVar('user_id', $admin_ses->RecallVar('user_id'));

			$backup_user_groups = $this->Application->RecallVar('UserGroups');
			$this->Application->StoreVar('UserGroups', $admin_ses->RecallVar('UserGroups'));

			// check permissions using event, that have "add|edit" rule
			$check_event = new kEvent($event->getPrefixSpecial() . ':OnProcessSelected');
			$check_event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true));
			$allowed_to_upload = $this->CheckPermission($check_event);

			// restore changed data, so nothing gets saved to database
			$this->Application->StoreVar('user_id', $backup_user_id);
			$this->Application->StoreVar('UserGroups', $backup_user_groups);

			return $allowed_to_upload;
		}

		/**
		 * Remembers, that file should be deleted on item's save from temp table
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnDeleteFile(kEvent &$event)
		{
			$event->status = kEvent::erSTOP;

			if ( strpos($this->Application->GetVar('file'), '../') !== false ) {
				return;
			}

			$object =& $event->getObject(Array ('skip_autoload' => true));
			$options = $object->GetFieldOptions($this->Application->GetVar('field'));

			$var_name = $this->_getPendingActionVariableName($event);
			$schedule = $this->Application->RecallVar($var_name);
			$schedule = $schedule ? unserialize($schedule) : Array ();
			$schedule[] = Array ('action' => 'delete', 'file' => FULL_PATH . $options['upload_dir'] . $this->Application->GetVar('file'));
			$this->Application->StoreVar($var_name, serialize($schedule));
		}

		/**
		 * Returns url for viewing uploaded file
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnViewFile(kEvent &$event)
		{
			$event->status = kEvent::erSTOP;

			$file = $this->Application->GetVar('file');
			if ( (strpos($file, '../') !== false) || (trim($file) !== $file) ) {
				// when relative paths or special chars are found template names from url, then it's hacking attempt
				return;
			}

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

			$field = $this->Application->GetVar('field');
			$options = $object->GetFieldOptions($field);

			// set current uploaded file
			if ( $this->Application->GetVar('tmp') ) {
				$options['upload_dir'] = WRITEBALE_BASE . '/tmp/';
				unset($options['include_path']);
				$object->SetFieldOptions($field, $options);

				$object->SetDBField($field, $this->Application->GetVar('id') . '_' . $file);
			}
			else {
				$object->SetDBField($field, $file);
			}

			// get url to uploaded file
			if ( $this->Application->GetVar('thumb') ) {
				$url = $object->GetField($field, $options['thumb_format']);
			}
			else {
				$url = $object->GetField($field, 'full_url'); // don't use "file_urls" format to prevent recursion
			}

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

			$path = $file_helper->urlToPath($url);

			if ( !file_exists($path) ) {
				exit;
			}

			header('Content-Length: ' . filesize($path));
			header('Content-Type: ' . kUtil::mimeContentType($path));
			header('Content-Disposition: inline; filename="' . $file . '"');

			readfile($path);
		}

		/**
		 * Validates MInput control fields
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnValidateMInputFields(kEvent &$event)
		{
			$minput_helper =& $this->Application->recallObject('MInputHelper');
			/* @var $minput_helper MInputHelper */

			$minput_helper->OnValidateMInputFields($event);
		}

		/**
		 * Validates individual object field and returns the result
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnValidateField(kEvent &$event)
		{
			$event->status = kEvent::erSTOP;
			$field = $this->Application->GetVar('field');

			if ( ($this->Application->GetVar('ajax') != 'yes') || !$field ) {
				return;
			}

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

			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

			if ( !$items_info ) {
				return;
			}

			list ($id, $field_values) = each($items_info);
			$object->Load($id);
			$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
			$object->setID($id);

			$response = Array ('status' => 'OK');

			$event->CallSubEvent($object->isLoaded() ? 'OnBeforeItemUpdate' : 'OnBeforeItemCreate');

			// validate all fields, since "Password_plain" field sets error to "Password" field, which is passed here
			$error_field = $object->GetFieldOption($field, 'error_field', false, $field);

			if ( !$object->Validate() && $object->GetErrorPseudo($error_field) ) {
				$response['status'] = $object->GetErrorMsg($error_field);
			}

			$ajax_form_helper =& $this->Application->recallObject('AjaxFormHelper');
			/* @var $ajax_form_helper AjaxFormHelper */

			$response['other_errors'] = $ajax_form_helper->getErrorMessages($object);

			$event->status = kEvent::erSTOP; // since event's OnBefore... events can change this event status
			echo json_encode($response);
		}

		/**
		 * Returns auto-complete values for ajax-dropdown
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSuggestValues(kEvent &$event)
		{
			if ( !$this->Application->isAdminUser ) {
				// very careful here, because this event allows to
				// view every object field -> limit only to logged-in admins
				return;
			}

			$event->status = kEvent::erSTOP;

			$field = $this->Application->GetVar('field');
			$cur_value = $this->Application->GetVar('cur_value');
			$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');

			$object =& $event->getObject();

			if ( !$field || !$cur_value || !$object->isField($field) ) {
				return;
			}

			$limit = $this->Application->GetVar('limit');
			if ( !$limit ) {
				$limit = 20;
			}

			$sql = 'SELECT DISTINCT ' . $field . '
					FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
					WHERE ' . $field . ' LIKE ' . $this->Conn->qstr($cur_value . '%') . '
					ORDER BY ' . $field . '
					LIMIT 0,' . $limit;
			$data = $this->Conn->GetCol($sql);

			$this->Application->XMLHeader();

			echo '<suggestions>';

			foreach ($data as $item) {
				echo '<item>' . htmlspecialchars($item) . '</item>';
			}

			echo '</suggestions>';
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSaveWidths(kEvent &$event)
		{
			$event->status = kEvent::erSTOP;

			/*$lang =& $this->Application->recallObject('lang.current');
			header('Content-type: text/xml; charset=' . $lang->GetDBField('Charset'));*/

			$picker_helper =& $this->Application->RecallObject('ColumnPickerHelper');
			/* @var $picker_helper kColumnPickerHelper */

			$picker_helper->PreparePicker($event->getPrefixSpecial(), $this->Application->GetVar('grid_name'));
			$picker_helper->SaveWidths($event->getPrefixSpecial(), $this->Application->GetVar('widths'));

			echo 'OK';
		}

		/**
		 * Called from CSV import script after item fields
		 * are set and validated, but before actual item create/update.
		 * If event status is kEvent::erSUCCESS, line will be imported,
		 * else it will not be imported but added to skipped lines
		 * and displayed in the end of import.
		 * Event status is preset from import script.
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeCSVLineImport(kEvent &$event)
		{
			// abstract, for hooking
		}

		/**
		 * [HOOK] Allows to add cloned subitem to given prefix
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnCloneSubItem(kEvent &$event)
		{
			$clones = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'Clones');

			$subitem_prefix = $event->Prefix . '-' . preg_replace('/^#/', '', $event->MasterEvent->Prefix);
			$clones[$subitem_prefix] = Array ('ParentPrefix' => $event->Prefix);
			$this->Application->setUnitOption($event->MasterEvent->Prefix, 'Clones', $clones);
		}

		/**
		 * Returns constrain for priority calculations
		 *
		 * @param kEvent $event
		 * @return void
		 * @see PriorityEventHandler
		 * @access protected
		 */
		protected function OnGetConstrainInfo(kEvent &$event)
		{
			$event->setEventParam('constrain_info', Array ('', ''));
		}
	}