<?php
/**
* @version	$Id: categories_event_handler.php 16621 2018-04-06 14:17:13Z 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 CategoriesEventHandler extends kDBEventHandler {

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

			$permissions = Array (
				'OnRebuildCache' => Array ('self' => 'add|edit'),
				'OnCopy' => Array ('self' => true),
				'OnCut' => Array ('self' => 'edit'),
				'OnPasteClipboard' => Array ('self' => true),
				'OnPaste' => Array ('self' => 'add|edit', 'subitem' => 'edit'),

				'OnRecalculatePriorities' => Array ('self' => 'add|edit'), // category ordering
				'OnItemBuild' => Array ('self' => true), // always allow to view individual categories (regardless of CATEGORY.VIEW right)
				'OnUpdatePreviewBlock' => Array ('self' => true), // for FCKEditor integration
			);

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

		/**
		 * Categories are sorted using special sorting event
		 *
		 */
		function mapEvents()
		{
			parent::mapEvents();

			$events_map = Array (
				'OnMassMoveUp' => 'OnChangePriority',
				'OnMassMoveDown' => 'OnChangePriority',
			);

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

		/**
		 * Checks user permission to execute given $event
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function CheckPermission(kEvent $event)
		{
			if ( $event->Name == 'OnResetCMSMenuCache' ) {
				// events from "Tools -> System Tools" section are controlled via that section "edit" permission

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

				$perm_value = $this->Application->CheckPermission('in-portal:service.edit');

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

			if ( !$this->Application->isAdmin ) {
				if ( $event->Name == 'OnSetSortingDirect' ) {
					// allow sorting on front event without view permission
					return true;
				}

				if ( $event->Name == 'OnItemBuild' ) {
					$category_id = $this->getPassedID($event);
					if ( $category_id == 0 ) {
						return true;
					}
				}
			}

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

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

				if ( ($event->Name == 'OnSave') && array_key_exists(0, $items) ) {
					// adding new item (ID = 0)
					$perm_value = $perm_helper->AddCheckPermission($items[0]['ParentId'], $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['ParentId'], $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);
			}

			if ( $event->Name == 'OnRecalculatePriorities' ) {
				/** @var kPermissionsHelper $perm_helper */
				$perm_helper = $this->Application->recallObject('PermissionsHelper');

				$category_id = $this->Application->GetVar('m_cat_id');

				return $perm_helper->AddCheckPermission($category_id, $event->Prefix) || $perm_helper->ModifyCheckPermission(0, $category_id, $event->Prefix);
			}

			if ( $event->Name == 'OnPasteClipboard' ) {
				// forces permission check to work by current category for "Paste In Category" operation
				$category_id = $this->Application->GetVar('m_cat_id');
				$this->Application->SetVar('c_id', $category_id);
			}

			return parent::CheckPermission($event);
		}

		/**
		 * Returns events, that require item-based (not just event-name based) permission check
		 *
		 * @return Array
		 */
		function _getMassPermissionEvents()
		{
			return array(
				'OnStoreSelected', '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)
		{
			// when saving data from temp table to live table check by data from temp table
			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
			$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');

			if ($event->Name == 'OnSave') {
				$table_name = $this->Application->GetTempName($table_name, 'prefix:' . $event->Prefix);
			}

			$sql = 'SELECT ' . $id_field . ', CreatedById, ParentId
					FROM ' . $table_name . '
					WHERE ' . $id_field . ' IN (' . $this->_getPermissionCheckIDs($event) . ')';
			$items = $this->Conn->Query($sql, $id_field);

			if (!$items) {
				// when creating new category, then no IDs are stored in session
				$items_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
				list ($id, $fields_hash) = each($items_info);

				if (array_key_exists('ParentId', $fields_hash)) {
					$item_category = $fields_hash['ParentId'];
				}
				else {
					$item_category = $this->Application->RecallVar('m_cat_id'); // saved in c:OnPreCreate event permission checking
				}

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

			return $items;
		}

		/**
		 * Creates "EDITING_MODE" constant.
		 *
		 * @param kEvent $event Event.
		 *
		 * @return void
		 */
		protected function OnAfterStartupHook(kEvent $event)
		{
			if ( !$this->Application->GetVar('admin') ) {
				// User can't edit anything.
				kUtil::safeDefine('EDITING_MODE', '');

				return;
			}

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

			// Store Admin Console User's ID to Front-End's session for cross-session permission checks.
			$this->Application->StoreVar('admin_user_id', (int)$admin_session->RecallVar('user_id'));

			$base_category = $this->Application->getBaseCategory();

			if ( $this->Application->CheckAdminPermission('CATEGORY.MODIFY', 0, $base_category) ) {
				// User can edit cms blocks (when viewing front-end through admin's frame).
				$editing_mode = $this->Application->GetVar('editing_mode');
				define('EDITING_MODE', $editing_mode ? $editing_mode : EDITING_MODE_BROWSE);
			}

			// User can't edit anything.
			kUtil::safeDefine('EDITING_MODE', '');
		}

		/**
		 * Set's mark, that root category is edited
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnEdit(kEvent $event)
		{
			$category_id = $this->Application->GetVar($event->getPrefixSpecial() . '_id');
			$home_category = $this->Application->getBaseCategory();

			$this->Application->StoreVar('IsRootCategory_' . $this->Application->GetVar('m_wid'), ($category_id === '0') || ($category_id == $home_category));

			parent::OnEdit($event);

			if ( $event->status == kEvent::erSUCCESS ) {
				// keep "Section Properties" link (in browse modes) clean
				$this->Application->DeleteVar('admin');
			}
		}

		/**
		 * Adds selected link to listing
		 *
		 * @param kEvent $event
		 */
		function OnProcessSelected($event)
		{
			/** @var kDBItem $object */
			$object = $event->getObject();

			$selected_ids = $this->Application->GetVar('selected_ids');

			$this->RemoveRequiredFields($object);
			$object->SetDBField($this->Application->RecallVar('dst_field'), $selected_ids['c']);
			$object->Update();

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

		/**
		 * Apply system filter to categories list
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetCustomQuery(kEvent $event)
		{
			parent::SetCustomQuery($event);

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

			// don't show "Content" category in advanced view
			$object->addFilter('system_categories', '%1$s.Status <> 4');

			// show system templates from current theme only + all virtual templates
			$object->addFilter('theme_filter', '%1$s.ThemeId = ' . $this->_getCurrentThemeId() . ' OR %1$s.ThemeId = 0');

			if ($event->Special == 'showall') {
				// if using recycle bin don't show categories from there
				$recycle_bin = $this->Application->ConfigValue('RecycleBinFolder');
				if ($recycle_bin) {
					$sql = 'SELECT TreeLeft, TreeRight
							FROM '.TABLE_PREFIX.'Categories
							WHERE CategoryId = '.$recycle_bin;
					$tree_indexes = $this->Conn->GetRow($sql);

					$object->addFilter('recyclebin_filter', '%1$s.TreeLeft < '.$tree_indexes['TreeLeft'].' OR %1$s.TreeLeft > '.$tree_indexes['TreeRight']);
				}
			}

			if ( (string)$event->getEventParam('parent_cat_id') !== '' ) {
				$parent_cat_id = $event->getEventParam('parent_cat_id');

				if ("$parent_cat_id" == 'Root') {
					$module_name = $event->getEventParam('module') ? $event->getEventParam('module') : 'In-Commerce';
					$parent_cat_id = $this->Application->findModule('Name', $module_name, 'RootCat');
				}
			}
			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 ("$parent_cat_id" != 'any') {
				if ($event->getEventParam('recursive')) {
					if ($parent_cat_id > 0) {
						// not "Home" category
						$tree_indexes = $this->Application->getTreeIndex($parent_cat_id);

						$object->addFilter('parent_filter', '%1$s.TreeLeft BETWEEN '.$tree_indexes['TreeLeft'].' AND '.$tree_indexes['TreeRight']);
					}
				}
				else {
					$object->addFilter('parent_filter', '%1$s.ParentId = '.$parent_cat_id);
				}
			}

			$this->applyViewPermissionFilter($object);

			if (!$this->Application->isAdminUser)	{
				// apply status filter only on front
				$object->addFilter('status_filter', $object->TableName.'.Status = 1');
			}

			// process "types" and "except" parameters
			$type_clauses = Array();

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

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

			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)
						/** @var kDBList $list */
						$list = $this->Application->recallObject($prefix_special);

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

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

				$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 $key => $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).')';
					$type_clauses['related']['except'] = '%1$s.ResourceId NOT IN ('.implode(',', $related_ids).')';
				}
				else {
					$type_clauses['related']['include'] = '0';
					$type_clauses['related']['except'] = '1';
				}

				$type_clauses['related']['having_filter'] = false;
			}

			if (in_array('category_related', $type_clauses)) {
				$object->removeFilter('parent_filter');
				$resource_id = $this->Conn->GetOne('
								SELECT ResourceId FROM '.$this->Application->getUnitOption($event->Prefix, 'TableName').'
								WHERE CategoryId = '.$parent_cat_id
							);

				$sql = 'SELECT DISTINCT(TargetId) FROM '.TABLE_PREFIX.'CatalogRelationships
						WHERE SourceId = '.$resource_id.' AND SourceType = 1';
				$related_cats = $this->Conn->GetCol($sql);
				$related_cats = is_array($related_cats) ? $related_cats : Array();

				$sql = 'SELECT DISTINCT(SourceId) FROM '.TABLE_PREFIX.'CatalogRelationships
						WHERE TargetId = '.$resource_id.' AND TargetType = 1 AND Type = 1';
				$related_cats2 = $this->Conn->GetCol($sql);
				$related_cats2 = is_array($related_cats2) ? $related_cats2 : Array();
				$related_cats = array_unique( array_merge( $related_cats2, $related_cats ) );

				if ($related_cats) {
					$type_clauses['category_related']['include'] = '%1$s.ResourceId IN ('.implode(',', $related_cats).')';
					$type_clauses['category_related']['except'] = '%1$s.ResourceId NOT IN ('.implode(',', $related_cats).')';
				}
				else
				{
					$type_clauses['category_related']['include'] = '0';
					$type_clauses['category_related']['except'] = '1';
				}
				$type_clauses['category_related']['having_filter'] = false;
			}

			if (in_array('product_related', $types)) {
				$object->removeFilter('parent_filter');

				$product_id = $event->getEventParam('product_id') ? $event->getEventParam('product_id') : $this->Application->GetVar('p_id');
				$resource_id = $this->Conn->GetOne('
								SELECT ResourceId FROM '.$this->Application->getUnitOption('p', 'TableName').'
								WHERE ProductId = '.$product_id
							);

				$sql = 'SELECT DISTINCT(TargetId) FROM '.TABLE_PREFIX.'CatalogRelationships
						WHERE SourceId = '.$resource_id.' AND TargetType = 1';
				$related_cats = $this->Conn->GetCol($sql);
				$related_cats = is_array($related_cats) ? $related_cats : Array();
				$sql = 'SELECT DISTINCT(SourceId) FROM '.TABLE_PREFIX.'CatalogRelationships
						WHERE TargetId = '.$resource_id.' AND SourceType = 1 AND Type = 1';
				$related_cats2 = $this->Conn->GetCol($sql);
				$related_cats2 = is_array($related_cats2) ? $related_cats2 : Array();
				$related_cats = array_unique( array_merge( $related_cats2, $related_cats ) );

				if ($related_cats) {
					$type_clauses['product_related']['include'] = '%1$s.ResourceId IN ('.implode(',', $related_cats).')';
					$type_clauses['product_related']['except'] = '%1$s.ResourceId NOT IN ('.implode(',', $related_cats).')';
				}
				else {
					$type_clauses['product_related']['include'] = '0';
					$type_clauses['product_related']['except'] = '1';
				}

				$type_clauses['product_related']['having_filter'] = false;
			}

			$type_clauses['menu']['include'] = '%1$s.IsMenu = 1';
			$type_clauses['menu']['except'] = '%1$s.IsMenu = 0';
			$type_clauses['menu']['having_filter'] = false;

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

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

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

				$search_sql = '	FROM ' . $search_helper->getSearchTable() . ' search_result
								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');

				$type_clauses['search']['include'] = '1';
				$type_clauses['search']['except'] = '0';
				$type_clauses['search']['having_filter'] = false;
			}

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

		/**
		 * Adds filter, that uses *.VIEW permissions to determine if an item should be shown to a user.
		 *
		 * @param kDBList $object Object.
		 *
		 * @return void
		 * @access protected
		 */
		protected function applyViewPermissionFilter(kDBList $object)
		{
			if ( !$this->Application->ConfigValue('CheckViewPermissionsInCatalog') ) {
				return;
			}

			if ( $this->Application->RecallVar('user_id') == USER_ROOT ) {
				// for "root" CATEGORY.VIEW permission is checked for items lists too
				$view_perm = 1;
			}
			else {
				/** @var kCountHelper $count_helper */
				$count_helper = $this->Application->recallObject('CountHelper');

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

			$object->addFilter('perm_filter', 'perm.PermId = ' . $view_perm); // check for CATEGORY.VIEW permission
		}

		/**
		 * Returns current theme id
		 *
		 * @return int
		 */
		function _getCurrentThemeId()
		{
			/** @var kThemesHelper $themes_helper */
			$themes_helper = $this->Application->recallObject('ThemesHelper');

			return (int)$themes_helper->getCurrentThemeId();
		}

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

			if ( $this->Application->isAdmin ) {
				return parent::getPassedID($event);
			}

			$event->setEventParam(kEvent::FLAG_ID_FROM_REQUEST, true);

			return $this->Application->GetVar('m_cat_id');
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 * @return int
		 */
		function _getPassedStructureID($event)
		{
			static $page_by_template = Array ();

			if ( $event->Special == 'current' ) {
				$event->setEventParam(kEvent::FLAG_ID_FROM_REQUEST, true);

				return $this->Application->GetVar('m_cat_id');
			}

			$event->setEventParam('raise_warnings', 0);

			$page_id = parent::getPassedID($event);

			if ( $page_id === false ) {
				$template = $event->getEventParam('page');
				if ( !$template ) {
					$template = $this->Application->GetVar('t');
				}

				// bug: when template contains "-" symbols (or others, that stripDisallowed will replace) it's not found
				if ( !array_key_exists($template, $page_by_template) ) {
					$template_crc = kUtil::crc32(mb_strtolower($template));

					$sql = 'SELECT ' . $this->Application->getUnitOption($event->Prefix, 'IDField') . '
							FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
							WHERE
								(
									(NamedParentPathHash = ' . $template_crc . ') OR
									(`Type` = ' . PAGE_TYPE_TEMPLATE . ' AND CachedTemplateHash = ' . $template_crc . ')
								) AND (ThemeId = ' . $this->_getCurrentThemeId() . ' OR ThemeId = 0)';

					$page_id = $this->Conn->GetOne($sql);
				}
				else {
					$page_id = $page_by_template[$template];
				}

				if ( $page_id ) {
					$page_by_template[$template] = $page_id;
				}
			}

			if ( !$page_id && !$this->Application->isAdmin ) {
				$page_id = $this->Application->GetVar('m_cat_id');
				$event->setEventParam(kEvent::FLAG_ID_FROM_REQUEST, true);
			}

			return $page_id;
		}

		function ParentGetPassedID($event)
		{
			return parent::getPassedID($event);
		}

		/**
		 * Adds calculates fields for item statuses
		 *
		 * @param kCatDBItem $object
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function prepareObject(&$object, kEvent $event)
		{
			if ( $this->_isVirtual($event) ) {
				return;
			}

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

			$object->addCalculatedField(
				'IsNew',
				'	IF(%1$s.NewItem = 2,
						IF(%1$s.CreatedOn >= (UNIX_TIMESTAMP() - '.
							$this->Application->ConfigValue('Category_DaysNew').
							'*3600*24), 1, 0),
						%1$s.NewItem
				)');
		}

		/**
		 * Checks, that this is virtual page
		 *
		 * @param kEvent $event
		 * @return int
		 * @access protected
		 */
		protected function _isVirtual(kEvent $event)
		{
			return strpos($event->Special, '-virtual') !== false;
		}

		/**
		 * Gets right special for configuring virtual page
		 *
		 * @param kEvent $event
		 * @return string
		 * @access protected
		 */
		protected function _getCategorySpecial(kEvent $event)
		{
			return $this->_isVirtual($event) ? '-virtual' : $event->Special;
		}

		/**
		 * Set correct parent path for newly created categories
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterCopyToLive(kEvent $event)
		{
			parent::OnAfterCopyToLive($event);

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

			$parent_path = false;
			$object->Load($event->getEventParam('id'));

			if ( $event->getEventParam('temp_id') == 0 ) {
				if ( $object->isLoaded() ) {
					// update path only for real categories (not including "Home" root category)
					$fields_hash = $object->buildParentBasedFields();
					$this->Conn->doUpdate($fields_hash, $object->TableName, 'CategoryId = ' . $object->GetID());
					$parent_path = $fields_hash['ParentPath'];
				}
			}
			else {
				$parent_path = $object->GetDBField('ParentPath');
			}

			if ( $parent_path ) {
				/** @var kPermCacheUpdater $cache_updater */
				$cache_updater = $this->Application->makeClass('kPermCacheUpdater', Array (null, $parent_path));

				$cache_updater->OneStepRun();
			}
		}

		/**
		 * Set cache modification mark if needed
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeDeleteFromLive(kEvent $event)
		{
			parent::OnBeforeDeleteFromLive($event);

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

			// loading anyway, because this object is needed by "c-perm:OnBeforeDeleteFromLive" event
			/** @var CategoriesItem $temp_object */
			$temp_object = $event->getObject(Array ('skip_autoload' => true));

			$temp_object->Load($id);

			if ( $id == 0 ) {
				if ( $temp_object->isLoaded() ) {
					// new category -> update cache (not loaded when "Home" category)
					$this->Application->StoreVar('PermCache_UpdateRequired', 1);
				}

				return ;
			}

			// existing category was edited, check if in-cache fields are modified
			/** @var CategoriesItem $live_object */
			$live_object = $this->Application->recallObject($event->Prefix . '.-item', null, Array ('live_table' => true, 'skip_autoload' => true));

			$live_object->Load($id);
			$cached_fields = Array ('l' . $this->Application->GetDefaultLanguageId() . '_Name', 'Filename', 'Template', 'ParentId', 'Priority');

			foreach ($cached_fields as $cached_field) {
				if ( $live_object->GetDBField($cached_field) != $temp_object->GetDBField($cached_field) ) {
					// use session instead of REQUEST because of permission editing in category can contain
					// multiple submits, that changes data before OnSave event occurs
					$this->Application->StoreVar('PermCache_UpdateRequired', 1);
					break;
				}
			}

			// remember category filename change between temp and live records
			if ( $temp_object->GetDBField('Filename') != $live_object->GetDBField('Filename') ) {
				$filename_changes = $this->Application->GetVar($event->Prefix . '_filename_changes', Array ());

				$filename_changes[ $live_object->GetID() ] = Array (
					'from' => $live_object->GetDBField('Filename'),
					'to' => $temp_object->GetDBField('Filename')
				);

				$this->Application->SetVar($event->Prefix . '_filename_changes', $filename_changes);
			}
		}

		/**
		 * Calls kDBEventHandler::OnSave original event
		 * Used in proj-cms:StructureEventHandler->OnSave
		 *
		 * @param kEvent $event
		 */
		function parentOnSave($event)
		{
			parent::OnSave($event);
		}

		/**
		 * Reset root-category flag when new category is created
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreCreate(kEvent $event)
		{
			// 1. for permission editing of Home category
			$this->Application->RemoveVar('IsRootCategory_' . $this->Application->GetVar('m_wid'));

			parent::OnPreCreate($event);

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

			// 2. preset template
			$category_id = $this->Application->GetVar('m_cat_id');
			$root_category = $this->Application->getBaseCategory();

			if ( $category_id == $root_category ) {
				$object->SetDBField('Template', $this->_getDefaultDesign());
			}

			// 3. set default owner
			$object->SetDBField('CreatedById', $this->Application->RecallVar('user_id'));
		}

		/**
		 * Checks cache update mark and redirect to cache if needed
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSave(kEvent $event)
		{
			// get data from live table before it is overwritten by parent OnSave method call
			$ids = $this->getSelectedIDs($event, true);
			$is_editing = implode('', $ids);
			$old_statuses = $is_editing ? $this->_getCategoryStatus($ids) : Array ();

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

			parent::OnSave($event);

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

			if ( $this->Application->RecallVar('PermCache_UpdateRequired') ) {
				$this->Application->RemoveVar('IsRootCategory_' . $this->Application->GetVar('m_wid'));
			}

			$this->Application->StoreVar('RefreshStructureTree', 1);
			$this->_resetMenuCache();

			if ( $is_editing ) {
				// send email event to category owner, when it's status is changed (from admin)
				$object->SwitchToLive();
				$new_statuses = $this->_getCategoryStatus($ids);
				$process_statuses = Array (STATUS_ACTIVE, STATUS_DISABLED);

				foreach ($new_statuses as $category_id => $new_status) {
					if ( $new_status != $old_statuses[$category_id] && in_array($new_status, $process_statuses) ) {
						$object->Load($category_id);
						$email_event = $new_status == STATUS_ACTIVE ? 'CATEGORY.APPROVE' : 'CATEGORY.DENY';
						$this->Application->emailUser($email_event, $object->GetDBField('CreatedById'));
					}
				}
			}

			// change opener stack in case if edited category filename was changed
			$filename_changes = $this->Application->GetVar($event->Prefix . '_filename_changes', Array ());

			if ( $filename_changes ) {
				/** @var kOpenerStack $opener_stack */
				$opener_stack = $this->Application->makeClass('kOpenerStack');

				list ($template, $params, $index_file) = $opener_stack->pop();

				foreach ($filename_changes as $change_info) {
					$template = str_ireplace($change_info['from'], $change_info['to'], $template);
				}

				$opener_stack->push($template, $params, $index_file);
				$opener_stack->save();
			}
		}

		/**
		 * Returns statuses of given categories
		 *
		 * @param Array $category_ids
		 * @return Array
		 */
		function _getCategoryStatus($category_ids)
		{
			$id_field = $this->Application->getUnitOption($this->Prefix, 'IDField');
			$table_name = $this->Application->getUnitOption($this->Prefix, 'TableName');

			$sql = 'SELECT Status, ' . $id_field . '
					FROM ' . $table_name . '
					WHERE ' . $id_field . ' IN (' . implode(',', $category_ids) . ')';
			return $this->Conn->GetCol($sql, $id_field);
		}

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

			if ( $object->IsRoot() ) {
				// don't create root category while saving permissions
				return;
			}

			parent::OnPreSaveCreated($event);
		}

		/**
		 * Deletes sym link to other category
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemDelete(kEvent $event)
		{
			parent::OnAfterItemDelete($event);

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

			$sql = 'UPDATE ' . $object->TableName . '
					SET SymLinkCategoryId = NULL
					WHERE SymLinkCategoryId = ' . $object->GetID();
			$this->Conn->Query($sql);

			// delete direct subscriptions to category, that was deleted
			$sql = 'SELECT SubscriptionId
					FROM ' . TABLE_PREFIX . 'SystemEventSubscriptions
					WHERE CategoryId = ' . $object->GetID();
			$ids = $this->Conn->GetCol($sql);

			if ( $ids ) {
				/** @var kTempTablesHandler $temp_handler */
				$temp_handler = $this->Application->recallObject('system-event-subscription_TempHandler', 'kTempTablesHandler');

				$temp_handler->DeleteItems('system-event-subscription', '', $ids);
			}
		}

		/**
		 * Exclude root categories from deleting
		 *
		 * @param kEvent $event
		 * @param string $type
		 * @return void
		 * @access protected
		 */
		protected function customProcessing(kEvent $event, $type)
		{
			if ( $event->Name == 'OnMassDelete' && $type == 'before' ) {
				$ids = $event->getEventParam('ids');
				if ( !$ids || $this->Application->ConfigValue('AllowDeleteRootCats') ) {
					return;
				}

				$root_categories = Array ();

				// get module root categories and exclude them
				foreach ($this->Application->ModuleInfo as $module_info) {
					$root_categories[] = $module_info['RootCat'];
				}

				$root_categories = array_unique($root_categories);

				if ( $root_categories && array_intersect($ids, $root_categories) ) {
					$event->setEventParam('ids', array_diff($ids, $root_categories));
					$this->Application->StoreVar('root_delete_error', 1);
				}
			}
		}

		/**
		 * Checks, that given template exists (physically) in given theme
		 *
		 * @param string $template
		 * @param int $theme_id
		 * @return bool
		 */
		function _templateFound($template, $theme_id = null)
		{
			static $init_made = false;

			if (!$init_made) {
				$this->Application->InitParser(true);
				$init_made = true;
			}

			if (!isset($theme_id)) {
				$theme_id = $this->_getCurrentThemeId();
			}

			$theme_name = $this->_getThemeName($theme_id);

			return $this->Application->TemplatesCache->TemplateExists('theme:' . $theme_name . '/' . $template);
		}

		/**
		 * Removes ".tpl" in template path
		 *
		 * @param string $template
		 * @return string
		 */
		function _stripTemplateExtension($template)
		{
	//		return preg_replace('/\.[^.\\\\\\/]*$/', '', $template);

			return preg_replace('/^[\\/]{0,1}(.*)\.tpl$/', "$1", $template);
		}

		/**
		 * Deletes all selected items.
		 * Automatically recourse 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;
			}

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

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

				$rb->Load($recycle_bin);

				/** @var CategoriesItem $cat */
				$cat = $event->getObject(Array ('skip_autoload' => true));

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

					if ( preg_match('/^' . preg_quote($rb->GetDBField('ParentPath'), '/') . '/', $cat->GetDBField('ParentPath')) ) {
						// already in "Recycle Bin" -> delete for real
						$to_delete[] = $id;
						continue;
					}

					// just move into "Recycle Bin" category
					$cat->SetDBField('ParentId', $recycle_bin);
					$cat->Update();
				}

				$ids = $to_delete;
			}

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

			if ( $ids ) {
				/** @var kRecursiveHelper $recursive_helper */
				$recursive_helper = $this->Application->recallObject('RecursiveHelper');

				foreach ($ids as $id) {
					$recursive_helper->DeleteCategory($id, $event->Prefix);
				}
			}

			$this->clearSelectedIDs($event);

			$this->_ensurePermCacheRebuild($event);
		}

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

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

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

		/**
		 * Add selected items to clipboard with mode = CUT
		 *
		 * @param kEvent $event
		 */
		function OnCut($event)
		{
			$this->Application->RemoveVar('clipboard');

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

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

		/**
		 * Controls all item paste operations. Can occur only with filled clipboard.
		 *
		 * @param kEvent $event
		 */
		function OnPasteClipboard($event)
		{
			$clipboard = unserialize( $this->Application->RecallVar('clipboard') );
			foreach ($clipboard as $prefix => $clipboard_data) {
				$paste_event = new kEvent($prefix.':OnPaste', Array('clipboard_data' => $clipboard_data));
				$this->Application->HandleEvent($paste_event);

				$event->copyFrom($paste_event);
			}
		}

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

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

		/**
		 * Paste categories with sub-items from clipboard
		 *
		 * @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;
			}

			// 1. get ParentId of moved category(-es) before it gets updated!!!)
			$source_category_id = 0;
			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
			$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');

			if ( $clipboard_data['cut'] ) {
				$sql = 'SELECT ParentId
						FROM ' . $table_name . '
						WHERE ' . $id_field . ' = ' . $clipboard_data['cut'][0];
				$source_category_id = $this->Conn->GetOne($sql);
			}

			/** @var kRecursiveHelper $recursive_helper */
			$recursive_helper = $this->Application->recallObject('RecursiveHelper');

			if ( $clipboard_data['cut'] ) {
				$recursive_helper->MoveCategories($clipboard_data['cut'], $this->Application->GetVar('m_cat_id'));
			}

			if ( $clipboard_data['copy'] ) {
				// don't allow to copy/paste system OR theme-linked virtual pages

				$sql = 'SELECT ' . $id_field . '
						FROM ' . $table_name . '
						WHERE ' . $id_field . ' IN (' . implode(',', $clipboard_data['copy']) . ') AND (`Type` = ' . PAGE_TYPE_VIRTUAL . ') AND (ThemeId = 0)';
				$allowed_ids = $this->Conn->GetCol($sql);

				if ( !$allowed_ids ) {
					return;
				}

				foreach ($allowed_ids as $id) {
					$recursive_helper->PasteCategory($id, $event->Prefix);
				}
			}

			/** @var kPriorityHelper $priority_helper */
			$priority_helper = $this->Application->recallObject('PriorityHelper');

			if ( $clipboard_data['cut'] ) {
				$ids = $priority_helper->recalculatePriorities($event, 'ParentId = ' . $source_category_id);

				if ( $ids ) {
					$priority_helper->massUpdateChanged($event->Prefix, $ids);
				}
			}

			// recalculate priorities of newly pasted categories in destination category
			$parent_id = $this->Application->GetVar('m_cat_id');
			$ids = $priority_helper->recalculatePriorities($event, 'ParentId = ' . $parent_id);

			if ( $ids ) {
				$priority_helper->massUpdateChanged($event->Prefix, $ids);
			}

			if ( $clipboard_data['cut'] || $clipboard_data['copy'] ) {
				$this->_ensurePermCacheRebuild($event);
			}
		}

		/**
		 * Ensures, that category permission cache is rebuild when category is added/edited/deleted
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function _ensurePermCacheRebuild(kEvent $event)
		{
			$this->Application->StoreVar('PermCache_UpdateRequired', 1);
			$this->Application->StoreVar('RefreshStructureTree', 1);
		}

		/**
		 * Occurs when pasting category
		 *
		 * @param kEvent $event
		 */
		/*function OnCatPaste($event)
		{
			$inp_clipboard = $this->Application->RecallVar('ClipBoard');
			$inp_clipboard = explode('-', $inp_clipboard, 2);

			if($inp_clipboard[0] == 'COPY')
			{
				$saved_cat_id = $this->Application->GetVar('m_cat_id');
				$cat_ids = $event->getEventParam('cat_ids');

				$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
				$table = $this->Application->getUnitOption($event->Prefix, 'TableName');
				$ids_sql = 'SELECT '.$id_field.' FROM '.$table.' WHERE ResourceId IN (%s)';
				$resource_ids_sql = 'SELECT ItemResourceId FROM '.TABLE_PREFIX.'CategoryItems WHERE CategoryId = %s AND PrimaryCat = 1';

				$object = $this->Application->recallObject($event->Prefix.'.item', $event->Prefix, Array('skip_autoload' => true));

				foreach($cat_ids as $source_cat => $dest_cat)
				{
					$item_resource_ids = $this->Conn->GetCol( sprintf($resource_ids_sql, $source_cat) );
					if(!$item_resource_ids) continue;

					$this->Application->SetVar('m_cat_id', $dest_cat);
					$item_ids = $this->Conn->GetCol( sprintf($ids_sql, implode(',', $item_resource_ids) ) );

					$temp = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler');
					if($item_ids) $temp->CloneItems($event->Prefix, $event->Special, $item_ids);
				}

				$this->Application->SetVar('m_cat_id', $saved_cat_id);
			}
		}*/

		/**
		 * Clears clipboard content
		 *
		 * @param kEvent $event
		 */
		function OnClearClipboard($event)
		{
			$this->Application->RemoveVar('clipboard');
		}

		/**
		 * Validates category data.
		 *
		 * @param kEvent $event Event.
		 *
		 * @return void
		 */
		protected function OnBeforeItemValidate(kEvent $event)
		{
			parent::OnBeforeItemValidate($event);

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

			$friendly_url = $object->GetDBField('FriendlyURL');

			if ( strlen($friendly_url) && $friendly_url != $object->GetOriginalField('FriendlyURL') ) {
				$sql = 'SELECT CategoryId
						FROM %s
						WHERE NamedParentPath = ' . $this->Conn->qstr('Content/' . $friendly_url);
				$duplicate_id = $this->Conn->GetOne(sprintf($sql, $object->TableName));

				if ( $duplicate_id === false && $object->IsTempTable() ) {
					$duplicate_id = $this->Conn->GetOne(sprintf(
						$sql,
						$this->Application->GetLiveName($object->TableName)
					));
				}

				if ( $duplicate_id !== false ) {
					$object->SetError('FriendlyURL', 'unique');
				}
			}
		}

		/**
		 * Sets correct status for new categories created on front-end
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemCreate(kEvent $event)
		{
			parent::OnBeforeItemCreate($event);

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

			if ( $object->GetDBField('ParentId') <= 0 ) {
				// no parent category - use current (happens during import)
				$object->SetDBField('ParentId', $this->Application->GetVar('m_cat_id'));
			}

			$this->_beforeItemChange($event);

			if ( $this->Application->isAdmin || $event->Prefix == 'st' ) {
				// don't check category permissions when auto-creating structure pages
				return ;
			}

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

			$new_status = false;
			$category_id = $this->Application->GetVar('m_cat_id');

			if ( $perm_helper->CheckPermission('CATEGORY.ADD', 0, $category_id) ) {
				$new_status = STATUS_ACTIVE;
			}
			else {
				if ( $perm_helper->CheckPermission('CATEGORY.ADD.PENDING', 0, $category_id) ) {
					$new_status = STATUS_PENDING;
				}
			}

			if ( $new_status ) {
				$object->SetDBField('Status', $new_status);

				// don't forget to set Priority for suggested from Front-End categories
				$min_priority = $this->_getNextPriority($object->GetDBField('ParentId'), $object->TableName);
				$object->SetDBField('Priority', $min_priority);
			}
			else {
				$event->status = kEvent::erPERM_FAIL;
				return ;
			}
		}

		/**
		 * Returns next available priority for given category from given table
		 *
		 * @param int $category_id
		 * @param string $table_name
		 * @return int
		 */
		function _getNextPriority($category_id, $table_name)
		{
			$sql = 'SELECT MIN(Priority)
					FROM ' . $table_name . '
					WHERE ParentId = ' . $category_id;
			return (int)$this->Conn->GetOne($sql) - 1;
		}

		/**
		 * Sets correct status for new categories created on front-end
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemUpdate(kEvent $event)
		{
			parent::OnBeforeItemUpdate($event);

			$this->_beforeItemChange($event);

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

			if ( $object->GetChangedFields() ) {
				$object->SetDBField('ModifiedById', $this->Application->RecallVar('user_id'));
			}
		}

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

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

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

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

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

			$special = $this->_getCategorySpecial($event);
			$sqls = $object->getFormOption('ListSQLs', Array ());

			return $sqls[array_key_exists($special, $sqls) ? $special : ''];
		}

		/**
		 * Performs redirect to correct suggest confirmation template
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnCreate(kEvent $event)
		{
			parent::OnCreate($event);

			if ( $this->Application->isAdmin || $event->status != kEvent::erSUCCESS ) {
				// don't sent email or rebuild cache directly after category is created by admin
				return;
			}

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

			/** @var kPermCacheUpdater $cache_updater */
			$cache_updater = $this->Application->makeClass('kPermCacheUpdater', Array (null, $object->GetDBField('ParentPath')));

			$cache_updater->OneStepRun();

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

			$next_template = $is_active ? 'suggest_confirm_template' : 'suggest_pending_confirm_template';
			$event->redirect = $this->Application->GetVar($next_template);
			$event->SetRedirectParam('opener', 's');

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

			$event_suffix = $is_active ? 'ADD' : 'ADD.PENDING';
			$this->Application->emailAdmin($perm_prefix . '.' . $event_suffix);
			$this->Application->emailUser($perm_prefix . '.' . $event_suffix, $object->GetDBField('CreatedById'));
		}

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

				$per_page = parent::getPerPage($event);

				$event->setEventParam('same_special', $same_special);
			}

			return parent::getPerPage($event);
		}

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

			if ( !$this->Application->isAdmin ) {
				$page_var = $event->getEventParam('page_var');

				if ( $page_var !== false ) {
					$page = $this->Application->GetVar($page_var);

					if ( is_numeric($page) ) {
						/** @var kDBList $object */
						$object = $event->getObject();

						$object->SetPage($page);
					}
				}
			}
		}

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

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

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

			if ( $ids ) {
				$status_field = $object->getStatusField();
				$propagate_category_status = $this->Application->GetVar('propagate_category_status');

				foreach ($ids as $id) {
					$object->Load($id);
					$object->SetDBField($status_field, $event->Name == 'OnMassApprove' ? 1 : 0);

					if ( $object->Update() ) {
						if ( $propagate_category_status ) {
							$sql = 'UPDATE ' . $object->TableName . '
									SET ' . $status_field . ' = ' . $object->GetDBField($status_field) . '
									WHERE TreeLeft BETWEEN ' . $object->GetDBField('TreeLeft') . ' AND ' . $object->GetDBField('TreeRight');
							$this->Conn->Query($sql);
						}

						$email_event = $event->Name == 'OnMassApprove' ? 'CATEGORY.APPROVE' : 'CATEGORY.DENY';
						$this->Application->emailUser($email_event, $object->GetDBField('CreatedById'));
					}
				}
			}

			$this->clearSelectedIDs($event);
			$this->Application->StoreVar('RefreshStructureTree', 1);
		}

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

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

			if ( $object->GetDBField('Status') != STATUS_ACTIVE && $object->GetDBField('Status') != 4 ) {
				if ( !$object->GetDBField('DirectLinkEnabled') || !$object->GetDBField('DirectLinkAuthKey') ) {
					return false;
				}

				return $this->Application->GetVar('authkey') == $object->GetDBField('DirectLinkAuthKey');
			}

			return true;
		}

		/**
		 * 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)
		{
			$types = $event->getEventParam('types');
			$types = $types ? explode(',', $types) : Array ();

			if ( in_array('search', $types) ) {
				$event->setPseudoClass('_List');

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

				// 1. no user sorting - sort by relevance
				$default_sortings = parent::_getDefaultSorting($event);
				$default_sorting = key($default_sortings['Sorting']) . ',' . current($default_sortings['Sorting']);

				if ( $object->isMainList() ) {
					$sort_by = $this->Application->GetVar('sort_by', '');

					if ( !$sort_by ) {
						$this->Application->SetVar('sort_by', 'Relevance,desc|' . $default_sorting);
					}
					elseif ( strpos($sort_by, 'Relevance,') !== false ) {
						$this->Application->SetVar('sort_by', $sort_by . '|' . $default_sorting);
					}
				}
				else {
					$sorting_settings = $this->getListSetting($event, 'Sortings');
					$sort_by = trim(getArrayValue($sorting_settings, 'Sort1') . ',' . getArrayValue($sorting_settings, 'Sort1_Dir'), ',');

					if ( !$sort_by ) {
						$event->setEventParam('sort_by', 'Relevance,desc|' . $default_sorting);
					}
					elseif ( strpos($sort_by, 'Relevance,') !== false ) {
						$event->setEventParam('sort_by', $sort_by . '|' . $default_sorting);
					}
				}

				$this->_removeForcedSortings($event);
			}

			parent::SetSorting($event);
		}

		/**
		 * Removes forced sortings
		 *
		 * @param kEvent $event
		 */
		protected function _removeForcedSortings(kEvent $event)
		{
			/** @var Array $list_sortings */
			$list_sortings = $this->Application->getUnitOption($event->Prefix, 'ListSortings', Array ());

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

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

		/**
		 * Default sorting in search results only comes from relevance field
		 *
		 * @param kEvent $event
		 * @return Array
		 * @access protected
		 */
		protected function _getDefaultSorting(kEvent $event)
		{
			$types = $event->getEventParam('types');
			$types = $types ? explode(',', $types) : Array ();

			return in_array('search', $types) ? Array () : parent::_getDefaultSorting($event);
		}

		// ============= for cms page processing =======================

		/**
		 * Returns default design template
		 *
		 * @return string
		 */
		function _getDefaultDesign()
		{
			$default_design = trim($this->Application->ConfigValue('cms_DefaultDesign'), '/');

			if (!$default_design) {
				// theme-based alias for default design
				return '#default_design#';
			}

			if (strpos($default_design, '#') === false) {
				// real template, not alias, so prefix with "/"
				return '/' . $default_design;
			}

			// alias
			return $default_design;
		}

		/**
		 * Returns default design based on given virtual template (used from kApplication::Run)
		 *
		 * @param string $t
		 * @return string
		 * @access public
		 */
		public function GetDesignTemplate($t = null)
		{
			if ( !isset($t) ) {
				$t = $this->Application->GetVar('t');
			}

			/** @var CategoriesItem $page */
			$page = $this->Application->recallObject($this->Prefix . '.-virtual', null, Array ('page' => $t));

			if ( $page->isLoaded() ) {
				$real_t = $page->GetDBField('CachedTemplate');
				$this->Application->SetVar('m_cat_id', $page->GetDBField('CategoryId'));

				if ( $page->GetDBField('FormId') ) {
					$this->Application->SetVar('form_id', $page->GetDBField('FormId'));
				}
			}
			else {
				$this->Application->UrlManager->show404();
			}

			// replace alias in form #alias_name# to actual template used in this theme
			if ( $this->Application->isAdmin ) {
				/** @var kThemesHelper $themes_helper */
				$themes_helper = $this->Application->recallObject('ThemesHelper');

				// only, used when in "Design Mode"
				$this->Application->SetVar('theme.current_id', $themes_helper->getCurrentThemeId());
			}

			/** @var kDBItem $theme */
			$theme = $this->Application->recallObject('theme.current');

			$template = $theme->GetField('TemplateAliases', $real_t);

			if ( $template ) {
				return $template;
			}

			return $real_t;
		}

		/**
		 * Sets category id based on found template (used from kApplication::Run)
		 *
		 * @deprecated
		 */
		/*function SetCatByTemplate()
		{
			$t = $this->Application->GetVar('t');
			$page = $this->Application->recallObject($this->Prefix . '.-virtual');

			if ( $page->isLoaded() ) {
				$this->Application->SetVar('m_cat_id', $page->GetDBField('CategoryId'));
			}
		}*/

		/**
		 * Prepares template paths
		 *
		 * @param kEvent $event
		 */
		function _beforeItemChange($event)
		{
			/** @var CategoriesItem $object */
			$object = $event->getObject();

			$now = adodb_mktime();

			if ( !$this->Application->isDebugMode() && strpos($event->Special, 'rebuild') === false ) {
				$object->SetDBField('Type', $object->GetOriginalField('Type'));
				$object->SetDBField('Protected', $object->GetOriginalField('Protected'));

				if ( $object->GetDBField('Protected') ) {
					// some fields are read-only for protected pages, when debug mode is off
					$object->SetDBField('AutomaticFilename', $object->GetOriginalField('AutomaticFilename'));
					$object->SetDBField('Filename', $object->GetOriginalField('Filename'));
					$object->SetDBField('Status', $object->GetOriginalField('Status'));
				}
			}

			$object->checkFilename();
			$object->generateFilename();

			// Don't allow creating records on behalf of another user.
			if ( !$this->Application->isAdminUser && !defined('CRON') ) {
				$object->SetDBField('CreatedById', $object->GetOriginalField('CreatedById'));
			}

			// Auto-assign records to currently logged-in user.
			if ( !$object->GetDBField('CreatedById') ) {
				$object->SetDBField('CreatedById', $this->Application->RecallVar('user_id'));
			}

			if ($object->GetChangedFields()) {
				$object->SetDBField('Modified_date', $now);
				$object->SetDBField('Modified_time', $now);
			}

			$object->setRequired('PageCacheKey', $object->GetDBField('OverridePageCacheKey'));
			$object->SetDBField('Template', $this->_stripTemplateExtension( $object->GetDBField('Template') ));

			$category_type = $object->GetDBField('Type');

			// Changing category type would associate/disassociate it to theme.
			if ( $category_type != $object->GetOriginalField('Type') ) {
				if ( $category_type == PAGE_TYPE_TEMPLATE ) {
					$object->SetDBField('ThemeId', $this->_getCurrentThemeId());
				}
				else {
					$object->SetDBField('ThemeId', 0);
				}
			}

			if ( $category_type == PAGE_TYPE_TEMPLATE ) {
				if ( !$this->_templateFound($object->GetDBField('Template'), $object->GetDBField('ThemeId')) ) {
					$object->SetError('Template', 'template_file_missing', 'la_error_TemplateFileMissing');
				}
			}

			$this->_saveTitleField($object, 'Title');
			$this->_saveTitleField($object, 'MenuTitle');

			$root_category = $this->Application->getBaseCategory();

			if ( file_exists(FULL_PATH . '/themes') && ($object->GetDBField('ParentId') == $root_category) && ($object->GetDBField('Template') == CATEGORY_TEMPLATE_INHERIT) ) {
				// there are themes + creating top level category
				$object->SetError('Template', 'no_inherit');
			}

			if ( !$this->Application->isAdminUser && $object->isVirtualField('cust_RssSource') ) {
				// only administrator can set/change "cust_RssSource" field

				if ($object->GetDBField('cust_RssSource') != $object->GetOriginalField('cust_RssSource')) {
					$object->SetError('cust_RssSource', 'not_allowed', 'la_error_OperationNotAllowed');
				}
			}

			if ( !$object->GetDBField('DirectLinkAuthKey') ) {
				$key_parts = Array (
					$object->GetID(),
					$object->GetDBField('ParentId'),
					$object->GetField('Name'),
					'b38'
				);

				$object->SetDBField('DirectLinkAuthKey', substr( md5( implode(':', $key_parts) ), 0, 20 ));
			}
		}

		/**
		 * Sets page name to requested field in case when:
		 * 1. page was auto created (through theme file rebuild)
		 * 2. requested field is empty
		 *
		 * @param kDBItem $object
		 * @param string $field
		 * @author Alex
		 */
		function _saveTitleField(&$object, $field)
		{
			$value = $object->GetField($field, 'no_default'); // current value of target field

			/** @var kMultiLanguage $ml_formatter */
			$ml_formatter = $this->Application->recallObject('kMultiLanguage');

			$src_field = $ml_formatter->LangFieldName('Name');
			$dst_field = $ml_formatter->LangFieldName($field);

			$dst_field_not_changed = $object->GetOriginalField($dst_field) == $value;

			if ($value == '' || preg_match('/^_Auto: (.*)/', $value) || (($object->GetOriginalField($src_field) == $value) && $dst_field_not_changed)) {
				// target field is empty OR target field value starts with "_Auto: " OR (source field value
				// before change was equals to current target field value AND target field value wasn't changed)
				$object->SetField($dst_field, $object->GetField($src_field));
			}
		}

		/**
		 * Don't allow to delete system pages, when not in debug mode
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemDelete(kEvent $event)
		{
			parent::OnBeforeItemDelete($event);

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

			if ( $object->GetDBField('Protected') && !$this->Application->isDebugMode(false) ) {
				$event->status = kEvent::erFAIL;
			}
		}

		/**
		 * Creates category based on given TPL file
		 *
		 * @param CategoriesItem $object
		 * @param string $template
		 * @param int $theme_id
		 * @param int $system_mode
		 * @param array $template_info
		 * @return bool
		 */
		function _prepareAutoPage(&$object, $template, $theme_id = null, $system_mode = SMS_MODE_AUTO, $template_info = Array ())
		{
			$template = $this->_stripTemplateExtension($template);

			if ($system_mode == SMS_MODE_AUTO) {
				$page_type = $this->_templateFound($template, $theme_id) ? PAGE_TYPE_TEMPLATE : PAGE_TYPE_VIRTUAL;
			}
			else {
				$page_type = $system_mode == SMS_MODE_FORCE ? PAGE_TYPE_TEMPLATE : PAGE_TYPE_VIRTUAL;
			}

			if (($page_type == PAGE_TYPE_TEMPLATE) && ($template_info === false)) {
				// do not auto-create system pages, when browsing through site
				return false;
			}

			if (!isset($theme_id)) {
				$theme_id = $this->_getCurrentThemeId();
			}

			$root_category = $this->Application->getBaseCategory();
			$page_category = $this->Application->GetVar('m_cat_id');
			if (!$page_category) {
				$page_category = $root_category;
				$this->Application->SetVar('m_cat_id', $page_category);
			}

			if (($page_type == PAGE_TYPE_VIRTUAL) && (strpos($template, '/') !== false)) {
				// virtual page, but have "/" in template path -> create it's path
				$category_path = explode('/', $template);
				$template = array_pop($category_path);

				$page_category = $this->_getParentCategoryFromPath($category_path, $root_category, $theme_id);
			}

			$page_name = ($page_type == PAGE_TYPE_TEMPLATE) ? '_Auto: ' . $template : $template;
			$page_description = '';

			if ($page_type == PAGE_TYPE_TEMPLATE) {
				$design_template = strtolower($template); // leading "/" not added !
				if ($template_info) {
					if (array_key_exists('name', $template_info) && $template_info['name']) {
						$page_name = $template_info['name'];
					}

					if (array_key_exists('desc', $template_info) && $template_info['desc']) {
						$page_description = $template_info['desc'];
					}

					if (array_key_exists('section', $template_info) && $template_info['section']) {
						// this will override any global "m_cat_id"
						$page_category = $this->_getParentCategoryFromPath(explode('||', $template_info['section']), $root_category, $theme_id);
					}
				}
			}
			else {
				$design_template = $this->_getDefaultDesign(); // leading "/" added !
			}

			$object->Clear();
			$object->SetDBField('ParentId', $page_category);
			$object->SetDBField('Type', $page_type);
			$object->SetDBField('Protected', 1); // $page_type == PAGE_TYPE_TEMPLATE

			$object->SetDBField('IsMenu', 0);
			$object->SetDBField('ThemeId', $theme_id);

			// put all templates to then end of list (in their category)
			$min_priority = $this->_getNextPriority($page_category, $object->TableName);
			$object->SetDBField('Priority', $min_priority);

			$object->SetDBField('Template', $design_template);
			$object->SetDBField('CachedTemplate', $design_template);

			$primary_language = $this->Application->GetDefaultLanguageId();
			$current_language = $this->Application->GetVar('m_lang');
			$object->SetDBField('l' . $primary_language . '_Name', $page_name);
			$object->SetDBField('l' . $current_language . '_Name', $page_name);
			$object->SetDBField('l' . $primary_language . '_Description', $page_description);
			$object->SetDBField('l' . $current_language . '_Description', $page_description);

			return $object->Create();
		}

		function _getParentCategoryFromPath($category_path, $base_category, $theme_id = null)
		{
			static $category_ids = Array ();

			if (!$category_path) {
				return $base_category;
			}

			if (array_key_exists(implode('||', $category_path), $category_ids)) {
				return $category_ids[ implode('||', $category_path) ];
			}

			$backup_category_id = $this->Application->GetVar('m_cat_id');

			/** @var CategoriesItem $object */
			$object = $this->Application->recallObject($this->Prefix . '.rebuild-path', null, Array ('skip_autoload' => true));

			$parent_id = $base_category;

			/** @var kFilenamesHelper $filenames_helper */
			$filenames_helper = $this->Application->recallObject('FilenamesHelper');

			$safe_category_path = array_map(Array (&$filenames_helper, 'replaceSequences'), $category_path);

			foreach ($category_path as $category_order => $category_name) {
				$this->Application->SetVar('m_cat_id', $parent_id);

				// get virtual category first, when possible
				$sql = 'SELECT ' . $object->IDField . '
						FROM ' . $object->TableName . '
						WHERE
							(
								Filename = ' . $this->Conn->qstr($safe_category_path[$category_order]) . ' OR
								Filename = ' . $this->Conn->qstr( $filenames_helper->replaceSequences('_Auto: ' . $category_name) ) . '
							) AND
							(ParentId = ' . $parent_id . ') AND
							(ThemeId = 0 OR ThemeId = ' . $theme_id . ')
						ORDER BY ThemeId ASC';
				$parent_id = $this->Conn->GetOne($sql);

				if ($parent_id === false) {
					// page not found
					$template = implode('/', array_slice($safe_category_path, 0, $category_order + 1));

					// don't process system templates in sub-categories
					$system = $this->_templateFound($template, $theme_id) && (strpos($template, '/') === false);

					if (!$this->_prepareAutoPage($object, $category_name, $theme_id, $system ? SMS_MODE_FORCE : false)) {
						// page was not created
						break;
					}

					$parent_id = $object->GetID();
				}
			}

			$this->Application->SetVar('m_cat_id', $backup_category_id);
			$category_ids[ implode('||', $category_path) ] = $parent_id;

			return $parent_id;
		}

		/**
		 * Returns theme name by it's id. Used in structure page creation.
		 *
		 * @param int $theme_id
		 * @return string
		 */
		function _getThemeName($theme_id)
		{
			static $themes = null;

			if (!isset($themes)) {
				$id_field = $this->Application->getUnitOption('theme', 'IDField');
				$table_name = $this->Application->getUnitOption('theme', 'TableName');

				$sql = 'SELECT Name, ' . $id_field . '
						FROM ' . $table_name . '
						WHERE Enabled = 1';
				$themes = $this->Conn->GetCol($sql, $id_field);
			}

			return array_key_exists($theme_id, $themes) ? $themes[$theme_id] : false;
		}

		/**
		 * Resets SMS-menu cache
		 *
		 * @param kEvent $event
		 */
		function OnResetCMSMenuCache($event)
		{
			if ($this->Application->GetVar('ajax') == 'yes') {
				$event->status = kEvent::erSTOP;
			}

			$this->_resetMenuCache();
			$event->SetRedirectParam('action_completed', 1);
		}

		/**
		 * Performs reset of category-related caches (menu, structure dropdown, template mapping)
		 *
		 * @return void
		 * @access protected
		 */
		protected function _resetMenuCache()
		{
			// reset cms menu cache (all variables are automatically rebuild, when missing)
			if ($this->Application->isCachingType(CACHING_TYPE_MEMORY)) {
				$this->Application->rebuildCache('master:cms_menu', kCache::REBUILD_LATER, CacheSettings::$cmsMenuRebuildTime);
				$this->Application->rebuildCache('master:StructureTree', kCache::REBUILD_LATER, CacheSettings::$structureTreeRebuildTime);
				$this->Application->rebuildCache('master:template_mapping', kCache::REBUILD_LATER, CacheSettings::$templateMappingRebuildTime);
			}
			else {
				$this->Application->rebuildDBCache('cms_menu', kCache::REBUILD_LATER, CacheSettings::$cmsMenuRebuildTime);
				$this->Application->rebuildDBCache('StructureTree', kCache::REBUILD_LATER, CacheSettings::$structureTreeRebuildTime);
				$this->Application->rebuildDBCache('template_mapping', kCache::REBUILD_LATER, CacheSettings::$templateMappingRebuildTime);
			}
		}

		/**
		 * Updates structure config
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterConfigRead(kEvent $event)
		{
			parent::OnAfterConfigRead($event);

			if (defined('IS_INSTALL') && IS_INSTALL) {
				// skip any processing, because Categories table doesn't exists until install is finished
				$this->addViewPermissionJoin($event);

				return ;
			}

			/** @var SiteConfigHelper $site_config_helper */
			$site_config_helper = $this->Application->recallObject('SiteConfigHelper');

			$settings = $site_config_helper->getSettings();

			$root_category = $this->Application->getBaseCategory();

			// set root category
			$section_adjustments = $this->Application->getUnitOption($event->Prefix, 'SectionAdjustments');

			$section_adjustments['in-portal:browse'] = Array (
				'url' => Array ('m_cat_id' => $root_category),
				'late_load' => Array ('m_cat_id' => $root_category),
				'onclick' => 'checkCatalog(' . $root_category . ', "c")',
			);

			if ( $this->Application->ConfigValue('Catalog_PreselectModuleTab') ) {
				$section_adjustments['in-portal:browse']['url']['anchor'] = 'tab-c';
			}

			$section_adjustments['in-portal:browse_site'] = Array (
				'url' => Array ('editing_mode' => $settings['default_editing_mode']),
			);

			$this->Application->setUnitOption($event->Prefix, 'SectionAdjustments', $section_adjustments);

			// prepare structure dropdown
			/** @var CategoryHelper $category_helper */
			$category_helper = $this->Application->recallObject('CategoryHelper');

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

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

			// limit design list by theme
			$theme_id = $this->_getCurrentThemeId();
			$design_sql = $fields['Template']['options_sql'];
			$design_sql = str_replace('(tf.FilePath = "/designs")', '(' . implode(' OR ', $this->getDesignFolders()) . ')' . ' AND (t.ThemeId = ' . $theme_id . ')', $design_sql);
			$fields['Template']['options_sql'] = $design_sql;

			// adds "Inherit From Parent" option to "Template" field
			$fields['Template']['options'] = Array (CATEGORY_TEMPLATE_INHERIT => $this->Application->Phrase('la_opt_InheritFromParent'));

			$this->Application->setUnitOption($event->Prefix, 'Fields', $fields);

			if ($this->Application->isAdmin) {
				// don't sort by Front-End sorting fields
				$config_mapping = $this->Application->getUnitOption($event->Prefix, 'ConfigMapping');
				$remove_keys = Array ('DefaultSorting1Field', 'DefaultSorting2Field', 'DefaultSorting1Dir', 'DefaultSorting2Dir');
				foreach ($remove_keys as $remove_key) {
					unset($config_mapping[$remove_key]);
				}
				$this->Application->setUnitOption($event->Prefix, 'ConfigMapping', $config_mapping);
			}
			else {
				// sort by parent path on Front-End only
				$list_sortings = $this->Application->getUnitOption($event->Prefix, 'ListSortings', Array ());
				$list_sortings['']['ForcedSorting'] = Array ("CurrentSort" => 'asc');
				$this->Application->setUnitOption($event->Prefix, 'ListSortings', $list_sortings);
			}

			$this->addViewPermissionJoin($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_parent_category_td', 'filter_block' => 'grid_like_filter');
				$grids[$process_grid . 'ShowAll'] = $grid_data;
			}
			$this->Application->setUnitOption($this->Prefix, 'Grids', $grids);
		}

		/**
		 * Adds permission table table JOIN clause only, when advanced catalog view permissions enabled.
		 *
		 * @param kEvent $event Event.
		 *
		 * @return self
		 * @access protected
		 */
		protected function addViewPermissionJoin(kEvent $event)
		{
			if ( $this->Application->ConfigValue('CheckViewPermissionsInCatalog') ) {
				$join_clause = 'LEFT JOIN ' . TABLE_PREFIX . 'CategoryPermissionsCache perm ON perm.CategoryId = %1$s.CategoryId';
			}
			else {
				$join_clause = '';
			}

			/** @var array $list_sqls */
			$list_sqls = $this->Application->getUnitOption($event->Prefix, 'ListSQLs');

			foreach ($list_sqls as $special => $list_sql) {
				$list_sqls[$special] = str_replace('{PERM_JOIN}', $join_clause, $list_sql);
			}

			$this->Application->setUnitOption($event->Prefix, 'ListSQLs', $list_sqls);

			return $this;
		}

		/**
		 * Returns folders, that can contain design templates
		 *
		 * @return array
		 * @access protected
		 */
		protected function getDesignFolders()
		{
			$ret = Array ('tf.FilePath = "/designs"', 'tf.FilePath = "/platform/designs"');

			foreach ($this->Application->ModuleInfo as $module_info) {
				$ret[] = 'tf.FilePath = "/' . $module_info['TemplatePath'] . 'designs"';
			}

			return array_unique($ret);
		}

		/**
		 * Removes this item and it's children (recursive) from structure dropdown
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemLoad(kEvent $event)
		{
			parent::OnAfterItemLoad($event);

			if ( !$this->Application->isAdmin ) {
				// calculate priorities dropdown only for admin
				return;
			}

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

			// remove this category & it's children from dropdown
			$sql = 'SELECT ' . $object->IDField . '
					FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
					WHERE ParentPath LIKE "' . $object->GetDBField('ParentPath') . '%"';
			$remove_categories = $this->Conn->GetCol($sql);

			$options = $object->GetFieldOption('ParentId', 'options');
			foreach ($remove_categories as $remove_category) {
				unset($options[$remove_category]);
			}
			$object->SetFieldOption('ParentId', 'options', $options);
		}

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

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

			// need to update path after category is created, so category is included in that path
			$fields_hash = $object->buildParentBasedFields();
			$this->Conn->doUpdate($fields_hash, $object->TableName, $object->IDField . ' = ' . $object->GetID());
			$object->SetDBFieldsFromHash($fields_hash);
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 */
		function OnAfterRebuildThemes($event)
		{
			$sql = 'SELECT t.ThemeId, CONCAT( tf.FilePath, \'/\', tf.FileName ) AS Path, tf.FileMetaInfo
					FROM ' . TABLE_PREFIX . 'ThemeFiles AS tf
					LEFT JOIN ' . TABLE_PREFIX . 'Themes AS t ON t.ThemeId = tf.ThemeId
					WHERE t.Enabled = 1 AND tf.FileType = 1
					AND (
						SELECT COUNT(CategoryId)
						FROM ' . TABLE_PREFIX . 'Categories c
						WHERE CONCAT(\'/\', c.Template, \'.tpl\') = CONCAT( tf.FilePath, \'/\', tf.FileName ) AND (c.ThemeId = t.ThemeId)
					) = 0 ';
			$files = $this->Conn->Query($sql, 'Path');
			if ( !$files ) {
				// all possible pages are already created
				return;
			}

			kUtil::setResourceLimit();

			/** @var CategoriesItem $dummy */
			$dummy = $this->Application->recallObject($event->Prefix . '.rebuild', NULL, Array ('skip_autoload' => true));

			$error_count = 0;
			foreach ($files as $a_file => $file_info) {
				$status = $this->_prepareAutoPage($dummy, $a_file, $file_info['ThemeId'], SMS_MODE_FORCE, unserialize($file_info['FileMetaInfo'])); // create system page
				if ( !$status ) {
					$error_count++;
				}
			}

			if ( $this->Application->ConfigValue('CategoryPermissionRebuildMode') == CategoryPermissionRebuild::SILENT ) {
				/** @var kPermCacheUpdater $updater */
				$updater = $this->Application->makeClass('kPermCacheUpdater');

				$updater->OneStepRun();
			}

			$this->_resetMenuCache();

			if ( $error_count ) {
				// allow user to review error after structure page creation
				$event->MasterEvent->redirect = false;
			}
		}

		/**
		 * Processes OnMassMoveUp, OnMassMoveDown events
		 *
		 * @param kEvent $event
		 */
		function OnChangePriority($event)
		{
			$this->Application->SetVar('priority_prefix', $event->getPrefixSpecial());
			$event->CallSubEvent('priority:' . $event->Name);

			$this->Application->StoreVar('RefreshStructureTree', 1);
			$this->_resetMenuCache();
		}

		/**
		 * Completely recalculates priorities in current category
		 *
		 * @param kEvent $event
		 */
		function OnRecalculatePriorities($event)
		{
			if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$this->Application->SetVar('priority_prefix', $event->getPrefixSpecial());
			$event->CallSubEvent('priority:' . $event->Name);

			$this->_resetMenuCache();
		}

		/**
		 * Update Preview Block for FCKEditor
		 *
		 * @param kEvent $event
		 */
		function OnUpdatePreviewBlock($event)
		{
			$event->status = kEvent::erSTOP;
			$string = $this->Application->unescapeRequestVariable($this->Application->GetVar('preview_content'));

			/** @var CategoryHelper $category_helper */
			$category_helper = $this->Application->recallObject('CategoryHelper');

			$string = $category_helper->replacePageIds($string);

			$this->Application->StoreVar('_editor_preview_content_', $string);
		}

		/**
		 * Makes simple search for categories
		 * based on keywords string
		 *
		 * @param kEvent $event
		 */
		function OnSimpleSearch($event)
		{
			$event->redirect = false;

			$keywords = $this->Application->unescapeRequestVariable(trim($this->Application->GetVar('keywords')));

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

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

			$search_table = $search_helper->getSearchTable();
			$sql = 'SHOW TABLES LIKE "'.$search_table.'"';

			if ( !isset($query_object->Get['keywords']) && !isset($query_object->Post['keywords']) && $this->Conn->Query($sql) ) {
				// used when navigating by pages or changing sorting in search results
				return;
			}

			if(!$keywords || strlen($keywords) < $this->Application->ConfigValue('Search_MinKeyword_Length'))
			{
				$search_helper->ensureEmptySearchTable();
				$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

			$keywords = strtr($keywords, Array('%' => '\\%', '_' => '\\_'));

			$event->setPseudoClass('_List');

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

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

			$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.
			$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!

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

			if ($event->MasterEvent && $event->MasterEvent->Name == 'OnListBuild') {
				$sub_search_ids = $event->MasterEvent->getEventParam('ResultIds');

				if ( $sub_search_ids !== false ) {
					if ( $sub_search_ids ) {
						$where_clause .= 'AND (' . $items_table . '.ResourceId IN (' . implode(',', $sub_search_ids) . '))';
					}
					else {
						$where_clause .= 'AND FALSE';
					}
				}
			}

			// exclude template based sections from search results (ie. registration)
			if ( $this->Application->ConfigValue('ExcludeTemplateSectionsFromSearch') ) {
				$where_clause .= ' AND ' . $items_table . '.ThemeId = 0';
			}

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

				if ( count($positive_words) > 1 ) {
					$condition = $field . ' LIKE "%' . implode(' ', $positive_words) . '%"';
					$revelance_parts[] = 'IF(' . $condition . ', ' . $weight_sum . ', 0)';
				}

				// search by partial word matches too
				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);

				$this->Application->StoreVar('search_performed', 1);
			}
		}

		/**
		 * Enter description here...
		 *
		 * @param kEvent $event
		 */
		function OnSubSearch($event)
		{
			// keep search results from other items after doing a sub-search on current item type
			$this->Application->SetVar('do_not_drop_search_table', true);

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

			$search_table = $search_helper->getSearchTable();
			$sql = 'SHOW TABLES LIKE "' . $search_table . '"';
			$ids = array();

			if ( $this->Conn->Query($sql) ) {
				$item_type = $this->Application->getUnitOption($event->Prefix, 'ItemType');

				// 1. get ids to be used as search bounds
				$sql = 'SELECT DISTINCT ResourceId
						FROM ' . $search_table . '
						WHERE ItemType = ' . $item_type;
				$ids = $this->Conn->GetCol($sql);

				// 2. delete previously found ids
				$sql = 'DELETE FROM ' . $search_table . '
						WHERE ItemType = ' . $item_type;
				$this->Conn->Query($sql);
			}

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

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

		/**
		 * Load item if id is available
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function LoadItem(kEvent $event)
		{
			if ( !$this->_isVirtual($event) ) {
				parent::LoadItem($event);
				return;
			}

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

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

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

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

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

		/**
		 * Returns constrain for priority calculations
		 *
		 * @param kEvent $event
		 * @return void
		 * @see PriorityEventHandler
		 * @access protected
		 */
		protected function OnGetConstrainInfo(kEvent $event)
		{
			$constrain = ''; // for OnSave

			$event_name = $event->getEventParam('original_event');
			$actual_event_name = $event->getEventParam('actual_event');

			if ( $actual_event_name == 'OnSavePriorityChanges' || $event_name == 'OnAfterItemLoad' || $event_name == 'OnAfterItemDelete' ) {
				/** @var kDBItem $object */
				$object = $event->getObject();

				$constrain = 'ParentId = ' . $object->GetDBField('ParentId');
			}
			elseif ( $actual_event_name == 'OnPreparePriorities' ) {
				$constrain = 'ParentId = ' . $this->Application->GetVar('m_cat_id');
			}
			elseif ( $event_name == 'OnSave' ) {
				$constrain = '';
			}
			else {
				$constrain = 'ParentId = ' . $this->Application->GetVar('m_cat_id');
			}

			$event->setEventParam('constrain_info', Array ($constrain, ''));
		}

		/**
		 * Parses category part of url, build main part of url
		 *
		 * @param int $rewrite_mode Mode in what rewrite listener was called. Possbile two modes: REWRITE_MODE_BUILD, REWRITE_MODE_PARSE.
		 * @param string $prefix Prefix, that listener uses for system integration
		 * @param Array $params Params, that are used for url building or created during url parsing.
		 * @param Array $url_parts Url parts to parse (only for parsing).
		 * @param bool $keep_events Keep event names in resulting url (only for building).
		 * @return bool|string|Array Return true to continue to next listener; return false (when building) not to rewrite given prefix; return false (when parsing) to stop processing at this listener.
		 */
		public function CategoryRewriteListener($rewrite_mode = REWRITE_MODE_BUILD, $prefix, &$params, &$url_parts, $keep_events = false)
		{
			if ($rewrite_mode == REWRITE_MODE_BUILD) {
				return $this->_buildMainUrl($prefix, $params, $keep_events);
			}

			if ( $this->_parseFriendlyUrl($url_parts, $params) ) {
				// friendly urls work like exact match only!
				return false;
			}

			$this->_parseCategory($url_parts, $params);

			return true;
		}

		/**
		 * Build main part of every url
		 *
		 * @param string $prefix_special
		 * @param Array $params
		 * @param bool $keep_events
		 * @return string
		 */
		protected function _buildMainUrl($prefix_special, &$params, $keep_events)
		{
			$ret = '';
			list ($prefix) = explode('.', $prefix_special);

			/** @var kRewriteUrlProcessor $rewrite_processor */
			$rewrite_processor = $this->Application->recallObject('kRewriteUrlProcessor');

			$processed_params = $rewrite_processor->getProcessedParams($prefix_special, $params, $keep_events);
			if ($processed_params === false) {
				return '';
			}

			// add language
			if ($processed_params['m_lang'] && ($processed_params['m_lang'] != $rewrite_processor->primaryLanguageId)) {
				$language_name = $this->Application->getCache('language_names[%LangIDSerial:' . $processed_params['m_lang'] . '%]');
				if ($language_name === false) {
					$sql = 'SELECT PackName
							FROM ' . TABLE_PREFIX . 'Languages
							WHERE LanguageId = ' . $processed_params['m_lang'];
					$language_name = $this->Conn->GetOne($sql);

					$this->Application->setCache('language_names[%LangIDSerial:' . $processed_params['m_lang'] . '%]', $language_name);
				}

				$ret .= $language_name . '/';
			}

			// add theme
			if ($processed_params['m_theme'] && ($processed_params['m_theme'] != $rewrite_processor->primaryThemeId)) {
				$theme_name = $this->Application->getCache('theme_names[%ThemeIDSerial:' . $processed_params['m_theme'] . '%]');
				if ($theme_name === false) {
					$sql = 'SELECT Name
							FROM ' . TABLE_PREFIX . 'Themes
							WHERE ThemeId = ' . $processed_params['m_theme'];
					$theme_name = $this->Conn->GetOne($sql);

					$this->Application->setCache('theme_names[%ThemeIDSerial:' . $processed_params['m_theme'] . '%]', $theme_name);

				}

				$ret .= $theme_name . '/';
			}

			// inject custom url parts made by other rewrite listeners just after language/theme url parts
			if ($params['inject_parts']) {
				$ret .= implode('/', $params['inject_parts']) . '/';
			}

			// add category
			if ($processed_params['m_cat_id'] > 0 && $params['pass_category']) {
				$category_filename = $this->Application->getCategoryCache($processed_params['m_cat_id'], 'filenames');

				preg_match('/^Content\/(.*)/i', $category_filename, $regs);

				if ($regs) {
					$template = array_key_exists('t', $params) ? $params['t'] : false;

					if (strtolower($regs[1]) == strtolower($template)) {
						// we could have category path like "Content/<template_path>" in this case remove template
						$params['pass_template'] = false;
					}

					$ret .= $regs[1] . '/';
				}

				$params['category_processed'] = true;
			}

			// reset category page
			$force_page_adding = false;
			if (array_key_exists('reset', $params) && $params['reset']) {
				unset($params['reset']);

				if ($processed_params['m_cat_id']) {
					$processed_params['m_cat_page'] = 1;
					$force_page_adding = true;
				}
			}

			if ((array_key_exists('category_processed', $params) && $params['category_processed'] && ($processed_params['m_cat_page'] > 1)) || $force_page_adding) {
				// category name was added before AND category page number found
				$ret = rtrim($ret, '/') . '_' . $processed_params['m_cat_page'] . '/';
			}

			$template = array_key_exists('t', $params) ? $params['t'] : false;
			$category_template = ($processed_params['m_cat_id'] > 0) && $params['pass_category'] ? $this->Application->getCategoryCache($processed_params['m_cat_id'], 'category_designs') : '';

			if ((strtolower($template) == '__default__') && ($processed_params['m_cat_id'] == 0)) {
				// for "Home" category set template to index when not set
				$template = 'index';
			}

			// remove template from url if it is category index cached template
			if ( ($template == $category_template) || (mb_strtolower($template) == '__default__') ) {
				// given template is also default template for this category OR '__default__' given
				$params['pass_template'] = false;
			}

			// remove template from url if it is site homepage on primary language & theme
			if ( ($template == 'index') && $processed_params['m_lang'] == $rewrite_processor->primaryLanguageId && $processed_params['m_theme'] == $rewrite_processor->primaryThemeId ) {
				// given template is site homepage on primary language & theme
				$params['pass_template'] = false;
			}

			if ($template && $params['pass_template']) {
				$ret .= $template . '/';
			}

			return mb_strtolower( rtrim($ret, '/') );
		}

		/**
		 * Checks if whole url_parts matches a whole In-CMS page
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		protected function _parseFriendlyUrl($url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$sql = 'SELECT CategoryId, NamedParentPath
					FROM ' . TABLE_PREFIX . 'Categories
					WHERE FriendlyURL = ' . $this->Conn->qstr(implode('/', $url_parts));
			$friendly = $this->Conn->GetRow($sql);

			/** @var kRewriteUrlProcessor $rewrite_processor */
			$rewrite_processor = $this->Application->recallObject('kRewriteUrlProcessor');

			if ( $friendly ) {
				$vars['is_friendly_url'] = true;
				$vars['m_cat_id'] = $friendly['CategoryId'];
				$vars['t'] = preg_replace('/^Content\//i', '', $friendly['NamedParentPath']);

				while ($url_parts) {
					$rewrite_processor->partParsed( array_shift($url_parts) );
				}

				return true;
			}

			return false;
		}

		/**
		 * Extracts category part from url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		protected function _parseCategory($url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$res = false;
			$url_part = array_shift($url_parts);

			$category_id = 0;
			$last_category_info = false;
			$category_path = $url_part == 'content' ? '' : 'content';

			/** @var kRewriteUrlProcessor $rewrite_processor */
			$rewrite_processor = $this->Application->recallObject('kRewriteUrlProcessor');

			do {
				$category_path = trim($category_path . '/' . $url_part, '/');
				// bb_<topic_id> -> forums/bb_2
				if ( !preg_match('/^bb_[\d]+$/', $url_part) && preg_match('/(.*)_([\d]+)$/', $category_path, $rets) ) {
					$category_path = $rets[1];
					$vars['m_cat_page'] = $rets[2];
				}

				$sql = 'SELECT CategoryId, SymLinkCategoryId, NamedParentPath
						FROM ' . TABLE_PREFIX . 'Categories
						WHERE (LOWER(NamedParentPath) = ' . $this->Conn->qstr($category_path) . ') AND (ThemeId = ' . $vars['m_theme'] . ' OR ThemeId = 0)';
				$category_info = $this->Conn->GetRow($sql);

				if ($category_info !== false) {
					$last_category_info = $category_info;
					$rewrite_processor->partParsed($url_part);

					$url_part = array_shift($url_parts);
					$res = true;
				}
			} while ($category_info !== false && $url_part);

			if ($last_category_info) {
				// this category is symlink to other category, so use it's url instead
				// (used in case if url prior to symlink adding was indexed by spider or was bookmarked)
				if ($last_category_info['SymLinkCategoryId']) {
					$sql = 'SELECT CategoryId, NamedParentPath
							FROM ' . TABLE_PREFIX . 'Categories
							WHERE (CategoryId = ' . $last_category_info['SymLinkCategoryId'] . ')';
					$category_info = $this->Conn->GetRow($sql);

					if ($category_info) {
						// web symlinked category was found use it
						// TODO: maybe 302 redirect should be made to symlinked category url (all other url parts should stay)
						$last_category_info = $category_info;
					}
				}

				// 1. Set virtual page as template, this will be replaced to physical template later in kApplication::Run.
				// 2. Don't set CachedTemplate field as template here, because we will loose original page associated with it's cms blocks!
				$vars['t'] = mb_strtolower( preg_replace('/^Content\//i', '', $last_category_info['NamedParentPath']));

				$vars['m_cat_id'] = $last_category_info['CategoryId'];
				$vars['is_virtual'] = true; // for template from POST, strange code there!
			}
			/*else {
				$vars['m_cat_id'] = 0;
			}*/

			return $res;
		}

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

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

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

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

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

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

		}
	}
