<?php
/**
* @version	$Id: cat_event_handler.php 15065 2012-01-17 15:11:29Z 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!');

class kCatDBEventHandler extends kDBEventHandler {

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

		$permissions = Array(
			'OnSaveSettings' => Array ('self' => 'add|edit|advanced:import'),
			'OnResetSettings' => Array ('self' => 'add|edit|advanced:import'),
			'OnBeforeDeleteOriginal' => Array ('self' => 'edit|advanced:approve'),
			'OnAfterDeleteOriginal' => Array ('self' => 'edit|advanced:approve'),

			'OnCopy' => Array ('self' => true),
			'OnDownloadFile' => Array ('self' => 'view'),
			'OnCancelAction' => Array ('self' => true),
			'OnItemBuild' => Array ('self' => true),
			'OnMakeVote' => Array ('self' => true),
			'OnReviewHelpful' => Array ('self' => true),
		);

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

	/**
	 * 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->Load($id) ) {
			$actions =& $this->Application->recallObject('kActions');
			/* @var $actions Params */

			$actions->Set($event->getPrefixSpecial() . '_id', $object->GetID());

			$use_pending_editing = $this->Application->getUnitOption($event->Prefix, 'UsePendingEditing');

			if ( $use_pending_editing && $event->Special != 'original' ) {
				$this->Application->SetVar($event->Prefix . '.original_id', $object->GetDBField('OrgId'));
			}
		}
		else {
			$object->setID($id);
		}
	}

	/**
	 * Checks user permission to execute given $event
	 *
	 * @param kEvent $event
	 * @return bool
	 * @access public
	 */
	public function CheckPermission(kEvent &$event)
	{
		if ( !$this->Application->isAdmin ) {
			if ( $event->Name == 'OnSetSortingDirect' ) {
				// allow sorting on front event without view permission
				return true;
			}
		}

		if ( $event->Name == 'OnExport' ) {
			// save category_id before doing export
			$this->Application->LinkVar('m_cat_id');
		}

		if ( in_array($event->Name, $this->_getMassPermissionEvents()) ) {
			$items = $this->_getPermissionCheckInfo($event);

			$perm_helper =& $this->Application->recallObject('PermissionsHelper');
			/* @var $perm_helper kPermissionsHelper */

			if ( ($event->Name == 'OnSave') && array_key_exists(0, $items) ) {
				// adding new item (ID = 0)
				$perm_value = $perm_helper->AddCheckPermission($items[0]['CategoryId'], $event->Prefix) > 0;
			}
			else {
				// leave only items, that can be edited
				$ids = Array ();
				$check_method = in_array($event->Name, Array ('OnMassDelete', 'OnCut')) ? 'DeleteCheckPermission' : 'ModifyCheckPermission';
				foreach ($items as $item_id => $item_data) {
					if ( $perm_helper->$check_method($item_data['CreatedById'], $item_data['CategoryId'], $event->Prefix) > 0 ) {
						$ids[] = $item_id;
					}
				}

				if ( !$ids ) {
					// no items left for editing -> no permission
					return $perm_helper->finalizePermissionCheck($event, false);
				}

				$perm_value = true;
				$event->setEventParam('ids', $ids); // will be used later by "kDBEventHandler::StoreSelectedIDs" method
			}

			return $perm_helper->finalizePermissionCheck($event, $perm_value);
		}

		$export_events = Array ('OnSaveSettings', 'OnResetSettings', 'OnExportBegin');
		if ( in_array($event->Name, $export_events) ) {
			// when import settings before selecting target import category
			return $this->Application->CheckPermission('in-portal:main_import.view');
		}

		if ( $event->Name == 'OnProcessSelected' ) {
			if ( $this->Application->RecallVar('dst_field') == 'ImportCategory' ) {
				// when selecting target import category
				return $this->Application->CheckPermission('in-portal:main_import.view');
			}
		}

		return parent::CheckPermission($event);
	}

	/**
	 * Returns events, that require item-based (not just event-name based) permission check
	 *
	 * @return Array
	 */
	function _getMassPermissionEvents()
	{
		return Array (
			'OnEdit', 'OnSave', 'OnMassDelete', 'OnMassApprove',
			'OnMassDecline', 'OnMassMoveUp', 'OnMassMoveDown',
			'OnCut',
		);
	}

	/**
	 * Returns category item IDs, that require permission checking
	 *
	 * @param kEvent $event
	 * @return string
	 */
	function _getPermissionCheckIDs(&$event)
	{
		if ($event->Name == 'OnSave') {
			$selected_ids = implode(',', $this->getSelectedIDs($event, true));
			if (!$selected_ids) {
				$selected_ids = 0; // when saving newly created item (OnPreCreate -> OnPreSave -> OnSave)
			}
		}
		else {
			// OnEdit, OnMassDelete events, when items are checked in grid
			$selected_ids = implode(',', $this->StoreSelectedIDs($event));
		}

		return $selected_ids;
	}

	/**
	 * Returns information used in permission checking
	 *
	 * @param kEvent $event
	 * @return Array
	 */
	function _getPermissionCheckInfo(&$event)
	{
		$perm_helper =& $this->Application->recallObject('PermissionsHelper');
		/* @var $perm_helper kPermissionsHelper */

		// when saving data from temp table to live table check by data from temp table
		$item_ids = $this->_getPermissionCheckIDs($event);
		$items = $perm_helper->GetCategoryItemData($event->Prefix, $item_ids, $event->Name == 'OnSave');

		if (!$items) {
			// when item not present in temp table, then permission is not checked, because there are no data in db to check
			$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
			list ($id, $fields_hash) = each($items_info);

			if (array_key_exists('CategoryId', $fields_hash)) {
				$item_category = $fields_hash['CategoryId'];
			}
			else {
				$item_category = $this->Application->GetVar('m_cat_id');
			}

			$items[$id] = Array (
				'CreatedById' => $this->Application->RecallVar('use_id'),
				'CategoryId' => $item_category,
			);
		}

		return $items;
	}

	/**
	 * Add selected items to clipboard with mode = COPY (CLONE)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnCopy(&$event)
	{
		$this->Application->RemoveVar('clipboard');

		$clipboard_helper =& $this->Application->recallObject('ClipboardHelper');
		/* @var $clipboard_helper kClipboardHelper */

		$clipboard_helper->setClipboard($event, 'copy', $this->StoreSelectedIDs($event));
		$this->clearSelectedIDs($event);
	}

	/**
	 * Add selected items to clipboard with mode = CUT
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnCut(&$event)
	{
		$this->Application->RemoveVar('clipboard');
		$clipboard_helper =& $this->Application->recallObject('ClipboardHelper');
		/* @var $clipboard_helper kClipboardHelper */

		$clipboard_helper->setClipboard($event, 'cut', $this->StoreSelectedIDs($event));
		$this->clearSelectedIDs($event);
	}

	/**
	 * Checks permission for OnPaste event
	 *
	 * @param kEvent $event
	 * @return bool
	 */
	function _checkPastePermission(&$event)
	{
		$perm_helper =& $this->Application->recallObject('PermissionsHelper');
		/* @var $perm_helper kPermissionsHelper */

		$category_id = $this->Application->GetVar('m_cat_id');
		if ($perm_helper->AddCheckPermission($category_id, $event->Prefix) == 0) {
			// no items left for editing -> no permission
			return $perm_helper->finalizePermissionCheck($event, false);
		}

		return true;
	}

	/**
	 * Performs category item paste
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnPaste(&$event)
	{
		if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) || !$this->_checkPastePermission($event) ) {
			$event->status = kEvent::erFAIL;
			return;
		}

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

		if ( !$clipboard_data['cut'] && !$clipboard_data['copy'] ) {
			return;
		}

		if ( $clipboard_data['copy'] ) {
			$temp =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler');
			/* @var $temp kTempTablesHandler */

			$this->Application->SetVar('ResetCatBeforeClone', 1); // used in "kCatDBEventHandler::OnBeforeClone"
			$temp->CloneItems($event->Prefix, $event->Special, $clipboard_data['copy']);
		}

		if ( $clipboard_data['cut'] ) {
			$object =& $this->Application->recallObject($event->getPrefixSpecial() . '.item', $event->Prefix, Array ('skip_autoload' => true));
			/* @var $object kCatDBItem */

			foreach ($clipboard_data['cut'] as $id) {
				$object->Load($id);
				$object->MoveToCat();
			}
		}
	}

	/**
	 * 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;
		}

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

		$to_delete = Array ();
		$recycle_bin = $this->Application->ConfigValue('RecycleBinFolder');

		if ( $recycle_bin ) {
			$rb =& $this->Application->recallObject('c.recycle', null, array ('skip_autoload' => true));
			/* @var $rb CategoriesItem */

			$rb->Load($recycle_bin);

			$object =& $this->Application->recallObject($event->Prefix . '.recycleitem', null, Array ('skip_autoload' => true));
			/* @var $object kCatDBItem */

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

				if ( preg_match('/^' . preg_quote($rb->GetDBField('ParentPath'), '/') . '/', $object->GetDBField('ParentPath')) ) {
					$to_delete[] = $id;
					continue;
				}

				$object->MoveToCat($recycle_bin);
			}

			$ids = $to_delete;
		}

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

		$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);
	}


	/**
	 * Return type clauses for list bulding on front
	 *
	 * @param kEvent $event
	 * @return Array
	 */
	function getTypeClauses(&$event)
	{
		$types = $event->getEventParam('types');
		$types = $types ? explode(',', $types) : Array ();

		$except_types = $event->getEventParam('except');
		$except_types = $except_types ? explode(',', $except_types) : Array ();

		$type_clauses = Array();

		$user_id = $this->Application->RecallVar('user_id');
		$owner_field = $this->getOwnerField($event->Prefix);

		$type_clauses['my_items']['include'] = '%1$s.'.$owner_field.' = '.$user_id;
		$type_clauses['my_items']['except'] = '%1$s.'.$owner_field.' <> '.$user_id;
		$type_clauses['my_items']['having_filter'] = false;

		$type_clauses['pick']['include'] = '%1$s.EditorsPick = 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
		$type_clauses['pick']['except'] = '%1$s.EditorsPick! = 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
		$type_clauses['pick']['having_filter'] = false;

		$type_clauses['hot']['include'] = '`IsHot` = 1 AND PrimaryCat = 1';
		$type_clauses['hot']['except'] = '`IsHot`! = 1 AND PrimaryCat = 1';
		$type_clauses['hot']['having_filter'] = true;

		$type_clauses['pop']['include'] = '`IsPop` = 1 AND PrimaryCat = 1';
		$type_clauses['pop']['except'] = '`IsPop`! = 1 AND PrimaryCat = 1';
		$type_clauses['pop']['having_filter'] = true;

		$type_clauses['new']['include'] = '`IsNew` = 1 AND PrimaryCat = 1';
		$type_clauses['new']['except'] = '`IsNew`! = 1 AND PrimaryCat = 1';
		$type_clauses['new']['having_filter'] = true;

		$type_clauses['displayed']['include'] = '';
		$displayed = $this->Application->GetVar($event->Prefix.'_displayed_ids');
		if ($displayed) {
			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
			$type_clauses['displayed']['except'] = '%1$s.'.$id_field.' NOT IN ('.$displayed.')';
		}
		else {
			$type_clauses['displayed']['except'] = '';
		}
		$type_clauses['displayed']['having_filter'] = false;

		if (in_array('search', $types) || in_array('search', $except_types)) {
			$event_mapping = Array (
				'simple'		=>	'OnSimpleSearch',
				'subsearch'		=>	'OnSubSearch',
				'advanced'		=>	'OnAdvancedSearch'
			);

			$keywords = $event->getEventParam('keyword_string');
			$type = $this->Application->GetVar('search_type', 'simple');

			if ( $keywords ) {
				// processing keyword_string param of ListProducts tag
				$this->Application->SetVar('keywords', $keywords);
				$type = 'simple';
			}

			$search_event = $event_mapping[$type];
			$this->$search_event($event);

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

			$search_sql = '	FROM ' . TABLE_PREFIX . 'ses_' . $this->Application->GetSID() . '_' . TABLE_PREFIX . 'Search
							search_result LEFT JOIN %1$s ON %1$s.ResourceId = search_result.ResourceId';
			$sql = str_replace('FROM %1$s', $search_sql, $object->GetPlainSelectSQL());

			$object->SetSelectSQL($sql);

			$object->addCalculatedField('Relevance', 'search_result.Relevance');
			$object->AddOrderField('search_result.Relevance', 'desc', true);

			$type_clauses['search']['include'] = 'PrimaryCat = 1 AND ('.TABLE_PREFIX.'Categories.Status = '.STATUS_ACTIVE.')';
			$type_clauses['search']['except'] = 'PrimaryCat = 1 AND ('.TABLE_PREFIX.'Categories.Status = '.STATUS_ACTIVE.')';
			$type_clauses['search']['having_filter'] = false;
		}

		if (in_array('related', $types) || in_array('related', $except_types)) {

			$related_to = $event->getEventParam('related_to');
			if (!$related_to) {
				$related_prefix = $event->Prefix;
			}
			else {
				$sql = 'SELECT Prefix
						FROM '.TABLE_PREFIX.'ItemTypes
						WHERE ItemName = '.$this->Conn->qstr($related_to);
				$related_prefix = $this->Conn->GetOne($sql);
			}

			$rel_table = $this->Application->getUnitOption('rel', 'TableName');
			$item_type = (int)$this->Application->getUnitOption($event->Prefix, 'ItemType');

			if ($item_type == 0) {
				trigger_error('<strong>ItemType</strong> not defined for prefix <strong>' . $event->Prefix . '</strong>', E_USER_WARNING);
			}

			// process case, then this list is called inside another list
			$prefix_special = $event->getEventParam('PrefixSpecial');
			if (!$prefix_special) {
				$prefix_special = $this->Application->Parser->GetParam('PrefixSpecial');
			}

			$id = false;
			if ($prefix_special !== false) {
				$processed_prefix = $this->Application->processPrefix($prefix_special);
				if ($processed_prefix['prefix'] == $related_prefix) {
					// printing related categories within list of items (not on details page)
					$list =& $this->Application->recallObject($prefix_special);
					/* @var $list kDBList */

					$id = $list->GetID();
				}
			}

			if ($id === false) {
				// printing related categories for single item (possibly on details page)
				if ($related_prefix == 'c') {
					$id = $this->Application->GetVar('m_cat_id');
				}
				else {
					$id = $this->Application->GetVar($related_prefix . '_id');
				}
			}

			$p_item =& $this->Application->recallObject($related_prefix.'.current', null, Array('skip_autoload' => true));
			/* @var $p_item kCatDBItem */

			$p_item->Load( (int)$id );

			$p_resource_id = $p_item->GetDBField('ResourceId');

			$sql = 'SELECT SourceId, TargetId FROM '.$rel_table.'
					WHERE
						(Enabled = 1)
						AND (
								(Type = 0 AND SourceId = '.$p_resource_id.' AND TargetType = '.$item_type.')
								OR
								(Type = 1
									AND (
											(SourceId = '.$p_resource_id.' AND TargetType = '.$item_type.')
											OR
											(TargetId = '.$p_resource_id.' AND SourceType = '.$item_type.')
										)
								)
						)';

			$related_ids_array = $this->Conn->Query($sql);
			$related_ids = Array();

			foreach ($related_ids_array as $record) {
				$related_ids[] = $record[ $record['SourceId'] == $p_resource_id ? 'TargetId' : 'SourceId' ];
			}

			if (count($related_ids) > 0) {
				$type_clauses['related']['include'] = '%1$s.ResourceId IN ('.implode(',', $related_ids).') AND PrimaryCat = 1';
				$type_clauses['related']['except'] = '%1$s.ResourceId NOT IN ('.implode(',', $related_ids).') AND PrimaryCat = 1';
			}
			else {
				$type_clauses['related']['include'] = '0';
				$type_clauses['related']['except'] = '1';
			}
			$type_clauses['related']['having_filter'] = false;
		}

		if (in_array('favorites', $types) || in_array('favorites', $except_types)) {
			$sql = 'SELECT ResourceId
					FROM '.$this->Application->getUnitOption('fav', 'TableName').'
					WHERE PortalUserId = '.$this->Application->RecallVar('user_id');
			$favorite_ids = $this->Conn->GetCol($sql);
			if ($favorite_ids) {
				$type_clauses['favorites']['include'] = '%1$s.ResourceId IN ('.implode(',', $favorite_ids).') AND PrimaryCat = 1';
				$type_clauses['favorites']['except'] = '%1$s.ResourceId NOT IN ('.implode(',', $favorite_ids).') AND PrimaryCat = 1';
			}
			else {
				$type_clauses['favorites']['include'] = 0;
				$type_clauses['favorites']['except'] = 1;
			}
			$type_clauses['favorites']['having_filter'] = false;
		}

		return $type_clauses;
	}

	/**
	 * Returns SQL clause, that will help to select only data from specified category & it's children
	 *
	 * @param int $category_id
	 * @return string
	 */
	function getCategoryLimitClause($category_id)
	{
		if (!$category_id) {
			return false;
		}

		$tree_indexes = $this->Application->getTreeIndex($category_id);
		if (!$tree_indexes) {
			// id of non-existing category was given
			return 'FALSE';
		}

		return TABLE_PREFIX.'Categories.TreeLeft BETWEEN '.$tree_indexes['TreeLeft'].' AND '.$tree_indexes['TreeRight'];
	}

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

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

		// add category filter if needed
		if ($event->Special != 'showall' && $event->Special != 'user') {
			if ( (string)$event->getEventParam('parent_cat_id') !== '' ) {
				$parent_cat_id = $event->getEventParam('parent_cat_id');
			}
			else {
				$parent_cat_id = $this->Application->GetVar('c_id');
				if (!$parent_cat_id) {
					$parent_cat_id = $this->Application->GetVar('m_cat_id');
				}
				if (!$parent_cat_id) {
					$parent_cat_id = 0;
				}
			}

			if ("$parent_cat_id" == '0') {
				// replace "0" category with "Content" category id (this way template
				$parent_cat_id = $this->Application->getBaseCategory();
			}

			if ((string)$parent_cat_id != 'any') {
				if ($event->getEventParam('recursive')) {
					$filter_clause = $this->getCategoryLimitClause($parent_cat_id);
					if ($filter_clause !== false) {
						$object->addFilter('category_filter', $filter_clause);
					}

					$object->addFilter('primary_filter', 'PrimaryCat = 1');
				}
				else {
					$object->addFilter('category_filter', TABLE_PREFIX.'CategoryItems.CategoryId = '.$parent_cat_id );
				}
			}
			else {
				$object->addFilter('primary_filter', 'PrimaryCat = 1');
			}
		}
		else {
			$object->addFilter('primary_filter', 'PrimaryCat = 1');

			// if using recycle bin don't show items from there
			$recycle_bin = $this->Application->ConfigValue('RecycleBinFolder');
			if ($recycle_bin) {
				$object->addFilter('recyclebin_filter', TABLE_PREFIX.'CategoryItems.CategoryId <> '.$recycle_bin);
			}
		}

		if ($event->Special == 'user') {
			$editable_user = $this->Application->GetVar('u_id');
			$object->addFilter('owner_filter', '%1$s.'.$this->getOwnerField($event->Prefix).' = '.$editable_user);
		}

		// add permission filter
		if ($this->Application->RecallVar('user_id') == USER_ROOT) {
			// for "root" CATEGORY.VIEW permission is checked for items lists too
			$view_perm = 1;
		}
		else {
			// for any real user itemlist view permission is checked instead of CATEGORY.VIEW
			$count_helper =& $this->Application->recallObject('CountHelper');
			/* @var $count_helper kCountHelper */

			list ($view_perm, $view_filter) = $count_helper->GetPermissionClause($event->Prefix, 'perm');
			$object->addFilter('perm_filter2', $view_filter);
		}

		$object->addFilter('perm_filter', 'perm.PermId = '.$view_perm);


		$types = $event->getEventParam('types');
		$this->applyItemStatusFilter($object, $types);

		$except_types = $event->getEventParam('except');
		$type_clauses = $this->getTypeClauses($event);

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

		$search_helper->SetComplexFilter($event, $type_clauses, $types, $except_types);
	}

	/**
	 * Adds filter that filters out items with non-required statuses
	 *
	 * @param kDBList $object
	 * @param string $types
	 */
	function applyItemStatusFilter(&$object, $types)
	{
		// Link1 (before modifications) [Status = 1, OrgId = NULL], Link2 (after modifications) [Status = -2, OrgId = Link1_ID]
		$pending_editing = $this->Application->getUnitOption($object->Prefix, 'UsePendingEditing');

		if (!$this->Application->isAdminUser) {
			$types = explode(',', $types);
			if (in_array('my_items', $types)) {
				$allow_statuses = Array (STATUS_ACTIVE, STATUS_PENDING, STATUS_PENDING_EDITING);
				$object->addFilter('status_filter', '%1$s.Status IN ('.implode(',', $allow_statuses).')');

				if ($pending_editing) {
					$user_id = $this->Application->RecallVar('user_id');
					$this->applyPendingEditingFilter($object, $user_id);
				}
			}
			else {
				$object->addFilter('status_filter', '(%1$s.Status = ' . STATUS_ACTIVE . ') AND (' . TABLE_PREFIX . 'Categories.Status = ' . STATUS_ACTIVE . ')');
				if ($pending_editing) {
					// if category item uses pending editing abilities, then in no cases show pending copies on front
					$object->addFilter('original_filter', '%1$s.OrgId = 0 OR %1$s.OrgId IS NULL');
				}
			}
		}
		else {
			if ($pending_editing) {
				$this->applyPendingEditingFilter($object);
			}
		}
	}

	/**
	 * Adds filter, that removes live items if they have pending editing copies
	 *
	 * @param kDBList $object
	 * @param int $user_id
	 */
	function applyPendingEditingFilter(&$object, $user_id = null)
	{
		$sql = 'SELECT OrgId
				FROM '.$object->TableName.'
				WHERE Status = '.STATUS_PENDING_EDITING.' AND OrgId IS NOT NULL';

		if (isset($user_id)) {
			$owner_field = $this->getOwnerField($object->Prefix);
			$sql .= ' AND '.$owner_field.' = '.$user_id;
		}

		$pending_ids = $this->Conn->GetCol($sql);
		if ($pending_ids) {
			$object->addFilter('no_original_filter', '%1$s.'.$object->IDField.' NOT IN ('.implode(',', $pending_ids).')');
		}
	}

	/**
	 * Adds calculates fields for item statuses
	 *
	 * @param kDBItem|kDBList $object
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function prepareObject(&$object, kEvent &$event)
	{
		$this->prepareItemStatuses($event);
		$object->addCalculatedField('CachedNavbar', 'l' . $this->Application->GetVar('m_lang') . '_CachedNavbar');

		if ( $event->Special == 'export' || $event->Special == 'import' ) {
			$export_helper =& $this->Application->recallObject('CatItemExportHelper');
			/* @var $export_helper kCatDBItemExportHelper */

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

	/**
	 * Creates calculated fields for all item statuses based on config settings
	 *
	 * @param kEvent $event
	 */
	function prepareItemStatuses(&$event)
	{
		$object =& $event->getObject( Array('skip_autoload' => true) );

		$property_map = $this->Application->getUnitOption($event->Prefix, 'ItemPropertyMappings');
		if (!$property_map) {
			return ;
		}

		// new items
		$object->addCalculatedField('IsNew', '	IF(%1$s.NewItem = 2,
													IF(%1$s.CreatedOn >= (UNIX_TIMESTAMP() - '.
														$this->Application->ConfigValue($property_map['NewDays']).
														'*3600*24), 1, 0),
													%1$s.NewItem
												)');

		// hot items (cache updated every hour)
		if ($this->Application->isCachingType(CACHING_TYPE_MEMORY)) {
			$serial_name = $this->Application->incrementCacheSerial($event->Prefix, null, false);
			$hot_limit = $this->Application->getCache($property_map['HotLimit'] . '[%' . $serial_name . '%]');
		}
		else {
			$hot_limit = $this->Application->getDBCache($property_map['HotLimit']);
		}

		if ($hot_limit === false) {
			$hot_limit = $this->CalculateHotLimit($event);
		}

		$object->addCalculatedField('IsHot', '	IF(%1$s.HotItem = 2,
													IF(%1$s.'.$property_map['ClickField'].' >= '.$hot_limit.', 1, 0),
													%1$s.HotItem
												)');

		// popular items
		$object->addCalculatedField('IsPop', '	IF(%1$s.PopItem = 2,
													IF(%1$s.CachedVotesQty >= '.
														$this->Application->ConfigValue($property_map['MinPopVotes']).
														' AND %1$s.CachedRating >= '.
														$this->Application->ConfigValue($property_map['MinPopRating']).
														', 1, 0),
													%1$s.PopItem)');

	}

	/**
	 * Calculates hot limit for current item's table
	 *
	 * @param kEvent $event
	 * @return float
	 * @access protected
	 */
	protected function CalculateHotLimit(&$event)
	{
		$property_map = $this->Application->getUnitOption($event->Prefix, 'ItemPropertyMappings');

		if ( !$property_map ) {
			return 0.00;
		}

		$click_field = $property_map['ClickField'];

		$last_hot = $this->Application->ConfigValue($property_map['MaxHotNumber']) - 1;
		$sql = 'SELECT ' . $click_field . '
				FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
				ORDER BY ' . $click_field . ' DESC
				LIMIT ' . $last_hot . ', 1';
		$res = $this->Conn->GetCol($sql);
		$hot_limit = (double)array_shift($res);

		if ( $this->Application->isCachingType(CACHING_TYPE_MEMORY) ) {
			$serial_name = $this->Application->incrementCacheSerial($event->Prefix, null, false);
			$this->Application->setCache($property_map['HotLimit'] . '[%' . $serial_name . '%]', $hot_limit);
		}
		else {
			$this->Application->setDBCache($property_map['HotLimit'], $hot_limit, 3600);
		}

		return $hot_limit;
	}

	/**
	 * Moves item to preferred category, updates item hits
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeItemUpdate(kEvent &$event)
	{
		parent::OnBeforeItemUpdate($event);

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

		// update hits field
		$property_map = $this->Application->getUnitOption($event->Prefix, 'ItemPropertyMappings');
		if ( $property_map ) {
			$click_field = $property_map['ClickField'];

			if ( $this->Application->isAdminUser && ($this->Application->GetVar($click_field . '_original') !== false) && floor($this->Application->GetVar($click_field . '_original')) != $object->GetDBField($click_field) ) {
				$sql = 'SELECT MAX(' . $click_field . ')
						FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
						WHERE FLOOR(' . $click_field . ') = ' . $object->GetDBField($click_field);
				$hits = ($res = $this->Conn->GetOne($sql)) ? $res + 0.000001 : $object->GetDBField($click_field);

				$object->SetDBField($click_field, $hits);
			}
		}

		// change category
		$target_category = $object->GetDBField('CategoryId');
		if ( $object->GetOriginalField('CategoryId') != $target_category ) {
			$object->MoveToCat($target_category);
		}
	}

	/**
	 * 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)
	{
		parent::OnAfterItemLoad($event);

		$special = substr($event->Special, -6);

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

		if ( $special == 'import' || $special == 'export' ) {
			$image_data = $object->getPrimaryImageData();

			if ( $image_data ) {
				$thumbnail_image = $image_data[$image_data['LocalThumb'] ? 'ThumbPath' : 'ThumbUrl'];
				if ( $image_data['SameImages'] ) {
					$full_image = '';
				}
				else {
					$full_image = $image_data[$image_data['LocalImage'] ? 'LocalPath' : 'Url'];
				}

				$object->SetDBField('ThumbnailImage', $thumbnail_image);
				$object->SetDBField('FullImage', $full_image);
				$object->SetDBField('ImageAlt', $image_data['AltName']);
			}
		}

		// substituting pending status value for pending editing
		if ( $object->HasField('OrgId') && $object->GetDBField('OrgId') > 0 && $object->GetDBField('Status') == -2 ) {
			$new_options = Array ();
			$options = $object->GetFieldOption('Status', 'options', false, Array ());

			foreach ($options as $key => $val) {
				if ( $key == 2 ) {
					$key = -2;
				}

				$new_options[$key] = $val;
			}

			$object->SetFieldOption('Status', 'options', $new_options);
		}

		if ( !$this->Application->isAdmin ) {
			// linking existing images for item with virtual fields
			$image_helper =& $this->Application->recallObject('ImageHelper');
			/* @var $image_helper ImageHelper */

			$image_helper->LoadItemImages($object);

			// linking existing files for item with virtual fields
			$file_helper =& $this->Application->recallObject('FileHelper');
			/* @var $file_helper FileHelper */

			$file_helper->LoadItemFiles($object);
		}

		if ( $object->isVirtualField('MoreCategories') ) {
			// set item's additional categories to virtual field (used in editing)
			$item_categories = $this->getItemCategories($object->GetDBField('ResourceId'));
			$object->SetDBField('MoreCategories', $item_categories ? '|' . implode('|', $item_categories) . '|' : '');
		}
	}

	/**
	 * Occurs after updating item
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterItemUpdate(kEvent &$event)
	{
		parent::OnAfterItemUpdate($event);

		$this->CalculateHotLimit($event);

		if ( substr($event->Special, -6) == 'import' ) {
			$this->setCustomExportColumns($event);
		}

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

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

			// process image upload in virtual fields
			$image_helper->SaveItemImages($object);

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

			// process file upload in virtual fields
			$file_helper->SaveItemFiles($object);

			if ( $event->Special != '-item' ) {
				// don't touch categories during cloning
				$this->processAdditionalCategories($object, 'update');
			}
		}

		$recycle_bin = $this->Application->ConfigValue('RecycleBinFolder');

		if ( $this->Application->isAdminUser && $recycle_bin ) {
			$sql = 'SELECT CategoryId
					FROM ' . $this->Application->getUnitOption('ci', 'TableName') . '
					WHERE ItemResourceId = ' . $object->GetDBField('ResourceId') . ' AND PrimaryCat = 1';
			$primary_category = $this->Conn->GetOne($sql);

			if ( $primary_category == $recycle_bin ) {
				$event->CallSubEvent('OnAfterItemDelete');
			}
		}

		if ( $object->GetChangedFields() ) {
			$now = adodb_mktime();
			$object->SetDBField('Modified_date', $now);
			$object->SetDBField('Modified_time', $now);
			$object->SetDBField('ModifiedById', $this->Application->RecallVar('user_id'));
		}
	}

	/**
	 * Sets values for import process
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterItemCreate(kEvent &$event)
	{
		parent::OnAfterItemCreate($event);

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

		if ( substr($event->Special, -6) == 'import' ) {
			$this->setCustomExportColumns($event);
		}

		$object->assignPrimaryCategory();

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

			// process image upload in virtual fields
			$image_helper->SaveItemImages($object);

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

			// process file upload in virtual fields
			$file_helper->SaveItemFiles($object);

			if ( $event->Special != '-item' ) {
				// don't touch categories during cloning
				$this->processAdditionalCategories($object, 'create');
			}
		}
	}

	/**
	 * Make record to search log
	 *
	 * @param string $keywords
	 * @param int $search_type 0 - simple search, 1 - advanced search
	 */
	function saveToSearchLog($keywords, $search_type = 0)
	{
		// don't save keywords for each module separately, just one time
		// static variable can't help here, because each module uses it's own class instance !
		if (!$this->Application->GetVar('search_logged')) {
			$sql = 'UPDATE '.TABLE_PREFIX.'SearchLogs
					SET Indices = Indices + 1
					WHERE Keyword = '.$this->Conn->qstr($keywords).' AND SearchType = '.$search_type; // 0 - simple search, 1 - advanced search
	        $this->Conn->Query($sql);
	        if ($this->Conn->getAffectedRows() == 0) {
	            $fields_hash = Array('Keyword' => $keywords, 'Indices' => 1, 'SearchType' => $search_type);
	        	$this->Conn->doInsert($fields_hash, TABLE_PREFIX.'SearchLogs');
	        }

	        $this->Application->SetVar('search_logged', 1);
		}
	}

	/**
	 * Makes simple search for category items
	 * based on keywords string
	 *
	 * @param kEvent $event
	 */
	function OnSimpleSearch(&$event)
	{
		$event->redirect = false;
		$search_table = TABLE_PREFIX.'ses_'.$this->Application->GetSID().'_'.TABLE_PREFIX.'Search';

		$keywords = kUtil::unhtmlentities( trim($this->Application->GetVar('keywords')) );

		$query_object =& $this->Application->recallObject('HTTPQuery');
		/* @var $query_object kHTTPQuery */

		$sql = 'SHOW TABLES LIKE "'.$search_table.'"';

		if(!isset($query_object->Get['keywords']) &&
			!isset($query_object->Post['keywords']) &&
			$this->Conn->Query($sql))
		{
			return; // used when navigating by pages or changing sorting in search results
		}
		if(!$keywords || strlen($keywords) < $this->Application->ConfigValue('Search_MinKeyword_Length'))
		{
			$this->Conn->Query('DROP TABLE IF EXISTS '.$search_table);
			$this->Application->SetVar('keywords_too_short', 1);
			return; // if no or too short keyword entered, doing nothing
		}

		$this->Application->StoreVar('keywords', $keywords);

		$this->saveToSearchLog($keywords, 0); // 0 - simple search, 1 - advanced search

		$event->setPseudoClass('_List');

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

		$this->Application->SetVar($event->getPrefixSpecial().'_Page', 1);
		$lang = $this->Application->GetVar('m_lang');
		$items_table = $this->Application->getUnitOption($event->Prefix, 'TableName');
		$module_name = $this->Application->findModule('Var', $event->Prefix, 'Name');

		$sql = 'SELECT *
				FROM ' . $this->Application->getUnitOption('confs', 'TableName') . '
				WHERE ModuleName = ' . $this->Conn->qstr($module_name) . ' AND SimpleSearch = 1';
		$search_config = $this->Conn->Query($sql, 'FieldName');

		$field_list = array_keys($search_config);

		$join_clauses = Array();

		// field processing
		$weight_sum = 0;

		$alias_counter = 0;

		$custom_fields = $this->Application->getUnitOption($event->Prefix, 'CustomFields');
		if ($custom_fields) {
			$custom_table = $this->Application->getUnitOption($event->Prefix.'-cdata', 'TableName');
			$join_clauses[] = '	LEFT JOIN '.$custom_table.' custom_data ON '.$items_table.'.ResourceId = custom_data.ResourceId';
		}

		// what field in search config becomes what field in sql (key - new field, value - old field (from searchconfig table))
		$search_config_map = Array();

		foreach ($field_list as $key => $field) {
			$local_table = TABLE_PREFIX.$search_config[$field]['TableName'];
			$weight_sum += $search_config[$field]['Priority']; // counting weight sum; used when making relevance clause

			// processing multilingual fields
			if ( !$search_config[$field]['CustomFieldId'] && $object->GetFieldOption($field, 'formatter') == 'kMultiLanguage' ) {
				$field_list[$key.'_primary'] = 'l'.$this->Application->GetDefaultLanguageId().'_'.$field;
				$field_list[$key] = 'l'.$lang.'_'.$field;

				if (!isset($search_config[$field]['ForeignField'])) {
					$field_list[$key.'_primary'] = $local_table.'.'.$field_list[$key.'_primary'];
					$search_config_map[ $field_list[$key.'_primary'] ] = $field;
				}
			}

			// processing fields from other tables
			$foreign_field = $search_config[$field]['ForeignField'];

			if ( $foreign_field ) {
				$exploded = explode(':', $foreign_field, 2);
				if ($exploded[0] == 'CALC') {
					// ignoring having type clauses in simple search
					unset($field_list[$key]);
					continue;
				}
				else {
					$multi_lingual = false;
					if ($exploded[0] == 'MULTI') {
						$multi_lingual = true;
						$foreign_field = $exploded[1];
					}

					$exploded = explode('.', $foreign_field);	// format: table.field_name
					$foreign_table = TABLE_PREFIX.$exploded[0];

					$alias_counter++;
					$alias = 't'.$alias_counter;

					if ($multi_lingual) {
						$field_list[$key] = $alias.'.'.'l'.$lang.'_'.$exploded[1];
						$field_list[$key.'_primary'] = 'l'.$this->Application->GetDefaultLanguageId().'_'.$field;
						$search_config_map[ $field_list[$key] ] = $field;
						$search_config_map[ $field_list[$key.'_primary'] ] = $field;
					}
					else {
						$field_list[$key] = $alias.'.'.$exploded[1];
						$search_config_map[ $field_list[$key] ] = $field;
					}

					$join_clause = str_replace('{ForeignTable}', $alias, $search_config[$field]['JoinClause']);
					$join_clause = str_replace('{LocalTable}', $items_table, $join_clause);

					$join_clauses[] = '	LEFT JOIN '.$foreign_table.' '.$alias.'
										ON '.$join_clause;
				}
			}
			else {
				// processing fields from local table
				if ($search_config[$field]['CustomFieldId']) {
					$local_table = 'custom_data';

					// search by custom field value on current language
					$custom_field_id = array_search($field_list[$key], $custom_fields);
					$field_list[$key] = 'l'.$lang.'_cust_'.$custom_field_id;

					// search by custom field value on primary language
					$field_list[$key.'_primary'] = $local_table.'.l'.$this->Application->GetDefaultLanguageId().'_cust_'.$custom_field_id;
					$search_config_map[ $field_list[$key.'_primary'] ] = $field;
				}

				$field_list[$key] = $local_table.'.'.$field_list[$key];
				$search_config_map[ $field_list[$key] ] = $field;
			}
		}

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

		$where_clause = Array ();
		foreach ($field_list as $field) {
			if (preg_match('/^' . preg_quote($items_table, '/') . '\.(.*)/', $field, $regs)) {
				// local real field
				$filter_data = $search_helper->getSearchClause($object, $regs[1], $keywords, false);
				if ($filter_data) {
					$where_clause[] = $filter_data['value'];
				}
			}
			elseif (preg_match('/^custom_data\.(.*)/', $field, $regs)) {
				$custom_field_name = 'cust_' . $search_config_map[$field];
				$filter_data = $search_helper->getSearchClause($object, $custom_field_name, $keywords, false);
				if ($filter_data) {
					$where_clause[] = str_replace('`' . $custom_field_name . '`', $field, $filter_data['value']);
				}
			}
			else {
				$where_clause[] = $search_helper->buildWhereClause($keywords, Array ($field));
			}
		}

		$where_clause = '((' . implode(') OR (', $where_clause) . '))'; // 2 braces for next clauses, see below!

		$search_scope = $this->Application->GetVar('search_scope');
		if ($search_scope == 'category') {
			$category_id = $this->Application->GetVar('m_cat_id');
			$category_filter = $this->getCategoryLimitClause($category_id);
			if ($category_filter !== false) {
				$join_clauses[] = ' LEFT JOIN '.TABLE_PREFIX.'CategoryItems ON '.TABLE_PREFIX.'CategoryItems.ItemResourceId = '.$items_table.'.ResourceId';
				$join_clauses[] = ' LEFT JOIN '.TABLE_PREFIX.'Categories ON '.TABLE_PREFIX.'Categories.CategoryId = '.TABLE_PREFIX.'CategoryItems.CategoryId';

				$where_clause = '('.$this->getCategoryLimitClause($category_id).') AND '.$where_clause;
			}
		}

		$where_clause = $where_clause . ' AND (' . $items_table . '.Status = ' . STATUS_ACTIVE . ')';

		if ($event->MasterEvent && $event->MasterEvent->Name == 'OnListBuild') {
			if ($event->MasterEvent->getEventParam('ResultIds')) {
				$where_clause .= ' AND '.$items_table.'.ResourceId IN ('.implode(',', $event->MasterEvent->getEventParam('ResultIds')).')';
			}
		}

		// making relevance clause
		$positive_words = $search_helper->getPositiveKeywords($keywords);
		$this->Application->StoreVar('highlight_keywords', serialize($positive_words));
		$revelance_parts = Array();
		reset($search_config);

		foreach ($positive_words as $keyword_index => $positive_word) {
			$positive_word = $search_helper->transformWildcards($positive_word);
			$positive_words[$keyword_index] = $this->Conn->escape($positive_word);
		}

		foreach ($field_list as $field) {

			if (!array_key_exists($field, $search_config_map)) {
				$map_key = $search_config_map[$items_table . '.' . $field];
			}
			else {
				$map_key = $search_config_map[$field];
			}

			$config_elem = $search_config[ $map_key ];
			$weight = $config_elem['Priority'];

			// search by whole words only ([[:<:]] - word boundary)
			/*$revelance_parts[] = 'IF('.$field.' REGEXP "[[:<:]]('.implode(' ', $positive_words).')[[:>:]]", '.$weight.', 0)';
			foreach ($positive_words as $keyword) {
				$revelance_parts[] = 'IF('.$field.' REGEXP "[[:<:]]('.$keyword.')[[:>:]]", '.$weight.', 0)';
			}*/

			// search by partial word matches too
			$revelance_parts[] = 'IF('.$field.' LIKE "%'.implode(' ', $positive_words).'%", '.$weight_sum.', 0)';
			foreach ($positive_words as $keyword) {
				$revelance_parts[] = 'IF('.$field.' LIKE "%'.$keyword.'%", '.$weight.', 0)';
			}
		}

		$revelance_parts = array_unique($revelance_parts);

		$conf_postfix = $this->Application->getUnitOption($event->Prefix, 'SearchConfigPostfix');
		$rel_keywords	= $this->Application->ConfigValue('SearchRel_Keyword_'.$conf_postfix)	/ 100;
		$rel_pop		= $this->Application->ConfigValue('SearchRel_Pop_'.$conf_postfix)		/ 100;
		$rel_rating		= $this->Application->ConfigValue('SearchRel_Rating_'.$conf_postfix)	/ 100;
		$relevance_clause = '('.implode(' + ', $revelance_parts).') / '.$weight_sum.' * '.$rel_keywords;
		if ($rel_pop && $object->isField('Hits')) {
			$relevance_clause .= ' + (Hits + 1) / (MAX(Hits) + 1) * '.$rel_pop;
		}
		if ($rel_rating && $object->isField('CachedRating')) {
			$relevance_clause .= ' + (CachedRating + 1) / (MAX(CachedRating) + 1) * '.$rel_rating;
		}

		// building final search query
		if (!$this->Application->GetVar('do_not_drop_search_table')) {
			$this->Conn->Query('DROP TABLE IF EXISTS '.$search_table); // erase old search table if clean k4 event
			$this->Application->SetVar('do_not_drop_search_table', true);
		}


		$search_table_exists = $this->Conn->Query('SHOW TABLES LIKE "'.$search_table.'"');
		if ($search_table_exists) {
			$select_intro = 'INSERT INTO '.$search_table.' (Relevance, ItemId, ResourceId, ItemType, EdPick) ';
		}
		else {
			$select_intro = 'CREATE TABLE '.$search_table.' AS ';
		}

		$edpick_clause = $this->Application->getUnitOption($event->Prefix.'.EditorsPick', 'Fields') ? $items_table.'.EditorsPick' : '0';


		$sql = $select_intro.' SELECT '.$relevance_clause.' AS Relevance,
							'.$items_table.'.'.$this->Application->getUnitOption($event->Prefix, 'IDField').' AS ItemId,
							'.$items_table.'.ResourceId,
							'.$this->Application->getUnitOption($event->Prefix, 'ItemType').' AS ItemType,
							 '.$edpick_clause.' AS EdPick
					FROM '.$object->TableName.'
					'.implode(' ', $join_clauses).'
					WHERE '.$where_clause.'
					GROUP BY '.$items_table.'.'.$this->Application->getUnitOption($event->Prefix, 'IDField').' ORDER BY Relevance DESC';

		$this->Conn->Query($sql);

		if ( !$search_table_exists ) {
			$sql = 'ALTER TABLE ' . $search_table . '
					ADD INDEX (ResourceId),
					ADD INDEX (Relevance)';
			$this->Conn->Query($sql);
		}
	}

	/**
	 * Enter description here...
	 *
	 * @param kEvent $event
	 */
	function OnSubSearch(&$event)
	{
		$ids = Array ();
		$search_table = TABLE_PREFIX . 'ses_' . $this->Application->GetSID() . '_' . TABLE_PREFIX . 'Search';
		$sql = 'SHOW TABLES LIKE "' . $search_table . '"';

		if ( $this->Conn->Query($sql) ) {
			$sql = 'SELECT DISTINCT ResourceId
					FROM ' . $search_table;
			$ids = $this->Conn->GetCol($sql);
		}

		$event->setEventParam('ResultIds', $ids);
		$event->CallSubEvent('OnSimpleSearch');
	}

	/**
	 * Enter description here...
	 *
	 * @param kEvent $event
	 * @todo Change all hardcoded Products table & In-Commerce module usage to dynamic usage from item config !!!
	 */
	function OnAdvancedSearch(&$event)
	{
		$query_object =& $this->Application->recallObject('HTTPQuery');
		/* @var $query_object kHTTPQuery */

		if ( !isset($query_object->Post['andor']) ) {
			// used when navigating by pages or changing sorting in search results
			return;
		}

		$this->Application->RemoveVar('keywords');
		$this->Application->RemoveVar('Search_Keywords');

		$module_name = $this->Application->findModule('Var', $event->Prefix, 'Name');

		$sql = 'SELECT *
				FROM '.$this->Application->getUnitOption('confs', 'TableName').'
				WHERE (ModuleName = '.$this->Conn->qstr($module_name).') AND (AdvancedSearch = 1)';
		$search_config = $this->Conn->Query($sql);

		$lang = $this->Application->GetVar('m_lang');

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

		$object->SetPage(1);

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

		$search_keywords = $this->Application->GetVar('value'); // will not be changed

		$keywords = $this->Application->GetVar('value'); // will be changed down there
		$verbs = $this->Application->GetVar('verb');
		$glues = $this->Application->GetVar('andor');

		$and_conditions = Array();
		$or_conditions = Array();
		$and_having_conditions = Array();
		$or_having_conditions = Array();
		$join_clauses = Array();
		$highlight_keywords = Array();
		$relevance_parts = Array();

		$alias_counter = 0;

		$custom_fields = $this->Application->getUnitOption($event->Prefix, 'CustomFields');
		if ($custom_fields) {
			$custom_table = $this->Application->getUnitOption($event->Prefix.'-cdata', 'TableName');
			$join_clauses[] = '	LEFT JOIN '.$custom_table.' custom_data ON '.$items_table.'.ResourceId = custom_data.ResourceId';
		}

		$search_log = '';
		$weight_sum = 0;

		// processing fields and preparing conditions
		foreach ($search_config as $record) {
			$field = $record['FieldName'];
			$join_clause = '';
			$condition_mode = 'WHERE';

			// field processing
			$local_table = TABLE_PREFIX.$record['TableName'];
			$weight_sum += $record['Priority']; // counting weight sum; used when making relevance clause

			// processing multilingual fields
			if ( $object->GetFieldOption($field, 'formatter') == 'kMultiLanguage' ) {
				$field_name = 'l'.$lang.'_'.$field;
			}
			else {
				$field_name = $field;
			}

			// processing fields from other tables
			$foreign_field = $record['ForeignField'];

			if ( $foreign_field ) {
				$exploded = explode(':', $foreign_field, 2);
				if($exploded[0] == 'CALC')
				{
					$user_groups = $this->Application->RecallVar('UserGroups');
					$field_name = str_replace('{PREFIX}', TABLE_PREFIX, $exploded[1]);
					$join_clause = str_replace('{PREFIX}', TABLE_PREFIX, $record['JoinClause']);
					$join_clause = str_replace('{USER_GROUPS}', $user_groups, $join_clause);
					$join_clause = ' LEFT JOIN '.$join_clause;

					$condition_mode = 'HAVING';
				}
				else {
					$exploded = explode('.', $foreign_field);
					$foreign_table = TABLE_PREFIX.$exploded[0];

					if($record['CustomFieldId']) {
						$exploded[1] = 'l'.$lang.'_'.$exploded[1];
					}

					$alias_counter++;
					$alias = 't'.$alias_counter;

					$field_name = $alias.'.'.$exploded[1];
					$join_clause = str_replace('{ForeignTable}', $alias, $record['JoinClause']);
					$join_clause = str_replace('{LocalTable}', $items_table, $join_clause);

					if($record['CustomFieldId'])
					{
						$join_clause .= ' AND '.$alias.'.CustomFieldId='.$record['CustomFieldId'];
					}
					$join_clause = '	LEFT JOIN '.$foreign_table.' '.$alias.'
										ON '.$join_clause;
				}
			}
			else
			{
				// processing fields from local table
				if ($record['CustomFieldId']) {
					$local_table = 'custom_data';
					$field_name = 'l'.$lang.'_cust_'.array_search($field_name, $custom_fields);
				}

				$field_name = $local_table.'.'.$field_name;
			}

			$condition = $this->getAdvancedSearchCondition($field_name, $record, $keywords, $verbs, $highlight_keywords);
			if ($record['CustomFieldId'] && strlen($condition)) {
				// search in primary value of custom field + value in current language
				$field_name = $local_table.'.'.'l'.$this->Application->GetDefaultLanguageId().'_cust_'.array_search($field, $custom_fields);
				$primary_condition = $this->getAdvancedSearchCondition($field_name, $record, $keywords, $verbs, $highlight_keywords);
				$condition = '('.$condition.' OR '.$primary_condition.')';
			}

			if ($condition) {
				if ($join_clause) {
					$join_clauses[] = $join_clause;
				}

				$relevance_parts[] = 'IF('.$condition.', '.$record['Priority'].', 0)';
				if ($glues[$field] == 1) { // and
					if ($condition_mode == 'WHERE') {
						$and_conditions[] = $condition;
					}
					else {
						$and_having_conditions[] = $condition;
					}
				}
				else { // or
					if ($condition_mode == 'WHERE') {
						$or_conditions[] = $condition;
					}
					else {
						$or_having_conditions[] = $condition;
					}
				}

				// create search log record
				$search_log_data = Array('search_config' => $record, 'verb' => getArrayValue($verbs, $field), 'value' => ($record['FieldType'] == 'range') ? $search_keywords[$field.'_from'].'|'.$search_keywords[$field.'_to'] : $search_keywords[$field]);
				$search_log[] = $this->Application->Phrase('la_Field').' "'.$this->getHuman('Field', $search_log_data).'" '.$this->getHuman('Verb', $search_log_data).' '.$this->Application->Phrase('la_Value').' '.$this->getHuman('Value', $search_log_data).' '.$this->Application->Phrase($glues[$field] == 1 ? 'lu_And' : 'lu_Or');
			}
		}

		if ($search_log) {
			$search_log = implode('<br />', $search_log);
	    	$search_log = preg_replace('/(.*) '.preg_quote($this->Application->Phrase('lu_and'), '/').'|'.preg_quote($this->Application->Phrase('lu_or'), '/').'$/is', '\\1', $search_log);
	    	$this->saveToSearchLog($search_log, 1); // advanced search
		}

		$this->Application->StoreVar('highlight_keywords', serialize($highlight_keywords));

		// making relevance clause
		if($relevance_parts)
		{
			$conf_postfix = $this->Application->getUnitOption($event->Prefix, 'SearchConfigPostfix');
			$rel_keywords	= $this->Application->ConfigValue('SearchRel_Keyword_'.$conf_postfix)	/ 100;
			$rel_pop		= $this->Application->ConfigValue('SearchRel_Pop_'.$conf_postfix)		/ 100;
			$rel_rating		= $this->Application->ConfigValue('SearchRel_Rating_'.$conf_postfix)	/ 100;
			$relevance_clause = '('.implode(' + ', $relevance_parts).') / '.$weight_sum.' * '.$rel_keywords;
			$relevance_clause .= ' + (Hits + 1) / (MAX(Hits) + 1) * '.$rel_pop;
			$relevance_clause .= ' + (CachedRating + 1) / (MAX(CachedRating) + 1) * '.$rel_rating;
		}
		else
		{
			$relevance_clause = '0';
		}

		// building having clause
		if($or_having_conditions)
		{
			$and_having_conditions[] = '('.implode(' OR ', $or_having_conditions).')';
		}
		$having_clause = implode(' AND ', $and_having_conditions);
		$having_clause = $having_clause ? ' HAVING '.$having_clause : '';

		// building where clause
		if($or_conditions)
		{
			$and_conditions[] = '('.implode(' OR ', $or_conditions).')';
		}
//		$and_conditions[] = $items_table.'.Status = 1';
		$where_clause = implode(' AND ', $and_conditions);
		if(!$where_clause)
		{
			if($having_clause)
			{
				$where_clause = '1';
			}
			else
			{
				$where_clause = '0';
				$this->Application->SetVar('adv_search_error', 1);
			}
		}
		$where_clause .= ' AND '.$items_table.'.Status = 1';

		// building final search query
		$search_table = TABLE_PREFIX.'ses_'.$this->Application->GetSID().'_'.TABLE_PREFIX.'Search';

		$this->Conn->Query('DROP TABLE IF EXISTS '.$search_table);

		$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
		$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');
		$pick_field = isset($fields['EditorsPick']) ? $items_table.'.EditorsPick' : '0';

		$sql = '	CREATE TABLE '.$search_table.'
					SELECT 	'.$relevance_clause.' AS Relevance,
							'.$items_table.'.'.$id_field.' AS ItemId,
							'.$items_table.'.ResourceId AS ResourceId,
							11 AS ItemType,
							'.$pick_field.' AS EdPick
					FROM '.$items_table.'
					'.implode(' ', $join_clauses).'
					WHERE '.$where_clause.'
					GROUP BY '.$items_table.'.'.$id_field.
					$having_clause;

		$res = $this->Conn->Query($sql);
	}

	function getAdvancedSearchCondition($field_name, $record, $keywords, $verbs, &$highlight_keywords)
	{
		$field = $record['FieldName'];

		$condition_patterns = Array (
			'any'			=> '%s LIKE %s',
			'contains'		=> '%s LIKE %s',
			'notcontains'	=> '(NOT (%1$s LIKE %2$s) OR %1$s IS NULL)',
			'is'			=> '%s = %s',
			'isnot'			=> '(%1$s != %2$s OR %1$s IS NULL)'
		);

		$condition = '';
		switch ($record['FieldType']) {
			case 'select':
				$keywords[$field] = kUtil::unhtmlentities( $keywords[$field] );
				if ($keywords[$field]) {
					$condition = sprintf($condition_patterns['is'], $field_name, $this->Conn->qstr( $keywords[$field] ));
				}
				break;

			case 'multiselect':
				$keywords[$field] = kUtil::unhtmlentities( $keywords[$field] );
				if ($keywords[$field]) {
					$condition = Array ();
					$values = explode('|', substr($keywords[$field], 1, -1));
					foreach ($values as $selected_value) {
						$condition[] = sprintf($condition_patterns['contains'], $field_name, $this->Conn->qstr('%|'.$selected_value.'|%'));
					}
					$condition = '('.implode(' OR ', $condition).')';
				}
				break;

			case 'text':
				$keywords[$field] = kUtil::unhtmlentities( $keywords[$field] );

				if (mb_strlen($keywords[$field]) >= $this->Application->ConfigValue('Search_MinKeyword_Length')) {
					$highlight_keywords[] = $keywords[$field];
					if (in_array($verbs[$field], Array('any', 'contains', 'notcontains'))) {
						$keywords[$field] = '%'.strtr($keywords[$field], Array('%' => '\\%', '_' => '\\_')).'%';
					}
					$condition = sprintf($condition_patterns[$verbs[$field]], $field_name, $this->Conn->qstr( $keywords[$field] ));
				}
				break;

			case 'boolean':
				if ($keywords[$field] != -1) {
					$property_mappings = $this->Application->getUnitOption($this->Prefix, 'ItemPropertyMappings');
					$items_table = $this->Application->getUnitOption($this->Prefix, 'TableName');

					switch ($field) {
						case 'HotItem':
							$hot_limit_var = getArrayValue($property_mappings, 'HotLimit');
							if ($hot_limit_var) {
								$hot_limit = (int)$this->Application->getDBCache($hot_limit_var);

								$condition = 	'IF('.$items_table.'.HotItem = 2,
												IF('.$items_table.'.Hits >= '.
												$hot_limit.
												', 1, 0), '.$items_table.'.HotItem) = '.$keywords[$field];
							}
							break;

						case 'PopItem':
							$votes2pop_var = getArrayValue($property_mappings, 'VotesToPop');
							$rating2pop_var = getArrayValue($property_mappings, 'RatingToPop');
							if ($votes2pop_var && $rating2pop_var) {
								$condition = 'IF('.$items_table.'.PopItem = 2, IF('.$items_table.'.CachedVotesQty >= '.
												$this->Application->ConfigValue($votes2pop_var).
												' AND '.$items_table.'.CachedRating >= '.
												$this->Application->ConfigValue($rating2pop_var).
												', 1, 0), '.$items_table.'.PopItem) = '.$keywords[$field];
							}
							break;

						case 'NewItem':
							$new_days_var = getArrayValue($property_mappings, 'NewDays');
							if ($new_days_var) {
								$condition = 	'IF('.$items_table.'.NewItem = 2,
												IF('.$items_table.'.CreatedOn >= (UNIX_TIMESTAMP() - '.
												$this->Application->ConfigValue($new_days_var).
												'*3600*24), 1, 0), '.$items_table.'.NewItem) = '.$keywords[$field];
							}
							break;

						case 'EditorsPick':
							$condition = $items_table.'.EditorsPick = '.$keywords[$field];
							break;
					}
				}
				break;

			case 'range':
				$range_conditions = Array();
				if ($keywords[$field.'_from'] && !preg_match("/[^0-9]/i", $keywords[$field.'_from'])) {
					$range_conditions[] = $field_name.' >= '.$keywords[$field.'_from'];
				}
				if ($keywords[$field.'_to'] && !preg_match("/[^0-9]/i", $keywords[$field.'_to'])) {
					$range_conditions[] = $field_name.' <= '.$keywords[$field.'_to'];
				}
				if ($range_conditions) {
					$condition = implode(' AND ', $range_conditions);
				}
				break;

			case 'date':
				if ($keywords[$field]) {
					if (in_array($keywords[$field], Array('today', 'yesterday'))) {
						$current_time = getdate();
						$day_begin = adodb_mktime(0, 0, 0, $current_time['mon'], $current_time['mday'], $current_time['year']);
						$time_mapping = Array('today' => $day_begin, 'yesterday' => ($day_begin - 86400));
						$min_time = $time_mapping[$keywords[$field]];
					}
					else {
						$time_mapping = Array (
							'last_week' => 604800, 'last_month' => 2628000, 'last_3_months' => 7884000,
							'last_6_months' => 15768000, 'last_year' => 31536000,
						);
						$min_time = adodb_mktime() - $time_mapping[$keywords[$field]];
					}
					$condition = $field_name.' > '.$min_time;
				}
				break;
		}

		return $condition;
	}

	/**
	 * Returns human readable representation of searched data to be placed in search log
	 * @param string $type
	 * @param Array $search_data
	 * @return string
	 * @access protected
	 */
	protected function getHuman($type, $search_data)
	{
		// all 3 variables are retrieved from $search_data array
		/* @var $search_config Array */
		/* @var $verb string */
		/* @var $value string */

		$type = ucfirst(strtolower($type));
		extract($search_data);

		switch ($type) {
			case 'Field':
				return $this->Application->Phrase($search_config['DisplayName']);
				break;

			case 'Verb':
				return $verb ? $this->Application->Phrase('lu_advsearch_'.$verb) : '';
				break;

			case 'Value':
				switch ($search_config['FieldType']) {
					case 'date':
						$values = Array(0 => 'lu_comm_Any', 'today' => 'lu_comm_Today',
										'yesterday' => 'lu_comm_Yesterday', 'last_week' => 'lu_comm_LastWeek',
										'last_month' => 'lu_comm_LastMonth', 'last_3_months' => 'lu_comm_Last3Months',
										'last_6_months' => 'lu_comm_Last6Months', 'last_year' => 'lu_comm_LastYear');
						$ret = $this->Application->Phrase($values[$value]);
						break;

					case 'range':
						$value = explode('|', $value);
						return $this->Application->Phrase('lu_comm_From').' "'.$value[0].'" '.$this->Application->Phrase('lu_comm_To').' "'.$value[1].'"';
						break;

					case 'boolean':
						$values = Array(1 => 'lu_comm_Yes', 0 => 'lu_comm_No', -1 => 'lu_comm_Both');
						$ret = $this->Application->Phrase($values[$value]);
						break;

					default:
						$ret = $value;
						break;

				}
				return '"'.$ret.'"';
				break;
		}

		return '';
	}

	/**
	 * 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 ) {
				if ( $this->Application->RewriteURLs() ) {
					// when page not found by prefix+special, then try to search it without special at all
					$page = $this->Application->GetVar($event->Prefix . '_Page');

					if ( !$page ) {
						// page not found in request -> get from session
						$page = $this->Application->RecallVar($event->Prefix . '_Page');
					}

					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');
				}
			}
			else {
				// page found in request -> store in session
				$this->Application->StoreVar($event->getPrefixSpecial() . '_Page', $page, true); //true for optional
			}

			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);
	}

/* === RELATED TO IMPORT/EXPORT: BEGIN === */

	/**
	 * 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;
		}

		$selected_cats_ids = $this->Application->GetVar('export_categories');

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

		$export_helper =& $this->Application->recallObject('CatItemExportHelper');
		/* @var $export_helper kCatDBItemExportHelper */

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

		$event->setRedirectParams($redirect_params);
	}

	/**
	 * Performs each export step & displays progress percent
	 *
	 * @param kEvent $event
	 */
	function OnExportProgress(&$event)
	{
		$export_object =& $this->Application->recallObject('CatItemExportHelper');
		/* @var $export_object kCatDBItemExportHelper */

		$event = new kEvent($event->getPrefixSpecial().':OnDummy');

		$action_method = 'perform'.ucfirst($event->Special);
		$field_values = $export_object->$action_method($event);

		// finish code is done from JS now
		if ($field_values['start_from'] == $field_values['total_records']) {
			if ($event->Special == 'import') {
				$this->Application->StoreVar('PermCache_UpdateRequired', 1);

				$event->SetRedirectParam('m_cat_id', $this->Application->RecallVar('ImportCategory'));
				$event->SetRedirectParam('anchor', 'tab-' . $event->Prefix);

				$event->redirect = 'catalog/catalog';
			}
			elseif ($event->Special == 'export') {
				$event->redirect = $export_object->getModuleName($event) . '/' . $event->Special . '_finish';
				$event->SetRedirectParam('pass', 'all');
			}

			return ;
		}

		$export_options = $export_object->loadOptions($event);
		echo $export_options['start_from']  * 100 / $export_options['total_records'];

		$event->status = kEvent::erSTOP;
	}

	/**
	 * Returns specific to each item type columns only
	 *
	 * @param kEvent $event
	 * @return Array
	 * @access protected
	 */
	public function getCustomExportColumns(kEvent &$event)
	{
		return Array (
			'__VIRTUAL__ThumbnailImage'	=>	'ThumbnailImage',
			'__VIRTUAL__FullImage' 		=>	'FullImage',
			'__VIRTUAL__ImageAlt'		=>	'ImageAlt'
		);
	}

	/**
	 * Sets non standart virtual fields (e.g. to other tables)
	 *
	 * @param kEvent $event
	 */
	function setCustomExportColumns(&$event)
	{
		$this->restorePrimaryImage($event);
	}

	/**
	 * Create/Update primary image record in info found in imported data
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function restorePrimaryImage(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kCatDBItem */

		$has_image_info = $object->GetDBField('ImageAlt') && ($object->GetDBField('ThumbnailImage') || $object->GetDBField('FullImage'));
		if ( !$has_image_info ) {
			return ;
		}

		$image_data = $object->getPrimaryImageData();

		$image =& $this->Application->recallObject('img', null, Array ('skip_autoload' => true));
		/* @var $image kDBItem */

		if ( $image_data ) {
			$image->Load($image_data['ImageId']);
		}
		else {
			$image->Clear();
			$image->SetDBField('Name', 'main');
			$image->SetDBField('DefaultImg', 1);
			$image->SetDBField('ResourceId', $object->GetDBField('ResourceId'));
		}

		$image->SetDBField('AltName', $object->GetDBField('ImageAlt'));

		if ( $object->GetDBField('ThumbnailImage') ) {
			$thumbnail_field = $this->isURL($object->GetDBField('ThumbnailImage')) ? 'ThumbUrl' : 'ThumbPath';
			$image->SetDBField($thumbnail_field, $object->GetDBField('ThumbnailImage'));
			$image->SetDBField('LocalThumb', $thumbnail_field == 'ThumbPath' ? 1 : 0);
		}

		if ( !$object->GetDBField('FullImage') ) {
			$image->SetDBField('SameImages', 1);
		}
		else {
			$image->SetDBField('SameImages', 0);
			$full_field = $this->isURL($object->GetDBField('FullImage')) ? 'Url' : 'LocalPath';
			$image->SetDBField($full_field, $object->GetDBField('FullImage'));
			$image->SetDBField('LocalImage', $full_field == 'LocalPath' ? 1 : 0);
		}

		if ( $image->isLoaded() ) {
			$image->Update();
		}
		else {
			$image->Create();
		}
	}

	/**
	 * Detects if image url is specified in a given path (instead of path on disk)
	 *
	 * @param string $path
	 * @return bool
	 * @access protected
	 */
	protected function isURL($path)
	{
		return preg_match('#(http|https)://(.*)#', $path);
	}

	/**
	 * Prepares item for import/export operations
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnNew(kEvent &$event)
	{
		parent::OnNew($event);

		if ( $event->Special == 'import' || $event->Special == 'export' ) {
			$export_helper =& $this->Application->recallObject('CatItemExportHelper');
			/* @var $export_helper kCatDBItemExportHelper */

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

	/**
	 * Process items selected in item_selector
	 *
	 * @param kEvent $event
	 */
	function OnProcessSelected(&$event)
	{
		$dst_field = $this->Application->RecallVar('dst_field');
		$selected_ids = $this->Application->GetVar('selected_ids');

		if ( $dst_field == 'ItemCategory' ) {
			// Item Edit -> Categories Tab -> New Categories
			$object =& $event->getObject();
			/* @var $object kCatDBItem */

			$category_ids = explode(',', $selected_ids['c']);
			foreach ($category_ids as $category_id) {
				$object->assignToCategory($category_id);
			}
		}

		if ($dst_field == 'ImportCategory') {
			// Tools -> Import -> Item Import -> Select Import Category
			$this->Application->StoreVar('ImportCategory', $selected_ids['c']);

			$event->SetRedirectParam($event->getPrefixSpecial() . '_id', 0);
			$event->SetRedirectParam($event->getPrefixSpecial() . '_event', 'OnExportBegin');
		}

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

	/**
	 * Saves Import/Export settings to session
	 *
	 * @param kEvent $event
	 */
	function OnSaveSettings(&$event)
	{
		$event->redirect = false;
		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));

		if ( $items_info ) {
			list($id, $field_values) = each($items_info);

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

			$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
			$field_values['ImportFilename'] = $object->GetDBField('ImportFilename'); //if upload formatter has renamed the file during moving !!!
			$field_values['ImportSource'] = 2;
			$field_values['ImportLocalFilename'] = $object->GetDBField('ImportFilename');
			$items_info[$id] = $field_values;

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

	/**
	 * Saves Import/Export settings to session
	 *
	 * @param kEvent $event
	 */
	function OnResetSettings(&$event)
	{
		$this->Application->StoreVar('ImportCategory', $this->Application->getBaseCategory());
	}

	/**
	 * Cancels item editing
	 * @param kEvent $event
	 * @return void
	 * @todo Used?
	 */
	function OnCancelAction(&$event)
	{
		$event->redirect = $this->Application->GetVar('cancel_template');
		$event->SetRedirectParam('pass', 'all,' . $event->getPrefixSpecial());
	}

/* === RELATED TO IMPORT/EXPORT: END === */

	/**
	 * Stores item's owner login into separate field together with id
	 *
	 * @param kEvent $event
	 * @param string $id_field
	 * @param string $cached_field
	 */
	function cacheItemOwner(&$event, $id_field, $cached_field)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		$user_id = $object->GetDBField($id_field);
		$options = $object->GetFieldOptions($id_field);

		if ( isset($options['options'][$user_id]) ) {
			$object->SetDBField($cached_field, $options['options'][$user_id]);
		}
		else {
			$id_field = $this->Application->getUnitOption('u', 'IDField');
			$table_name = $this->Application->getUnitOption('u', 'TableName');

			$sql = 'SELECT Username
					FROM ' . $table_name . '
					WHERE ' . $id_field . ' = ' . $user_id;
			$object->SetDBField($cached_field, $this->Conn->GetOne($sql));
		}
	}

	/**
	 * 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)
	{
		parent::OnPreSave($event);

		$use_pending_editing = $this->Application->getUnitOption($event->Prefix, 'UsePendingEditing');

		if ( $event->status == kEvent::erSUCCESS && $use_pending_editing ) {
			// decision: clone or not clone

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

			if ( $object->GetID() == 0 || $object->GetDBField('OrgId') > 0 ) {
				// new items or cloned items shouldn't be cloned again
				return ;
			}

			$perm_helper =& $this->Application->recallObject('PermissionsHelper');
			/* @var $perm_helper kPermissionsHelper */

			$owner_field = $this->getOwnerField($event->Prefix);

			if ( $perm_helper->ModifyCheckPermission($object->GetDBField($owner_field), $object->GetDBField('CategoryId'), $event->Prefix) == 2 ) {

				// 1. clone original item
				$temp_handler =& $this->Application->recallObject($event->getPrefixSpecial() . '_TempHandler', 'kTempTablesHandler');
				/* @var $temp_handler kTempTablesHandler */

				$cloned_ids = $temp_handler->CloneItems($event->Prefix, $event->Special, Array ($object->GetID()), null, null, null, true);
				$ci_table = $this->Application->GetTempName(TABLE_PREFIX . 'CategoryItems');

				// 2. delete record from CategoryItems (about cloned item) that was automatically created during call of Create method of kCatDBItem
				$sql = 'SELECT ResourceId
						FROM ' . $object->TableName . '
						WHERE ' . $object->IDField . ' = ' . $cloned_ids[0];
				$clone_resource_id = $this->Conn->GetOne($sql);

				$sql = 'DELETE FROM ' . $ci_table . '
						WHERE ItemResourceId = ' . $clone_resource_id . ' AND PrimaryCat = 1';
				$this->Conn->Query($sql);

				// 3. copy main item categoryitems to cloned item
				$sql = '	INSERT INTO ' . $ci_table . ' (CategoryId, ItemResourceId, PrimaryCat, ItemPrefix, Filename)
							SELECT CategoryId, ' . $clone_resource_id . ' AS ItemResourceId, PrimaryCat, ItemPrefix, Filename
							FROM ' . $ci_table . '
							WHERE ItemResourceId = ' . $object->GetDBField('ResourceId');
				$this->Conn->Query($sql);

				// 4. put cloned id to OrgId field of item being cloned
				$sql = 'UPDATE ' . $object->TableName . '
						SET OrgId = ' . $object->GetID() . '
						WHERE ' . $object->IDField . ' = ' . $cloned_ids[0];
				$this->Conn->Query($sql);

				// 5. substitute id of item being cloned with clone id
				$this->Application->SetVar($event->getPrefixSpecial() . '_id', $cloned_ids[0]);

				$selected_ids = $this->getSelectedIDs($event, true);
				$selected_ids[ array_search($object->GetID(), $selected_ids) ] = $cloned_ids[0];
				$this->StoreSelectedIDs($event, $selected_ids);

				// 6. delete original item from temp table
				$temp_handler->DeleteItems($event->Prefix, $event->Special, Array ($object->GetID()));
			}
		}
	}

	/**
	 * Sets item's owner field
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnPreCreate(kEvent &$event)
	{
		parent::OnPreCreate($event);

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

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

		$owner_field = $this->getOwnerField($event->Prefix);
		$object->SetDBField($owner_field, $this->Application->RecallVar('user_id'));
	}

	/**
	 * Occurs before original item of item in pending editing got deleted (for hooking only)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeDeleteOriginal(kEvent &$event)
	{

	}

	/**
	 * Occurs after original item of item in pending editing got deleted (for hooking only)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterDeleteOriginal(kEvent &$event)
	{

	}

	/**
	 * 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)
	{
		parent::OnBeforeClone($event);

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

		$object->SetDBField('ResourceId', 0); // this will reset it

		if ( $this->Application->GetVar('ResetCatBeforeClone') ) {
			$object->SetDBField('CategoryId', null);
		}
	}

	/**
	 * Set status for new category item based on user permission in category
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnBeforeItemCreate(kEvent &$event)
	{
		parent::OnBeforeItemCreate($event);

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

		$is_admin = $this->Application->isAdminUser;
		$owner_field = $this->getOwnerField($event->Prefix);

		if ( (!$object->IsTempTable() && !$is_admin) || ($is_admin && !$object->GetDBField($owner_field)) ) {
			// Front-end OR owner not specified -> set to currently logged-in user
			$object->SetDBField($owner_field, $this->Application->RecallVar('user_id'));
		}

		if ( !$this->Application->isAdmin ) {
			$this->setItemStatusByPermission($event);
		}
	}

	/**
	 * Sets category item status based on user permissions (only on Front-end)
	 *
	 * @param kEvent $event
	 */
	function setItemStatusByPermission(&$event)
	{
		$use_pending_editing = $this->Application->getUnitOption($event->Prefix, 'UsePendingEditing');

		if (!$use_pending_editing) {
			return ;
		}

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

		$perm_helper =& $this->Application->recallObject('PermissionsHelper');
		/* @var $perm_helper kPermissionsHelper */

		$primary_category = $object->GetDBField('CategoryId') > 0 ? $object->GetDBField('CategoryId') : $this->Application->GetVar('m_cat_id');
		$item_status = $perm_helper->AddCheckPermission($primary_category, $event->Prefix);

		if ($item_status == STATUS_DISABLED) {
			$event->status = kEvent::erFAIL;
		}
		else {
			$object->SetDBField('Status', $item_status);
		}
	}

	/**
	 * Creates category item & redirects to confirmation template (front-end only)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnCreate(kEvent &$event)
	{
		parent::OnCreate($event);

		$this->SetFrontRedirectTemplate($event, 'suggest');
	}

	/**
	 * Returns item's categories (allows to exclude primary category)
	 *
	 * @param int $resource_id
	 * @param bool $with_primary
	 * @return Array
	 */
	function getItemCategories($resource_id, $with_primary = false)
	{
		$sql = 'SELECT CategoryId
				FROM '.TABLE_PREFIX.'CategoryItems
				WHERE (ItemResourceId = '.$resource_id.')';

		if (!$with_primary) {
			$sql .= ' AND (PrimaryCat = 0)';
		}

		return $this->Conn->GetCol($sql);
	}

	/**
	 * Adds new and removes old additional categories from category item
	 *
	 * @param kCatDBItem $object
	 * @param int $mode
	 */
	function processAdditionalCategories(&$object, $mode)
	{
		if ( !$object->isVirtualField('MoreCategories') ) {
			// given category item doesn't require such type of processing
			return ;
		}

		$process_categories = $object->GetDBField('MoreCategories');
		if ($process_categories === '') {
			// field was not in submit & have default value (when no categories submitted, then value is null)
			return ;
		}

		if ($mode == 'create') {
			// prevents first additional category to become primary
			$object->assignPrimaryCategory();
		}

		$process_categories = $process_categories ? explode('|', substr($process_categories, 1, -1)) : Array ();
		$existing_categories = $this->getItemCategories($object->GetDBField('ResourceId'));

		$add_categories = array_diff($process_categories, $existing_categories);
		foreach ($add_categories as $category_id) {
			$object->assignToCategory($category_id);
		}

		$remove_categories = array_diff($existing_categories, $process_categories);
		foreach ($remove_categories as $category_id) {
			$object->removeFromCategory($category_id);
		}
	}

	/**
	 * Creates category item & redirects to confirmation template (front-end only)
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnUpdate(kEvent &$event)
	{
		$use_pending = $this->Application->getUnitOption($event->Prefix, 'UsePendingEditing');
		if ($this->Application->isAdminUser || !$use_pending) {
			parent::OnUpdate($event);
			$this->SetFrontRedirectTemplate($event, 'modify');
			return ;
		}

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

		$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
		if ($items_info) {
			$perm_helper =& $this->Application->recallObject('PermissionsHelper');
			/* @var $perm_helper kPermissionsHelper */

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

			$owner_field = $this->getOwnerField($event->Prefix);

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

			foreach ($items_info as $id => $field_values) {
				$object->Load($id);
				$edit_perm = $perm_helper->ModifyCheckPermission($object->GetDBField($owner_field), $object->GetDBField('CategoryId'), $event->Prefix);

				if ($use_pending && !$object->GetDBField('OrgId') && ($edit_perm == STATUS_PENDING)) {
					// pending editing enabled + not pending copy -> get/create pending copy & save changes to it
					$original_id = $object->GetID();
					$original_resource_id = $object->GetDBField('ResourceId');
					$file_helper->PreserveItemFiles($field_values);

					$object->Load($original_id, 'OrgId');
					if (!$object->isLoaded()) {
						// 1. user has no pending copy of live item -> clone live item
						$cloned_ids = $temp_handler->CloneItems($event->Prefix, $event->Special, Array($original_id), null, null, null, true);

						$object->Load($cloned_ids[0]);
						$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));

						// 1a. delete record from CategoryItems (about cloned item) that was automatically created during call of Create method of kCatDBItem
						$ci_table = $this->Application->getUnitOption('ci', 'TableName');
						$sql = 'DELETE FROM '.$ci_table.'
								WHERE ItemResourceId = '.$object->GetDBField('ResourceId').' AND PrimaryCat = 1';
						$this->Conn->Query($sql);

						// 1b. copy main item categoryitems to cloned item
						$sql = 'INSERT INTO '.$ci_table.' (CategoryId, ItemResourceId, PrimaryCat, ItemPrefix, Filename)
								SELECT CategoryId, '.$object->GetDBField('ResourceId').' AS ItemResourceId, PrimaryCat, ItemPrefix, Filename
								FROM '.$ci_table.'
								WHERE ItemResourceId = '.$original_resource_id;
						$this->Conn->Query($sql);

						// 1c. put cloned id to OrgId field of item being cloned
						$object->SetDBField('Status', STATUS_PENDING_EDITING);
						$object->SetDBField('OrgId', $original_id);
					}
					else {
						// 2. user has pending copy of live item -> just update field values
						$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
					}

					// update id in request (used for redirect in mod-rewrite mode)
					$this->Application->SetVar($event->getPrefixSpecial().'_id', $object->GetID());
				}
				else {
					// 3. already editing pending copy -> just update field values
					$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
				}

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

		$this->SetFrontRedirectTemplate($event, 'modify');
	}

	/**
	 * Sets next template to one required for front-end after adding/modifying item
	 *
	 * @param kEvent $event
	 * @param string $template_key - {suggest,modify}
	 */
	function SetFrontRedirectTemplate(&$event, $template_key)
	{
		if ( $this->Application->isAdmin || $event->status != kEvent::erSUCCESS ) {
			return;
		}

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

		$is_active = ($object->GetDBField('Status') == STATUS_ACTIVE);

		$next_template = $is_active ? 'confirm_template' : 'pending_confirm_template';
		$event->redirect = $this->Application->GetVar($template_key . '_' . $next_template);
		$event->SetRedirectParam('opener', 's');

		// send email events
		$perm_prefix = $this->Application->getUnitOption($event->Prefix, 'PermItemPrefix');
		$owner_field = $this->getOwnerField($event->Prefix);
		$owner_id = $object->GetDBField($owner_field);

		switch ( $event->Name ) {
			case 'OnCreate':
				$event_suffix = $is_active ? 'ADD' : 'ADD.PENDING';
				$this->Application->EmailEventAdmin($perm_prefix . '.' . $event_suffix); // there are no ADD.PENDING event for admin :(
				$this->Application->EmailEventUser($perm_prefix . '.' . $event_suffix, $owner_id);
				break;

			case 'OnUpdate':
				$event_suffix = $is_active ? 'MODIFY' : 'MODIFY.PENDING';
				$user_id = is_numeric($object->GetDBField('ModifiedById')) ? $object->GetDBField('ModifiedById') : $owner_id;
				$this->Application->EmailEventAdmin($perm_prefix . '.' . $event_suffix); // there are no ADD.PENDING event for admin :(
				$this->Application->EmailEventUser($perm_prefix . '.' . $event_suffix, $user_id);
				break;
		}
	}

	/**
	 * Apply same processing to each item being selected in grid
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function iterateItems(kEvent &$event)
	{
		if ( $event->Name != 'OnMassApprove' && $event->Name != 'OnMassDecline' ) {
			parent::iterateItems($event);
		}

		if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
			$event->status = kEvent::erFAIL;
			return ;
		}

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

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

		if ( $ids ) {
			foreach ($ids as $id) {
				$ret = true;
				$object->Load($id);

				switch ( $event->Name ) {
					case 'OnMassApprove':
						$ret = $object->ApproveChanges();
						break;

					case 'OnMassDecline':
						$ret = $object->DeclineChanges();
						break;
				}

				if ( !$ret ) {
					$event->status = kEvent::erFAIL;
					$event->redirect = false;
					break;
				}
			}
		}

		$this->clearSelectedIDs($event);
	}

	/**
	 * Deletes items & preserves clean env
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnDelete(kEvent &$event)
	{
		parent::OnDelete($event);

		if ( $event->status == kEvent::erSUCCESS && !$this->Application->isAdmin ) {
			$event->SetRedirectParam('pass', 'm');
			$event->SetRedirectParam('m_cat_id', 0);
		}
	}

	/**
	 * Checks, that currently loaded item is allowed for viewing (non permission-based)
	 *
	 * @param kEvent $event
	 * @return bool
	 * @access protected
	 */
	protected function checkItemStatus(kEvent &$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		if ( !$object->isLoaded() ) {
			if ( $event->Special != 'previous' && $event->Special != 'next' ) {
				$this->_errorNotFound($event);
			}

			return true;
		}

		$status = $object->GetDBField('Status');
		$user_id = $this->Application->RecallVar('user_id');
		$owner_field = $this->getOwnerField($event->Prefix);

		if ( ($status == STATUS_PENDING_EDITING || $status == STATUS_PENDING) && ($object->GetDBField($owner_field) == $user_id) ) {
			return true;
		}

		return $status == STATUS_ACTIVE;
	}

	/**
	 * 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)
	{
		if ( !$this->Application->isAdmin ) {
			$event->setEventParam('same_special', true);
		}

		parent::SetSorting($event);
	}

	/**
	 * Returns current per-page setting for list
	 *
	 * @param kEvent $event
	 * @return int
	 * @access protected
	 */
	protected function getPerPage(kEvent &$event)
	{
		if ( !$this->Application->isAdmin ) {
			$event->setEventParam('same_special', true);
		}

		return parent::getPerPage($event);
	}

	/**
	 * Returns owner field for given prefix
	 *
	 * @param $prefix
	 * @return string
	 * @access protected
	 */
	protected function getOwnerField($prefix)
	{
		return $this->Application->getUnitOption($prefix, 'OwnerField', 'CreatedById');
	}

	/**
	 * Creates virtual image fields for item
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterConfigRead(kEvent &$event)
	{
		parent::OnAfterConfigRead($event);

		if (defined('IS_INSTALL') && IS_INSTALL) {
			return ;
		}

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

			$file_helper->createItemFiles($event->Prefix, true); // create image fields
			$file_helper->createItemFiles($event->Prefix, false); // create file fields
		}

		$this->changeSortings($event);

		// add grids for advanced view (with primary category column)
		$grids = $this->Application->getUnitOption($this->Prefix, 'Grids');
		$process_grids = Array ('Default', 'Radio');
		foreach ($process_grids as $process_grid) {
			$grid_data = $grids[$process_grid];
			$grid_data['Fields']['CachedNavbar'] = Array ('title' => 'la_col_Path', 'data_block' => 'grid_primary_category_td', 'filter_block' => 'grid_like_filter');
			$grids[$process_grid . 'ShowAll'] = $grid_data;
		}
		$this->Application->setUnitOption($this->Prefix, 'Grids', $grids);

		// add options for CategoryId field (quick way to select item's primary category)
		$category_helper =& $this->Application->recallObject('CategoryHelper');
		/* @var $category_helper CategoryHelper */

		$virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');

		$virtual_fields['CategoryId']['default'] = (int)$this->Application->GetVar('m_cat_id');
		$virtual_fields['CategoryId']['options'] = $category_helper->getStructureTreeAsOptions();

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

	function changeSortings(&$event)
	{
		$remove_sortings = Array ();

		if ( !$this->Application->isAdmin ) {
			// remove Pick sorting on Front-end, when not required
			$config_mapping = $this->Application->getUnitOption($event->Prefix, 'ConfigMapping', Array ());

			if ( !isset($config_mapping['ForceEditorPick']) || !$this->Application->ConfigValue($config_mapping['ForceEditorPick']) ) {
				$remove_sortings[] = 'EditorsPick';
			}
		}
		else {
			// remove all forced sortings in Admin Console
			$remove_sortings = array_merge($remove_sortings, Array ('Priority', 'EditorsPick'));
		}

		if ( !$remove_sortings ) {
			return;
		}

		$list_sortings = $this->Application->getUnitOption($event->Prefix, 'ListSortings', Array ());
		/* @var $list_sortings Array */

		foreach ($list_sortings as $special => $sorting_fields) {
			foreach ($remove_sortings as $sorting_field) {
				unset($list_sortings[$special]['ForcedSorting'][$sorting_field]);
			}
		}

		$this->Application->setUnitOption($event->Prefix, 'ListSortings', $list_sortings);
	}

	/**
	 * Returns file contents associated with item
	 *
	 * @param kEvent $event
	 */
	function OnDownloadFile(&$event)
	{
		$object =& $event->getObject();
		/* @var $object kCatDBItem */

		$event->status = kEvent::erSTOP;

		$field = $this->Application->GetVar('field');
		if (!preg_match('/^File([\d]+)/', $field)) {
			return ;
		}

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

		$filename = $object->GetField($field, 'full_path');
		$file_helper->DownloadFile($filename);
	}

	/**
	 * Saves user's vote
	 *
	 * @param kEvent $event
	 */
	function OnMakeVote(&$event)
	{
		$event->status = kEvent::erSTOP;

		if ($this->Application->GetVar('ajax') != 'yes') {
			// this is supposed to call from AJAX only
			return ;
		}

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

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

		$object->Load( $this->Application->GetVar('id') );

		echo $rating_helper->makeVote($object);
	}

	/**
	 * Marks review as useful
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnReviewHelpful(&$event)
	{
		if ( $this->Application->GetVar('ajax') == 'yes' ) {
			$event->status = kEvent::erSTOP;
		}

		$review_id = (int)$this->Application->GetVar('review_id');

		if ( !$review_id ) {
			return;
		}

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

		$spam_helper->InitHelper($review_id, 'ReviewHelpful', strtotime('+1 month') - strtotime('now'));
		$field = (int)$this->Application->GetVar('helpful') ? 'HelpfulCount' : 'NotHelpfulCount';

		$sql = 'SELECT ' . $field . '
				FROM ' . $this->Application->getUnitOption('rev', 'TableName') . '
				WHERE ' . $this->Application->getUnitOption('rev', 'IDField') . ' = ' . $review_id;
		$count = $this->Conn->GetOne($sql);

		if ( $spam_helper->InSpamControl() ) {
			if ( $this->Application->GetVar('ajax') == 'yes' ) {
				echo $count;
			}

			return;
		}

		$sql = 'UPDATE ' . $this->Application->getUnitOption('rev', 'TableName') . '
				SET ' . $field . ' = ' . $field . ' + 1
				WHERE ' . $this->Application->getUnitOption('rev', 'IDField') . ' = ' . $review_id;
		$this->Conn->Query($sql);

		if ( $this->Conn->getAffectedRows() ) {
			// db was changed -> review with such ID exists
			$spam_helper->AddToSpamControl();
		}

		if ( $this->Application->GetVar('ajax') == 'yes' ) {
			echo $count + 1;
		}
	}

	/**
	 * [HOOK] Allows to add cloned subitem to given prefix
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnCloneSubItem(kEvent &$event)
	{
		parent::OnCloneSubItem($event);

		if ( $event->MasterEvent->Prefix == 'fav' ) {
			$clones = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'Clones');
			$subitem_prefix = $event->Prefix . '-' . $event->MasterEvent->Prefix;

			$clones[$subitem_prefix]['ParentTableKey'] = 'ResourceId';
			$clones[$subitem_prefix]['ForeignKey'] = 'ResourceId';

			$this->Application->setUnitOption($event->MasterEvent->Prefix, 'Clones', $clones);
		}
	}

	/**
	 * Set's new unique resource id to user
	 *
	 * @param kEvent $event
	 * @return void
	 * @access protected
	 */
	protected function OnAfterItemValidate(kEvent &$event)
	{
		$object =& $event->getObject();
		/* @var $object kDBItem */

		$resource_id = $object->GetDBField('ResourceId');

		if ( !$resource_id ) {
			$object->SetDBField('ResourceId', $this->Application->NextResourceId());
		}
	}
}