<?php
/**
* @version	$Id: rewrite_url_processor.php 15544 2012-09-27 09:23:27Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2011 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 kRewriteUrlProcessor extends kUrlProcessor {

	/**
	 * Holds a reference to httpquery
	 *
	 * @var kHttpQuery
	 * @access protected
	 */
	protected $HTTPQuery = null;

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

	/**
	 * Category item prefix, that was found
	 *
	 * @var string|bool
	 * @access public
	 */
	public $modulePrefix = false;

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

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

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

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

	/**
	 * Factory storage sub-set, containing mod-rewrite listeners, used during url building and parsing
	 *
	 * @var Array
	 * @access protected
	 */
	protected $rewriteListeners = Array ();

	/**
	 * Constructor of kRewriteUrlProcessor class
	 *
	 * @param $manager
	 * @return kRewriteUrlProcessor
	 */
	public function __construct(&$manager)
	{
		parent::__construct($manager);

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

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

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

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

		if (!$this->primaryThemeId) {
			// when domain-based theme not found -> use site-wide theme
			$this->primaryThemeId = $this->Application->GetDefaultThemeId(true);
		}

		$this->_initRewriteListeners();
	}

	/**
	 * Parses url
	 *
	 * @return void
	 */
	public function parseRewriteURL()
	{
		$url = $this->Application->GetVar('_mod_rw_url_');

		if ( $url ) {
			$this->_redirectToDefaultUrlEnding($url);
			$url = $this->_removeUrlEnding($url);
		}

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

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

			if ( !$this->_partsToParse ) {
				// don't cache 404 Not Found
				$this->_setCachedUrl($url, Array ('vars' => $vars, 'passed' => $passed));
			}

			if ( $this->Application->GetVarDirect('t', 'Post') ) {
				// template from POST overrides template from URL.
				$vars['t'] = $this->Application->GetVarDirect('t', 'Post');

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

	/**
	 * Detects url ending of given url
	 *
	 * @param string $url
	 * @return string
	 * @access protected
	 */
	protected function _findUrlEnding($url)
	{
		if ( !$url ) {
			return '';
		}

		foreach ($this->_urlEndings as $url_ending) {
			if ( mb_substr($url, mb_strlen($url) - mb_strlen($url_ending)) == $url_ending ) {
				return $url_ending;
			}
		}

		return '';
	}

	/**
	 * Removes url ending from url
	 *
	 * @param string $url
	 * @return string
	 * @access protected
	 */
	protected function _removeUrlEnding($url)
	{
		$url_ending = $this->_findUrlEnding($url);

		if ( !$url_ending ) {
			return $url;
		}

		return mb_substr($url, 0, mb_strlen($url) - mb_strlen($url_ending));
	}

	/**
	 * Redirects user to page with default url ending, where needed
	 *
	 * @param string $url
	 * @return void
	 * @access protected
	 */
	protected function _redirectToDefaultUrlEnding($url)
	{
		$default_ending = $this->Application->ConfigValue('ModRewriteUrlEnding');

		if ( $this->_findUrlEnding($url) == $default_ending || !$this->Application->ConfigValue('ForceModRewriteUrlEnding') ) {
			return;
		}

		// user manually typed url with different url ending -> redirect to same url with default url ending
		$target_url = $this->Application->BaseURL() . $this->_removeUrlEnding($url) . $default_ending;

		trigger_error('Mod-rewrite url "<strong>' . $_SERVER['REQUEST_URI'] . '</strong>" without "<strong>' . $default_ending . '</strong>" line ending used', E_USER_NOTICE);
		$this->Application->Redirect('external:' . $target_url, Array ('response_code' => 301));
	}

	/**
	 * Returns url parsing result from cache or false, when not yet parsed
	 *
	 * @param $url
	 * @return Array|bool
	 * @access protected
	 */
	protected function _getCachedUrl($url)
	{
		if ( !$url || (defined('DBG_CACHE_URLS') && !DBG_CACHE_URLS) ) {
			return false;
		}

		$sql = 'SELECT *
				FROM ' . TABLE_PREFIX . 'CachedUrls
				WHERE Hash = ' . kUtil::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 < TIMENOW) ) {
				// delete expired
				$sql = 'DELETE FROM ' . TABLE_PREFIX . 'CachedUrls
						WHERE UrlId = ' . $data['UrlId'];
				$this->Conn->Query($sql);

				return false;
			}

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

		return false;
	}

	/**
	 * Caches url
	 *
	 * @param string $url
	 * @param Array $data
	 * @return void
	 * @access protected
	 */
	protected function _setCachedUrl($url, $data)
	{
		if ( !$url || (defined('DBG_CACHE_URLS') && !DBG_CACHE_URLS) ) {
			return;
		}

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

		// get expiration
		if ( $vars['m_cat_id'] > 0 ) {
			$sql = 'SELECT PageExpiration
					FROM ' . TABLE_PREFIX . 'Categories
					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' => kUtil::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');
	}

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

		if ($init_done || count($this->Application->RewriteListeners) == 0) {
			// not initialized 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);

				// don't use temp variable, since it will swap objects in Factory in PHP5
				$this->rewriteListeners[$prefix][$index] = Array ();
				$this->rewriteListeners[$prefix][$index][0] = $this->Application->recallObject($listener_prefix);
				$this->rewriteListeners[$prefix][$index][1] = $listener_method;
			}
		}

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

		$init_done = true;
	}

	/**
	 * Parses given string into a set of variables (url in this case)
	 *
	 * @param string $string
	 * @param string $pass_name
	 * @return Array
	 * @access public
	 */
	public function parse($string, $pass_name = 'pass')
	{
		// external url (could be back this website as well)
		if ( preg_match('/external:(.*)/', $string, $regs) ) {
			$string = $regs[1];
		}

		$vars = Array ();
		$url_components = parse_url($string);

		if ( isset($url_components['query']) ) {
			parse_str(html_entity_decode($url_components['query']), $url_params);

			if ( isset($url_params[ENV_VAR_NAME]) ) {
				$url_params = array_merge($url_params, $this->manager->plain->parse($url_params[ENV_VAR_NAME], $pass_name));
				unset($url_params[ENV_VAR_NAME]);
			}

			$vars = array_merge($vars, $url_params);
		}

		$this->_fixPass($vars, $pass_name);

		if ( isset($url_components['path']) ) {
			if ( BASE_PATH ) {
				$string = preg_replace('/^' . preg_quote(BASE_PATH, '/') . '/', '', $url_components['path'], 1);
			}
			else {
				$string = $url_components['path'];
			}

			$string = $this->_removeUrlEnding(trim($string, '/'));
		}
		else {
			$string = '';
		}

		$url_parts = $string ? explode('/', mb_strtolower($string)) : Array ();

		$this->_partsToParse = $url_parts;

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

		if ( !$url_parts ) {
			$this->_initAll();
			$vars['t'] = $this->Application->UrlManager->getTemplateName();

			return $vars;
		}

		$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->_parsePhysicalTemplate($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 ) {
			$vars = array_merge($vars, $this->manager->prepare404($vars['m_theme']));
		}

		return $vars;
	}

	/**
	 * Ensures, that "m" is always in "pass" variable
	 *
	 * @param Array $vars
	 * @param string $pass_name
	 * @return void
	 * @access protected
	 */
	protected function _fixPass(&$vars, $pass_name)
	{
		if ( isset($vars[$pass_name]) ) {
			$vars[$pass_name] = array_unique(explode(',', 'm,' . $vars[$pass_name]));
		}
		else {
			$vars[$pass_name] = Array ('m');
		}
	}

	/**
	 * Initializes theme & language based on parse results
	 *
	 * @return void
	 * @access protected
	 */
	protected function _initAll()
	{
		$this->Application->VerifyThemeId();
		$this->Application->VerifyLanguageId();

		// no need, since we don't have any cached phrase IDs + nobody will use PhrasesCache::LanguageId soon
		// $this->Application->Phrases->Init('phrases');
	}

	/**
	 * Sets default parsed values before actual url parsing (only, for empty url)
	 *
	 * @param Array $vars
	 * @access protected
	 */
	protected 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;
			}
		}
	}

	/**
	 * Processes url using rewrite listeners
	 *
	 * Pattern: Chain of Command
	 *
	 * @param Array $url_parts
	 * @param Array $vars
	 * @return bool
	 * @access protected
	 */
	protected function _processRewriteListeners(&$url_parts, &$vars)
	{
		$this->_initRewriteListeners();
		$page_number = $this->_parsePage($url_parts, $vars);

		foreach ($this->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;
	}

	/**
	 * Set's page (when found) to all modules
	 *
	 * @param Array $url_parts
	 * @param Array $vars
	 * @return string
	 * @access protected
	 *
	 * @todo Should find a way, how to determine what rewrite listener page is it
	 */
	protected 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;
	}

	/**
	 * Gets language part from url
	 *
	 * @param Array $url_parts
	 * @param Array $vars
	 * @return bool
	 * @access protected
	 */
	protected 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 . 'Languages
				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
	 */
	protected 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 . 'Themes
				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;
	}

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

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

		do {
			$index_added = false;
			$template_path = implode('/', $url_parts);
			$template_found = $themes_helper->getTemplateId($template_path, $vars['m_theme']);

			if ( !$template_found ) {
				$index_added = true;
				$template_found = $themes_helper->getTemplateId($template_path . '/index', $vars['m_theme']);
			}

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

		if ( $template_found ) {
			$template_parts = explode('/', $template_path);
			$vars['t'] = $template_path . ($index_added ? '/index' : '');

			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
//			$vars['m_cat_id'] = $themes_helper->getPageByTemplate($template_path, $vars['m_theme']);

			return true;
		}

		return false;
	}

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

		$query_vars = $this->Application->getUnitOption($prefix, 'QueryString', Array ());
		/* @var $query_vars Array */

		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 $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
	 * @param int $theme_id
	 * @return string
	 * @access public
	 * @todo Move to kPlainUrlProcessor
	 */
	public function GetItemTemplate($category, $module_prefix, $theme_id = null)
	{
		if ( !isset($theme_id) ) {
			$theme_id = $this->Application->GetVar('m_theme');
		}

		$category_id = is_array($category) ? $category['CategoryId'] : $category;
		$cache_key = __CLASS__ . '::' . __FUNCTION__ . '[%CIDSerial:' . $category_id . '%][%ThemeIDSerial:' . $theme_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 . 'Categories 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 . 'Categories 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 . 'Themes
					WHERE ThemeId = ' . (int)$themes_helper->getCurrentThemeId();
			$template_aliases = $this->Conn->GetOne($sql);

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

		if ( substr($item_template, 0, 1) == '#' ) {
			// it's template alias + "#" isn't allowed in filenames
			$item_template = (string)getArrayValue($this->_templateAliases, $item_template);
		}

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

		return $item_template;
	}

	/**
	 * Returns category custom field id, where given module prefix item template name is stored
	 *
	 * @param string $module_prefix
	 * @return int
	 * @access public
	 * @todo Move to kPlainUrlProcessor; decrease visibility, since used only during upgrade
	 */
	public 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 . 'CustomFields
				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;
	}

	/**
	 * Marks url part as parsed
	 *
	 * @param string $url_part
	 * @param string $parse_direction
	 * @access public
	 */
	public 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);
		}
	}

	/**
	 * Determines if there is more to parse in url
	 *
	 * @return bool
	 * @access public
	 */
	public function moreToParse()
	{
		return count($this->_partsToParse) > 0;
	}

	/**
	 * Builds url
	 *
	 * @param string $t
	 * @param Array $params
	 * @param string $pass
	 * @param bool $pass_events
	 * @param bool $env_var
	 * @return string
	 * @access public
	 */
	public function build($t, $params, $pass = 'all', $pass_events = false, $env_var = false)
	{
		if ( $this->Application->GetVar('admin') || (array_key_exists('admin', $params) && $params['admin']) ) {
			$params['admin'] = 1;

			if ( !array_key_exists('editing_mode', $params) ) {
				$params['editing_mode'] = EDITING_MODE;
			}
		}

		$ret = '';
		$env = '';

		$encode = false;

		if ( isset($params['__URLENCODE__']) ) {
			$encode = $params['__URLENCODE__'];
			unset($params['__URLENCODE__']);
		}

		if ( isset($params['__SSL__']) ) {
			unset($params['__SSL__']);
		}

		$catalog_item_found = false;
		$pass_info = $this->getPassInfo($pass);

		if ( $pass_info ) {
			if ( $pass_info[0] == 'm' ) {
				array_shift($pass_info);
			}

			$inject_parts = Array (); // url parts for beginning of url
			$params['t'] = $t; // make template available for rewrite listeners
			$params['pass_template'] = true; // by default we keep given template in resulting url

			if ( !array_key_exists('pass_category', $params) ) {
				$params['pass_category'] = false; // by default we don't keep categories in url
			}

			foreach ($pass_info as $pass_index => $pass_element) {
				list ($prefix) = explode('.', $pass_element);
				$catalog_item = $this->Application->findModule('Var', $prefix) && $this->Application->getUnitOption($prefix, 'CatalogItem');

				if ( array_key_exists($prefix, $this->rewriteListeners) ) {
					// if next prefix is same as current, but with special => exclude current prefix from url
					$next_prefix = array_key_exists($pass_index + 1, $pass_info) ? $pass_info[$pass_index + 1] : false;
					if ( $next_prefix ) {
						$next_prefix = substr($next_prefix, 0, strlen($prefix) + 1);
						if ( $prefix . '.' == $next_prefix ) {
							continue;
						}
					}

					// rewritten url part
					$url_part = $this->BuildModuleEnv($pass_element, $params, $pass_events);

					if ( is_string($url_part) && $url_part ) {
						$ret .= $url_part . '/';

						if ( $catalog_item ) {
							// pass category later only for catalog items
							$catalog_item_found = true;
						}
					}
					elseif ( is_array($url_part) ) {
						// rewrite listener want to insert something at the beginning of url too
						if ( $url_part[0] ) {
							$inject_parts[] = $url_part[0];
						}

						if ( $url_part[1] ) {
							$ret .= $url_part[1] . '/';
						}

						if ( $catalog_item ) {
							// pass category later only for catalog items
							$catalog_item_found = true;
						}
					}
					elseif ( $url_part === false ) {
						// rewrite listener decided not to rewrite given $pass_element
						$env .= ':' . $this->manager->plain->BuildModuleEnv($pass_element, $params, $pass_events);
					}
				}
				else {
					$env .= ':' . $this->manager->plain->BuildModuleEnv($pass_element, $params, $pass_events);
				}
			}

			if ( $catalog_item_found || preg_match('/c\.[-\d]*/', implode(',', $pass_info)) ) {
				// "c" prefix is present -> keep category
				$params['pass_category'] = true;
			}

			$params['inject_parts'] = $inject_parts;

			$ret = $this->BuildModuleEnv('m', $params, $pass_events) . '/' . $ret;
			$cat_processed = array_key_exists('category_processed', $params) && $params['category_processed'];

			// remove temporary parameters used by listeners
			unset($params['t'], $params['inject_parts'], $params['pass_template'], $params['pass_category'], $params['category_processed']);

			$ret = trim($ret, '/');

			if ( isset($params['url_ending']) ) {
				if ( $ret ) {
					$ret .= $params['url_ending'];
				}

				unset($params['url_ending']);
			}
			elseif ( $ret ) {
				$ret .= MOD_REWRITE_URL_ENDING;
			}

			if ( $env ) {
				$params[ENV_VAR_NAME] = ltrim($env, ':');
			}
		}

		unset($params['pass'], $params['opener'], $params['m_event']);

		if ( array_key_exists('escape', $params) && $params['escape'] ) {
			$ret = addslashes($ret);
			unset($params['escape']);
		}

		$ret = str_replace('%2F', '/', urlencode($ret));

		if ( $params ) {
			$params_str = '';
			$join_string = $encode ? '&' : '&amp;';

			foreach ($params as $param => $value) {
				$params_str .= $join_string . $param . '=' . $value;
			}

			$ret .= '?' . substr($params_str, strlen($join_string));
		}

		if ( $encode ) {
			$ret = str_replace('\\', '%5C', $ret);
		}

		return $ret;
	}

	/**
	 * Builds env part that corresponds prefix passed
	 *
	 * @param string $prefix_special item's prefix & [special]
	 * @param Array $params url params
	 * @param bool $pass_events
	 * @return string
	 * @access protected
	 */
	protected function BuildModuleEnv($prefix_special, &$params, $pass_events = false)
	{
		list ($prefix) = explode('.', $prefix_special);

		$url_parts = Array ();
		$listener = $this->rewriteListeners[$prefix][0];

		$ret = $listener[0]->$listener[1](REWRITE_MODE_BUILD, $prefix_special, $params, $url_parts, $pass_events);

		return $ret;
	}
}