<?php
/**
* @version	$Id: mod_rewrite_helper.php 14388 2011-06-27 07:07:32Z 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 kModRewriteHelper extends kHelper {

		/**
		 * Holds a refererence to httpquery
		 *
		 * @var kHttpQuery
		 */
		var $HTTPQuery = null;

		/**
		 * Urls parts, that needs to be matched by rewrite listeners
		 *
		 * @var Array
		 */
		var $_partsToParse = Array ();

		/**
		 * Category item prefix, that was found
		 *
		 * @var string
		 */
		var $_modulePrefix = false;

		/**
		 * Template aliases for current theme
		 *
		 * @var Array
		 */
		var $_templateAliases = null;

 		/**
		 * Domain-based primary language id
		 *
		 * @var int
		 */
		var $primaryLanguageId = false;

		/**
		 * Domain-based primary theme id
		 *
		 * @var int
		 */
		var $primaryThemeId = false;

		/**
		 * Possible url endings from ModRewriteUrlEnding configuration variable
		 *
		 * @var Array
		 */
		var $_urlEndings = Array ('.html', '/', '');

		/**
		 * Constructor of kModRewriteHelper class
		 *
		 * @return kModRewriteHelper
		 */
		function kModRewriteHelper()
		{
			parent::kHelper();

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

			// domain based primary language
			$this->primaryLanguageId = $this->Application->siteDomainField('PrimaryLanguageId');

			// domain based primary theme
			$this->primaryThemeId = $this->Application->siteDomainField('PrimaryThemeId');
		}

		function processRewriteURL()
		{
			$passed = Array ();
			$url = $this->HTTPQuery->Get('_mod_rw_url_');

			if ($url) {
				foreach ($this->_urlEndings as $url_ending) {
					if (substr($url, strlen($url) - strlen($url_ending)) == $url_ending) {
						$url = substr($url, 0, strlen($url) - strlen($url_ending));
						$default_ending = $this->Application->ConfigValue('ModRewriteUrlEnding');

						// user manually typed url with different url ending -> redirect to same url with default url ending
						if (($url_ending != $default_ending) && $this->Application->ConfigValue('ForceModRewriteUrlEnding')) {
							$target_url = $this->Application->BaseURL() . $url . $default_ending;
							$this->Application->Redirect('external:' . $target_url, Array ('response_code' => 301));
						}

						break;
					}
				}
			}

			$restored = false;

			$cached = $this->_getCachedUrl($url);

			if ($cached !== false) {
				$vars = $cached['vars'];
				$passed = $cached['passed'];
				$restored = true;
			}
			else {
				$vars = $this->parseRewriteURL($url);
				$passed = $vars['pass']; // also used in bottom of this method
				unset($vars['pass']);

				$this->_setCachedUrl($url, Array ('vars' => $vars, 'passed' => $passed));

				if (array_key_exists('t', $this->HTTPQuery->Post) && $this->HTTPQuery->Post['t']) {
					// template from POST overrides template from URL.
					$vars['t'] = $this->HTTPQuery->Post['t'];
					if (isset($vars['is_virtual']) && $vars['is_virtual']) {
						$vars['m_cat_id'] = 0; // this is virtual template category (for Proj-CMS)
					}
				}

				unset($vars['is_virtual']);
			}

			foreach ($vars as $name => $value) {
				$this->HTTPQuery->Set($name, $value);
			}

			$this->InitAll(); // also will use parsed language to load phrases from it

			$this->HTTPQuery->finalizeParsing($passed);
		}

		function _getCachedUrl($url)
		{
			if (!$url) {
				return false;
			}

			$sql = 'SELECT *
					FROM ' . TABLE_PREFIX . 'CachedUrls
					WHERE Hash = ' . crc32($url) . ' AND DomainId = ' . (int)$this->Application->siteDomainField('DomainId');
			$data = $this->Conn->GetRow($sql);

			if ($data) {
				$lifetime = (int)$data['LifeTime']; // in seconds
				if (($lifetime > 0) && ($data['Cached'] + $lifetime < adodb_mktime())) {
					// delete expired
					$sql = 'DELETE FROM ' . TABLE_PREFIX . 'CachedUrls
							WHERE UrlId = ' . $data['UrlId'];
					$this->Conn->Query($sql);

					return false;
				}

				return unserialize($data['ParsedVars']);
			}

			return false;
		}

		function _setCachedUrl($url, $data)
		{
			if (!$url) {
				return ;
			}

			$vars = $data['vars'];
			$passed = $data['passed'];
			sort($passed);

			// get expiration
			if ($vars['m_cat_id'] > 0) {
				$sql = 'SELECT PageExpiration
						FROM ' . TABLE_PREFIX . 'Category
						WHERE CategoryId = ' . $vars['m_cat_id'];
				$expiration = $this->Conn->GetOne($sql);
			}

			// get prefixes
			$prefixes = Array ();
			$m_index = array_search('m', $passed);

			if ($m_index !== false) {
				unset($passed[$m_index]);

				if ($vars['m_cat_id'] > 0) {
					$prefixes[] = 'c:' . $vars['m_cat_id'];
				}

				$prefixes[] = 'lang:' . $vars['m_lang'];
				$prefixes[] = 'theme:' . $vars['m_theme'];
			}

			foreach ($passed as $prefix) {
				if (array_key_exists($prefix . '_id', $vars) && is_numeric($vars[$prefix . '_id'])) {
					$prefixes[] = $prefix . ':' . $vars[$prefix . '_id'];
				}
				else {
					$prefixes[] = $prefix;
				}
			}

			$fields_hash = Array (
				'Url' => $url,
				'Hash' => crc32($url),
				'DomainId' => (int)$this->Application->siteDomainField('DomainId'),
				'Prefixes' => $prefixes ? '|' . implode('|', $prefixes) . '|' : '',
				'ParsedVars' => serialize($data),
				'Cached' => adodb_mktime(),
				'LifeTime' => isset($expiration) && is_numeric($expiration) ? $expiration : -1
			);

			$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'CachedUrls');
		}

		function parseRewriteURL($url)
		{
			$vars = Array ('pass' => Array ('m'));
			$url_parts = $url ? explode('/', trim(mb_strtolower($url, 'UTF-8'), '/')) : Array ();

			$this->_partsToParse = $url_parts;

			if ( ($this->HTTPQuery->Get('rewrite') == 'on') || !$url_parts ) {
				$this->_setDefaultValues($vars);
			}

			if ( !$url_parts ) {
				$this->InitAll();
				$vars['t'] = $this->HTTPQuery->getDefaultTemplate('');

				return $vars;
			}
			else {
				$vars['t'] = '';
			}

			$this->_parseLanguage($url_parts, $vars);
			$this->_parseTheme($url_parts, $vars);

			// http://site-url/<language>/<theme>/<category>[_<category_page>]/<template>/<module_page>
			// http://site-url/<language>/<theme>/<category>[_<category_page>]/<module_page> (category-based section template)
			// http://site-url/<language>/<theme>/<category>[_<category_page>]/<template>/<module_item>
			// http://site-url/<language>/<theme>/<category>[_<category_page>]/<module_item> (category-based detail template)
			// http://site-url/<language>/<theme>/<rl_injections>/<category>[_<category_page>]/<rl_part> (customized url)

			if ( $this->processRewriteListeners($url_parts, $vars) ) {
				return $vars;
			}

			$this->_parsePhisycalTemplate($url_parts, $vars);

			if ( ($this->_modulePrefix === false) && $vars['m_cat_id'] && !$this->_partsToParse ) {
				// no category item found, but category found and all url matched -> module index page

				return $vars;
			}

			if ( $this->_partsToParse ) {
				$not_found = $this->Application->ConfigValue('ErrorTemplate');
				$vars['t'] = $not_found ? $not_found : 'error_notfound';

				$themes_helper =& $this->Application->recallObject('ThemesHelper');
				/* @var $themes_helper kThemesHelper */

				$vars['m_cat_id'] = $themes_helper->getPageByTemplate($vars['t'], $vars['m_theme']);

				header('HTTP/1.0 404 Not Found');
			}

			return $vars;
		}

		function InitAll()
		{
			$this->Application->VerifyThemeId();
			$this->Application->VerifyLanguageId();
			$this->Application->Phrases->Init('phrases');
		}

		/**
		 * Processes url using rewrite listeners
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		function processRewriteListeners(&$url_parts, &$vars)
		{
			$this->initRewriteListeners();
			$page_number = $this->_parsePage($url_parts, $vars);

			foreach ($this->Application->RewriteListeners as $prefix => $listeners) {
				// set default page
				// $vars[$prefix . '_Page'] = 1; // will override page in session in case, when none is given in url

				if ($page_number) {
					// page given in url - use it
					$vars[$prefix . '_id'] = 0;
					$vars[$prefix . '_Page'] = $page_number;
				}

				// $listeners[1] - listener, used for parsing
				$listener_result = $listeners[1][0]->$listeners[1][1](REWRITE_MODE_PARSE, $prefix, $vars, $url_parts);
				if ($listener_result === false) {
					// will not proceed to other methods
					return true;
				}
			}

			// will proceed to other methods
			return false;
		}

		/**
		 * Parses real template name from url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		function _parsePhisycalTemplate($url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			do {
				$template_path = implode('/', $url_parts);

				$physical_template = array_search($template_path, $this->Application->structureTemplateMapping);

				if (($physical_template !== false) && (substr($physical_template, 0, 3) != 'id:')) {
					// replace menu template name with it's actual template name on disk
					list ($template_path) = explode(':', $physical_template, 2);
				}

				$t_parts['path'] = dirname($template_path) == '.' ? '' : '/' . dirname($template_path);
				$t_parts['file'] = basename($template_path);

				$sql = 'SELECT FileId
						FROM ' . TABLE_PREFIX . 'ThemeFiles
						WHERE (ThemeId = ' . $vars['m_theme'] . ') AND (FilePath = ' . $this->Conn->qstr($t_parts['path']) . ') AND (FileName = ' . $this->Conn->qstr($t_parts['file'] . '.tpl') . ')';
				$template_found = $this->Conn->GetOne($sql);

				if (!$template_found) {
					array_shift($url_parts);
				}
			} while (!$template_found && $url_parts);

			if ($template_found) {
				$vars['t'] = $template_path;

				$template_parts = explode('/', $template_path);

				while ( $template_parts ) {
					$this->partParsed( array_pop($template_parts), 'rtl' );
				}

				// 1. will damage actual category during category item review add process
				// 2. will use "use_section" parameter of "m_Link" tag to gain same effect
//				$themes_helper =& $this->Application->recallObject('ThemesHelper');
//				/* @var $themes_helper kThemesHelper */
//
//				$vars['m_cat_id'] = $themes_helper->getPageByTemplate($template_path, $vars['m_theme']);

				return true;
			}

			return false;
		}

		/**
		 * 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.
		 */
		function MainRewriteListener($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
		 */
		function _buildMainUrl($prefix_special, &$params, $keep_events)
		{
			$ret = '';
			list ($prefix) = explode('.', $prefix_special);

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

			// add language
			if (!$this->primaryLanguageId) {
				// when domain-based language not found -> use site-wide language
				$this->primaryLanguageId = $this->Application->GetDefaultLanguageId();
			}

			if ($processed_params['m_lang'] && ($processed_params['m_lang'] != $this->primaryLanguageId)) {
				$language_name = $this->Application->getCache('language_names[%LangIDSerial:' . $processed_params['m_lang'] . '%]');
				if ($language_name === false) {
					$sql = 'SELECT PackName
							FROM ' . TABLE_PREFIX . 'Language
							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 (!$this->primaryThemeId) {
				// when domain-based theme not found -> use site-wide theme
				$this->primaryThemeId = $this->Application->GetDefaultThemeId(true);
			}

			if ($processed_params['m_theme'] && ($processed_params['m_theme'] != $this->primaryThemeId)) {
				$theme_name = $this->Application->getCache('theme_names[%ThemeIDSerial:' . $processed_params['m_theme'] . '%]');
				if ($theme_name === false) {
					$sql = 'SELECT Name
							FROM ' . TABLE_PREFIX . 'Theme
							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 OR site homepage
			if (($template == $category_template) || (mb_strtolower($template) == '__default__') || ($template == 'index')) {
				// given template is also default template for this category OR '__default__' given OR site homepage
				$params['pass_template'] = false;
			}

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

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

		/**
		 * Gets language part from url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		function _parseLanguage(&$url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$url_part = reset($url_parts);

			$sql = 'SELECT LanguageId, IF(LOWER(PackName) = ' . $this->Conn->qstr($url_part) . ', 2, PrimaryLang) AS SortKey
					FROM ' . TABLE_PREFIX . 'Language
					WHERE Enabled = 1
					ORDER BY SortKey DESC';
			$language_info = $this->Conn->GetRow($sql);

			if ($language_info && $language_info['LanguageId'] && $language_info['SortKey']) {
				// primary language will be selected in case, when $url_part doesn't match to other's language pack name
				// don't use next enabled language, when primary language is disabled
				$vars['m_lang'] = $language_info['LanguageId'];

				if ($language_info['SortKey'] == 2) {
					// language was found by pack name
					array_shift($url_parts);
					$this->partParsed($url_part);
				}
				elseif ($this->primaryLanguageId) {
					// use domain-based primary language instead of site-wide primary language
					$vars['m_lang'] = $this->primaryLanguageId;
				}

				return true;
			}

			return false;
		}

		/**
		 * Gets theme part from url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		function _parseTheme(&$url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$url_part = reset($url_parts);

			$sql = 'SELECT ThemeId, IF(LOWER(Name) = ' . $this->Conn->qstr($url_part) . ', 2, PrimaryTheme) AS SortKey, TemplateAliases
					FROM ' . TABLE_PREFIX . 'Theme
					WHERE Enabled = 1
					ORDER BY SortKey DESC';
			$theme_info = $this->Conn->GetRow($sql);

			if ($theme_info && $theme_info['ThemeId'] && $theme_info['SortKey']) {
				// primary theme will be selected in case, when $url_part doesn't match to other's theme name
				// don't use next enabled theme, when primary theme is disabled
				$vars['m_theme'] = $theme_info['ThemeId'];

				if ($theme_info['TemplateAliases']) {
					$this->_templateAliases = unserialize($theme_info['TemplateAliases']);
				}
				else {
					$this->_templateAliases = Array ();
				}

				if ($theme_info['SortKey'] == 2) {
					// theme was found by name
					array_shift($url_parts);
					$this->partParsed($url_part);
				}
				elseif ($this->primaryThemeId) {
					// use domain-based primary theme instead of site-wide primary theme
					$vars['m_theme'] = $this->primaryThemeId;
				}

				return true;
			}

			$vars['m_theme'] = 0; // required, because used later for category/template detection

			return false;
		}

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

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

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

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

				return true;
			}

			return false;
		}

		/**
		 * Set's page (when found) to all modules
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return string
		 *
		 * @todo Should find a way, how to determine what rewrite listerner page is it
		 */
		function _parsePage(&$url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$page_number = end($url_parts);
			if (!is_numeric($page_number)) {
				return false;
			}

			array_pop($url_parts);
			$this->partParsed($page_number, 'rtl');

			return $page_number;
		}

		/**
		 * Remove page numbers for all rewrite listeners
		 *
		 * @todo Should find a way, how to determine what rewrite listerner page is it
		 */
		function removePages()
		{
			/*foreach ($this->Application->RewriteListeners as $prefix => $listener) {
				$this->Application->DeleteVar($prefix . '_Page');
			}*/
		}

		/**
		 * Extracts category part from url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool
		 */
		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';

			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 . 'Category
						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;
					$this->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 . 'Category
							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']), 'UTF-8' );

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

		/**
		 * Builds/parses category item 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 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.
		 */
		function CategoryItemRewriteListener($rewrite_mode = REWRITE_MODE_BUILD, $prefix, &$params, &$url_parts, $keep_events = false)
		{
			static $parsed = false;

			if ($rewrite_mode == REWRITE_MODE_BUILD) {
				return $this->_buildCategoryItemUrl($prefix, $params, $keep_events);
			}

			if (!$parsed) {
				$this->_modulePrefix = $this->_parseCategoryItemUrl($url_parts, $params);

				if ($this->_modulePrefix !== false) {
					$params['pass'][] = $this->_modulePrefix;
				}

				$parsed = true;
			}

			return true;
		}

		/**
		 * Build category teim part of url
		 *
		 * @param string $prefix_special
		 * @param Array $params
		 * @param bool $keep_events
		 * @return string
		 */
		function _buildCategoryItemUrl($prefix_special, &$params, $keep_events)
		{
			static $default_per_page = Array ();

			$ret = '';
			list ($prefix) = explode('.', $prefix_special);
			$processed_params = $this->getProcessedParams($prefix_special, $params, $keep_events);

			if ($processed_params === false) {
				return '';
			}

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

				$default_per_page[$prefix] = $list_helper->getDefaultPerPage($prefix);
			}

			if ($processed_params[$prefix_special . '_id']) {
				$category_id = array_key_exists('m_cat_id', $params) ? $params['m_cat_id'] : $this->Application->GetVar('m_cat_id');

				// if template is also item template of category, then remove template
				$template = array_key_exists('t', $params) ? $params['t'] : false;
				$item_template = $this->GetItemTemplate($category_id, $prefix);

				if ($template == $item_template || strtolower($template) == '__default__') {
					// given template is also default template for this category item or '__default__' given
					$params['pass_template'] = false;
				}

				// get item's filename
				if ($prefix == 'bb') {
					$ret .= 'bb_' . $processed_params[$prefix_special . '_id'] . '/';
				}
				else {
					$filename = $this->Application->getFilename($prefix, $processed_params[$prefix_special . '_id'], $category_id);
					if ($filename !== false) {
						$ret .= $filename . '/';
					}
				}
			} else {
				if ($processed_params[$prefix_special . '_Page'] == 1) {
					// when printing category items and we are on the 1st page -> there is no information about
					// category item prefix and $params['pass_category'] will not be added automatically
					$params['pass_category'] = true;
				}
				elseif ($processed_params[$prefix_special . '_Page'] > 1) {
					// $ret .= $processed_params[$prefix_special . '_Page'] . '/';
					$params['page'] = $processed_params[$prefix_special . '_Page'];
				}

				$per_page = $processed_params[$prefix_special . '_PerPage'];

				if ($per_page && ($per_page != $default_per_page[$prefix])) {
					$params['per_page'] = $processed_params[$prefix_special . '_PerPage'];
				}
			}

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

		/**
		 * Sets template and id, corresponding to category item given in url
		 *
		 * @param Array $url_parts
		 * @param Array $vars
		 * @return bool|string
		 */
		function _parseCategoryItemUrl(&$url_parts, &$vars)
		{
			if (!$url_parts) {
				return false;
			}

			$item_filename = end($url_parts);
			if (is_numeric($item_filename)) {
				// this page, don't process here
				return false;
			}

			if (preg_match('/^bb_([\d]+)/', $item_filename, $regs)) {
				// process topics separatly, because they don't use item filenames
				array_pop($url_parts);
				$this->partParsed($item_filename, 'rtl');

				return $this->_parseTopicUrl($regs[1], $vars);
			}

			// locating the item in CategoryItems by filename to detect its ItemPrefix and its category ParentPath
			$sql = 'SELECT ci.ItemResourceId, ci.ItemPrefix, c.ParentPath, ci.CategoryId
					FROM ' . TABLE_PREFIX . 'CategoryItems AS ci
					LEFT JOIN ' . TABLE_PREFIX . 'Category AS c ON c.CategoryId = ci.CategoryId
					WHERE (ci.CategoryId = ' . (int)$vars['m_cat_id'] . ') AND (ci.Filename = ' . $this->Conn->qstr($item_filename) . ')';
			$cat_item = $this->Conn->GetRow($sql);

			if ($cat_item !== false) {
				// item found
				$module_prefix = $cat_item['ItemPrefix'];
				$item_template = $this->GetItemTemplate($cat_item, $module_prefix);

				// converting ResourceId to correpsonding Item id
				$module_config = $this->Application->getUnitOptions($module_prefix);

				$sql = 'SELECT ' . $module_config['IDField'] . '
						FROM ' . $module_config['TableName'] . '
					 	WHERE ResourceId = ' . $cat_item['ItemResourceId'];
				$item_id = $this->Conn->GetOne($sql);

				array_pop($url_parts);

				if ($item_id) {
					$this->partParsed($item_filename, 'rtl');

					if ($item_template) {
						// when template is found in category -> set it
						$vars['t'] = $item_template;
					}

					// we have category item id
					$vars[$module_prefix . '_id'] = $item_id;

					return $module_prefix;
				}
			}

			return false;
		}

		/**
		 * Set's template and topic id corresponding to topic given in url
		 *
		 * @param int $topic_id
		 * @param Array $vars
		 * @return string
		 */
		function _parseTopicUrl($topic_id, &$vars)
		{
			$sql = 'SELECT c.ParentPath, c.CategoryId
					FROM ' . TABLE_PREFIX . 'Category AS c
					WHERE c.CategoryId = ' . (int)$vars['m_cat_id'];
			$cat_item = $this->Conn->GetRow($sql);

			$item_template = $this->GetItemTemplate($cat_item, 'bb');

			if ($item_template) {
				$vars['t'] = $item_template;
			}

			$vars['bb_id'] = $topic_id;

			return 'bb';
		}

		/**
		 * Returns enviroment variable values for given prefix (uses directly given params, when available)
		 *
		 * @param string $prefix_special
		 * @param Array $params
		 * @param bool $keep_events
		 * @return Array
		 */
		function getProcessedParams($prefix_special, &$params, $keep_events)
		{
			list ($prefix) = explode('.', $prefix_special);

			$query_vars = $this->Application->getUnitOption($prefix, 'QueryString');
			if (!$query_vars) {
				// given prefix doesn't use "env" variable to pass it's data
				return false;
			}

			$event_key = array_search('event', $query_vars);
			if ($event_key) {
				// pass through event of this prefix
				unset($query_vars[$event_key]);
			}

			if (array_key_exists($prefix_special . '_event', $params) && !$params[$prefix_special . '_event']) {
				// if empty event, then remove it from url
				unset( $params[$prefix_special . '_event'] );
			}

			// if pass events is off and event is not implicity passed
			if (!$keep_events && !array_key_exists($prefix_special . '_event', $params)) {
				unset($params[$prefix_special . '_event']); // remove event from url if requested
				//otherwise it will use value from get_var
			}

			$processed_params = Array ();
			foreach ($query_vars as $index => $var_name) {
				// if value passed in params use it, otherwise use current from application
				$var_name = $prefix_special . '_' . $var_name;
				$processed_params[$var_name] = array_key_exists($var_name, $params) ? $params[$var_name] : $this->Application->GetVar($var_name);
				if (array_key_exists($var_name, $params)) {
					unset($params[$var_name]);
				}
			}

			return $processed_params;
		}

		/**
		 * Returns module item details template specified in given category custom field for given module prefix
		 *
		 * @param int|Array $category
		 * @param string $module_prefix
		 * @return string
		 */
		function GetItemTemplate($category, $module_prefix)
		{
			$category_id = is_array($category) ? $category['CategoryId'] : $category;
			$cache_key = __CLASS__ . '::' . __FUNCTION__ . '[%CIDSerial:' . $category_id . '%]:' . $module_prefix;

			$cached_value = $this->Application->getCache($cache_key);
			if ($cached_value !== false) {
				return $cached_value;
			}

			if (!is_array($category)) {
				if ($category == 0) {
					$category = $this->Application->findModule('Var', $module_prefix, 'RootCat');
				}
				$sql = 'SELECT c.ParentPath, c.CategoryId
						FROM ' . TABLE_PREFIX . 'Category AS c
						WHERE c.CategoryId = ' . $category;
				$category = $this->Conn->GetRow($sql);
			}
			$parent_path = implode(',',explode('|', substr($category['ParentPath'], 1, -1)));

			// item template is stored in module' system custom field - need to get that field Id
			$primary_lang = $this->Application->GetDefaultLanguageId();
			$item_template_field_id = $this->getItemTemplateCustomField($module_prefix);

			// looking for item template through cats hierarchy sorted by parent path
			$query = '	SELECT ccd.l' . $primary_lang . '_cust_' . $item_template_field_id . ',
									FIND_IN_SET(c.CategoryId, ' . $this->Conn->qstr($parent_path) . ') AS Ord1,
									c.CategoryId, c.Name, ccd.l' . $primary_lang . '_cust_' . $item_template_field_id . '
						FROM ' . TABLE_PREFIX . 'Category AS c
						LEFT JOIN ' . TABLE_PREFIX . 'CategoryCustomData AS ccd
						ON ccd.ResourceId = c.ResourceId
						WHERE c.CategoryId IN (' . $parent_path . ') AND ccd.l' . $primary_lang . '_cust_' . $item_template_field_id . ' != \'\'
						ORDER BY FIND_IN_SET(c.CategoryId, ' . $this->Conn->qstr($parent_path) . ') DESC';
			$item_template = $this->Conn->GetOne($query);

			if (!isset($this->_templateAliases)) {
				// when empty url OR mod-rewrite disabled

				$themes_helper =& $this->Application->recallObject('ThemesHelper');
				/* @var $themes_helper kThemesHelper */

				$sql = 'SELECT TemplateAliases
						FROM ' . TABLE_PREFIX . 'Theme
						WHERE ThemeId = ' . (int)$themes_helper->getCurrentThemeId();
				$template_aliases = $this->Conn->GetOne($sql);

				$this->_templateAliases = $template_aliases ? unserialize($template_aliases) : Array ();
			}

			if ($item_template && array_key_exists($item_template, $this->_templateAliases)) {
				$item_template = $this->_templateAliases[$item_template];
			}

			$this->Application->setCache($cache_key, $item_template);

			return $item_template;
		}

		/**
		 * Loads all registered rewrite listeners, so they could be quickly accessed later
		 *
		 */
		function initRewriteListeners()
		{
			static $init_done = false;

			if ($init_done || count($this->Application->RewriteListeners) == 0) {
				// not inited OR mod-rewrite url with missing config cache
				return ;
			}

			foreach ($this->Application->RewriteListeners as $prefix => $listener_data) {
				foreach ($listener_data['listener'] as $index => $rewrite_listener) {
					list ($listener_prefix, $listener_method) = explode(':', $rewrite_listener);
					$listener =& $this->Application->recallObject($listener_prefix);

					$this->Application->RewriteListeners[$prefix][$index] = Array (&$listener, $listener_method);
				}
			}

			define('MOD_REWRITE_URL_ENDING', $this->Application->ConfigValue('ModRewriteUrlEnding'));

			$init_done = true;
		}

		/**
		 * Returns category custom field id, where given module prefix item template name is stored
		 *
		 * @param string $module_prefix
		 * @return int
		 */
		function getItemTemplateCustomField($module_prefix)
		{
			$cache_key = __CLASS__ . '::' . __FUNCTION__ . '[%CfSerial%]:' . $module_prefix;
			$cached_value = $this->Application->getCache($cache_key);

			if ($cached_value !== false) {
				return $cached_value;
			}

			$sql = 'SELECT CustomFieldId
				 	FROM ' . TABLE_PREFIX . 'CustomField
				 	WHERE FieldName = ' . $this->Conn->qstr($module_prefix . '_ItemTemplate');
			$item_template_field_id = $this->Conn->GetOne($sql);

			$this->Application->setCache($cache_key, $item_template_field_id);

			return $item_template_field_id;
		}

		/**
		 * Sets default parsed values before actual url parsing (only, for empty url)
		 *
		 * @param Array $vars
		 */
		function _setDefaultValues(&$vars)
		{
			$defaults = Array (
				'm_cat_id' => 0, // no category
				'm_cat_page' => 1, // first category page
				'm_opener' => 's', // stay on same page
				't' => 'index' // main site page
			);

			if ($this->primaryLanguageId) {
				// domain-based primary language
				$defaults['m_lang'] = $this->primaryLanguageId;
			}

			if ($this->primaryThemeId) {
				// domain-based primary theme
				$defaults['m_theme'] = $this->primaryThemeId;
			}

			foreach ($defaults as $default_key => $default_value) {
				if ($this->HTTPQuery->Get($default_key) === false) {
					$vars[$default_key] = $default_value;
				}
			}
		}

		/**
		 * Marks url part as parsed
		 *
		 * @param string $url_part
		 * @param string $parse_direction
		 */
		function partParsed($url_part, $parse_direction = 'ltr')
		{
			if ( !$this->_partsToParse ) {
				return ;
			}

			if ( $parse_direction == 'ltr' ) {
				$expected_url_part = reset($this->_partsToParse);

				if ( $url_part == $expected_url_part ) {
					array_shift($this->_partsToParse);
				}
			}
			else {
				$expected_url_part = end($this->_partsToParse);

				if ( $url_part == $expected_url_part ) {
					array_pop($this->_partsToParse);
				}
			}

			if ( $url_part != $expected_url_part ) {
				trigger_error('partParsed: expected URL part "<strong>' . $expected_url_part . '</strong>", received URL part "<strong>' . $url_part . '</strong>"', E_USER_NOTICE);
			}
		}
	}
