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

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

class kUnitConfigReader extends kBase implements kiCacheable {

	/**
	 * Configs reader
	 *
	 * @var Array
	 * @access private
	 */
	var $configData = Array();
	var $configFiles = Array();

	var $CacheExpired = false;

	var $prefixFiles = array();

	var $ProcessAllConfigs = false;
	var $FinalStage = false;
	var $StoreCache = false;
	var $AfterConfigProcessed = array();

	/**
	 * Escaped directory separator for using in regular expressions
	 *
	 * @var string
	 */
	var $_directorySeparator = '';

	/**
	 * Regular expression for detecting module folder
	 *
	 * @var string
	 */
	var $_moduleFolderRegExp = '';

	/**
	 * Folders to skip during unit config search
	 *
	 * @var Array
	 */
	var $_skipFolders = Array ('CVS', '.svn', 'admin_templates', 'libchart');

	/**
	 * Creates instance of unit config reader
	 *
	 */
	public function __construct()
	{
		parent::__construct();

		$this->_directorySeparator = preg_quote(DIRECTORY_SEPARATOR);

		$editor_path = explode('/', trim(EDITOR_PATH, '/'));
		$this->_skipFolders[] = array_pop($editor_path); // last of cmseditor folders

		$this->_moduleFolderRegExp = '#' . $this->_directorySeparator . '(core|modules' . $this->_directorySeparator . '.*?)' . $this->_directorySeparator . '#';
	}

	/**
	 * Sets data from cache to object
	 *
	 * @param Array $data
	 * @access public
	 */
	public function setFromCache(&$data)
	{
		$this->prefixFiles = $data['ConfigReader.prefixFiles'];
	}

	/**
	 * Gets object data for caching
	 *
	 * @access public
	 * @return Array
	 */
	public function getToCache()
	{
		return Array (
			'ConfigReader.prefixFiles' => $this->prefixFiles,
		);
	}

	function scanModules($folderPath, $cache = true)
	{
		if (defined('IS_INSTALL') && IS_INSTALL && !defined('FORCE_CONFIG_CACHE')) {
			// disable config caching during installation
			$cache = false;
		}

		if ($cache) {
			$restored = $this->Application->cacheManager->LoadUnitCache();

			if ($restored) {
				if ( defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode() ) {
					$this->Application->Debugger->appendHTML('UnitConfigReader: Restoring Cache');
				}

				return;
			}
		}

		if ( defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode() ) {
			$this->Application->Debugger->appendHTML('UnitConfigReader: Generating Cache');
		}

		$this->ProcessAllConfigs = true;

		$this->includeConfigFiles($folderPath, $cache);
		$this->ParseConfigs();

		// tell AfterConfigRead to store cache if needed
		// can't store it here because AfterConfigRead needs ability to change config data
		$this->StoreCache = $cache;

		if ( !$this->Application->InitDone ) {
			// scanModules is called multiple times during installation process
			$this->Application->InitManagers();

			// get build-in rewrite listeners ONLY to be able to parse mod-rewrite url when unit config cache is missing
			$this->retrieveCollections();
			$this->_sortRewriteListeners();
		}

		$this->Application->cacheManager->applyDelayedUnitProcessing();
	}

	function findConfigFiles($folderPath, $level = 0)
	{
		// if FULL_PATH = "/" ensure, that all "/" in $folderPath are not deleted
		$reg_exp = '/^' . preg_quote(FULL_PATH, '/') . '/';
		$folderPath = preg_replace($reg_exp, '', $folderPath, 1); // this make sense, since $folderPath may NOT contain FULL_PATH

		$base_folder = FULL_PATH . $folderPath . DIRECTORY_SEPARATOR;
		$sub_folders = glob($base_folder . '*', GLOB_ONLYDIR);
		if (!$sub_folders) {
			return ;
		}

		if ($level == 0) {
			// don't scan Front-End themes because of extensive directory structure
			$sub_folders = array_diff($sub_folders, Array ($base_folder . 'themes', $base_folder . 'tools'));
		}

		foreach ($sub_folders as $full_path) {
			$sub_folder = substr($full_path, strlen($base_folder));

			if (in_array($sub_folder, $this->_skipFolders)) {
				continue;
			}

			if (preg_match('/^\./', $sub_folder)) {
				// don't scan ".folders"
				continue;
			}

			$config_name = $this->getConfigName($folderPath . DIRECTORY_SEPARATOR . $sub_folder);

			if (file_exists(FULL_PATH . $config_name)) {
				$this->configFiles[] = $config_name;
			}

			$this->findConfigFiles($full_path, $level + 1);
		}
	}

	function includeConfigFiles($folderPath, $cache = true)
	{
		$this->Application->refreshModuleInfo();

		if ( $this->Application->isCachingType(CACHING_TYPE_MEMORY) ) {
			$data = $this->Application->getCache('master:config_files', false, CacheSettings::$unitCacheRebuildTime);
		}
		else {
			$data = $this->Application->getDBCache('config_files', CacheSettings::$unitCacheRebuildTime);
		}

		if ( $data ) {
			$this->configFiles = unserialize($data);

			if ( !defined('DBG_VALIDATE_CONFIGS') && !DBG_VALIDATE_CONFIGS ) {
				shuffle($this->configFiles);
			}
		}
		else {
			if ( $cache ) {
				if ( $this->Application->isCachingType(CACHING_TYPE_MEMORY) ) {
					$this->Application->rebuildCache('master:config_files', kCache::REBUILD_NOW, CacheSettings::$unitCacheRebuildTime);
				}
				else {
					$this->Application->rebuildDBCache('config_files', kCache::REBUILD_NOW, CacheSettings::$unitCacheRebuildTime);
				}
			}

			$this->findConfigFiles(FULL_PATH . DIRECTORY_SEPARATOR . 'core'); // search from core directory
			$this->findConfigFiles($folderPath); // search from modules directory

			if ( $cache ) {
				if ( $this->Application->isCachingType(CACHING_TYPE_MEMORY) ) {
					$this->Application->setCache('master:config_files', serialize($this->configFiles));
				}
				else {
					$this->Application->setDBCache('config_files', serialize($this->configFiles));
				}
			}
		}

		foreach ($this->configFiles as $filename) {
			$prefix = $this->PreloadConfigFile($filename);

			if (!$prefix) {
				throw new Exception('Prefix not defined in config file <strong>' . $filename . '</strong>');
			}
		}

		if ($cache) {
			unset($this->configFiles);
		}
	}

	/**
	 * Process all read config files - called ONLY when there is no cache!
	 *
	 */
	function ParseConfigs()
	{
		// 1. process normal configs and their dependencies
		$prioritized_configs = array();
		foreach ($this->configData as $prefix => $config) {
			if (isset($config['ConfigPriority'])) {
				$prioritized_configs[$prefix] = $config['ConfigPriority'];
				continue;
			}
			$this->parseConfig($prefix);
		}

		foreach ($this->configData as $prefix => $config) {
			$this->ProcessDependencies($prefix);
			$this->postProcessConfig($prefix, 'AggregateConfigs', 'sub_prefix');
			$clones = $this->postProcessConfig($prefix, 'Clones', 'prefix');
		}

		// 2. process prioritized configs and their dependencies
		asort($prioritized_configs);
		foreach ($prioritized_configs as $prefix => $priority) {
			$this->parseConfig($prefix);
		}

		foreach ($prioritized_configs as $prefix => $priority) {
			$this->ProcessDependencies($prefix);
		}
	}

	function AfterConfigRead($store_cache = null)
	{
//		if (!$this->ProcessAllConfigs) return ;
		$this->FinalStage = true;
		foreach ($this->configData as $prefix => $config) {
			$this->runAfterConfigRead($prefix);
		}

		if ( !isset($store_cache) ) {
			// $store_cache not overridden -> use global setting
			$store_cache = $this->StoreCache;
		}

		if ($store_cache || (defined('IS_INSTALL') && IS_INSTALL)) {
			// cache is not stored during install, but dynamic clones should be processed in any case
			$this->processDynamicClones();
			$this->retrieveCollections();
		}

		if ($store_cache) {
			$this->_sortRewriteListeners();

			$after_event = new kEvent('adm:OnAfterCacheRebuild');
			$this->Application->HandleEvent($after_event);

			$this->Application->cacheManager->UpdateUnitCache();

			if (defined('DEBUG_MODE') && DEBUG_MODE && defined('DBG_VALIDATE_CONFIGS') && DBG_VALIDATE_CONFIGS) {
				// validate configs here to have changes from OnAfterConfigRead hooks to prefixes
				foreach ($this->configData as $prefix => $config) {
					if (!isset($config['TableName'])) continue;
					$this->ValidateConfig($prefix);
				}
			}
		}
	}

	/**
	 * Sort rewrite listeners according to RewritePriority (non-prioritized listeners goes first)
	 *
	 */
	function _sortRewriteListeners()
	{
		$listeners = Array ();
		$prioritized_listeners = Array ();

		// process non-prioritized listeners
		foreach ($this->Application->RewriteListeners as $prefix => $listener_data) {
			if ($listener_data['priority'] === false) {
				$listeners[$prefix] = $listener_data;
			}
			else {
				$prioritized_listeners[$prefix] = $listener_data['priority'];
			}
		}

		// process prioritized listeners
		asort($prioritized_listeners, SORT_NUMERIC);
		foreach ($prioritized_listeners as $prefix => $priority) {
			$listeners[$prefix] = $this->Application->RewriteListeners[$prefix];
		}

		$this->Application->RewriteListeners = $listeners;
	}

	/**
	 * Re-reads all configs
	 *
	 */
	function ReReadConfigs()
	{
		// don't reset prefix file, since file scanning could slow down the process
		$prefix_files_backup = $this->prefixFiles;
		$this->Application->cacheManager->EmptyUnitCache();
		$this->prefixFiles = $prefix_files_backup;

		// parse all configs
		$this->ProcessAllConfigs = true;
		$this->AfterConfigProcessed = Array ();
		$this->includeConfigFiles(MODULES_PATH, false);
		$this->ParseConfigs();
		$this->AfterConfigRead(false);
		$this->processDynamicClones();

		// don't call kUnitConfigReader::retrieveCollections since it
		// will overwrite what we already have in kApplication class instance
	}

	/**
	 * Process clones, that were defined via OnAfterConfigRead event
	 *
	 */
	function processDynamicClones()
	{
		$new_clones = Array();
		foreach ($this->configData as $prefix => $config) {
			$clones = $this->postProcessConfig($prefix, 'Clones', 'prefix');

			if ($clones) {
				$new_clones = array_merge($new_clones, $clones);
			}
		}

		// execute delayed methods for cloned unit configs
		$this->Application->cacheManager->applyDelayedUnitProcessing();

		// call OnAfterConfigRead for cloned configs
		$new_clones = array_unique($new_clones);
		foreach ($new_clones as $prefix) {
			$this->runAfterConfigRead($prefix);
		}
	}

	/**
	 * Process all collectible unit config options here to also catch ones, defined from OnAfterConfigRead events
	 *
	 */
	function retrieveCollections()
	{
		foreach ($this->configData as $prefix => $config) {
			// collect replacement templates
			if (array_key_exists('ReplacementTemplates', $config) && $config['ReplacementTemplates']) {
				$this->Application->ReplacementTemplates = array_merge($this->Application->ReplacementTemplates, $config['ReplacementTemplates']);
			}

			// collect rewrite listeners
			if (array_key_exists('RewriteListener', $config) && $config['RewriteListener']) {
				$rewrite_listeners = $config['RewriteListener'];

				if (!is_array($rewrite_listeners)) {
					// when one method is used to build and parse url
					$rewrite_listeners = Array ($rewrite_listeners, $rewrite_listeners);
				}

				foreach ($rewrite_listeners as $index => $rewrite_listener) {
					if (strpos($rewrite_listener, ':') === false) {
						$rewrite_listeners[$index] = $prefix . '_EventHandler:' . $rewrite_listener;
					}
				}

				$rewrite_priority = array_key_exists('RewritePriority', $config) ? $config['RewritePriority'] : false;

				$this->Application->RewriteListeners[$prefix] = Array ('listener' => $rewrite_listeners, 'priority' => $rewrite_priority);
			}
		}
	}

	/**
	 * Register nessasary classes
	 * This method should only process the data which is cached!
	 *
	 * @param string $prefix
	 * @access private
	 */
	function parseConfig($prefix)
	{
		$this->parseClasses($prefix);
		$this->parseScheduledTasks($prefix);
		$this->parseHooks($prefix);
		$this->parseAggregatedTags($prefix);
	}

	protected function parseClasses($prefix)
	{
		$config =& $this->configData[$prefix];
		$register_classes = $this->getClasses($prefix);

		foreach ($register_classes as $class_info) {
			// remember class dependencies
			$class_name = $class_info['class'];
			$require_classes = isset($class_info['require_classes']) ? $class_info['require_classes'] : Array ();

			if ($require_classes) {
				$require_classes = (array)$require_classes;

				if ( !isset($config['_Dependencies'][$class_name]) ) {
					$config['_Dependencies'][$class_name] = Array ();
				}

				$config['_Dependencies'][$class_name] = array_merge($config['_Dependencies'][$class_name], $require_classes);
			}

			// register class
			$this->Application->registerClass(
				$class_name,
				$config['BasePath'] . DIRECTORY_SEPARATOR . $class_info['file'],
				$class_info['pseudo']
			);

			if ( isset($class_info['build_event']) && $class_info['build_event'] ) {
				$this->Application->delayUnitProcessing('registerBuildEvent', Array ($class_info['pseudo'], $class_info['build_event']));
			}
		}
	}

	protected function parseScheduledTasks($prefix)
	{
		$config =& $this->configData[$prefix];

		if ( !isset($config['ScheduledTasks']) || !$config['ScheduledTasks'] ) {
			return ;
		}

		$scheduled_tasks = $config['ScheduledTasks'];

		foreach ($scheduled_tasks as $short_name => $scheduled_task_info) {
			$event_status = array_key_exists('Status', $scheduled_task_info) ? $scheduled_task_info['Status'] : STATUS_ACTIVE;
			$this->Application->delayUnitProcessing('registerScheduledTask', Array ( $short_name, $config['Prefix'] . ':' . $scheduled_task_info['EventName'], $scheduled_task_info['RunInterval'], $event_status ));
		}
	}

	protected function parseHooks($prefix)
	{
		$config =& $this->configData[$prefix];

		if ( !isset($config['Hooks']) || !$config['Hooks'] ) {
			return ;
		}

		$hooks = $config['Hooks'];

		foreach ($hooks as $hook) {
			if ( isset($config['ParentPrefix']) && ($hook['HookToPrefix'] == $config['ParentPrefix']) ) {
				trigger_error('Depricated Hook Usage [prefix: <strong>' . $config['Prefix'] . '</strong>; do_prefix: <strong>' . $hook['DoPrefix'] . '</strong>] use <strong>#PARENT#</strong> as <strong>HookToPrefix</strong> value, where HookToPrefix is same as ParentPrefix', defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_NOTICE);
			}

			if ($hook['HookToPrefix'] == '') {
				// new: set hooktoprefix to current prefix if not set
				$hook['HookToPrefix'] = $config['Prefix'];
			}

			if ( isset($config['ParentPrefix']) ) {
				// new: allow to set hook to parent prefix what ever it is
				if ($hook['HookToPrefix'] == '#PARENT#') {
					$hook['HookToPrefix'] = $config['ParentPrefix'];
				}

				if ($hook['DoPrefix'] == '#PARENT#') {
					$hook['DoPrefix'] = $config['ParentPrefix'];
				}
			}
			elseif ($hook['HookToPrefix'] == '#PARENT#' || $hook['DoPrefix'] == '#PARENT#') {
				// we need parent prefix but it's not set !
				continue;
			}

			$hook_events = (array)$hook['HookToEvent'];
			$do_prefix = $hook['DoPrefix'] == '' ? $config['Prefix'] : $hook['DoPrefix'];

			foreach ($hook_events as $hook_event) {
				$hook_event = $hook['HookToPrefix'] . '.' . $hook['HookToSpecial'] . ':' . $hook_event;
				$do_event = $do_prefix . '.' . $hook['DoSpecial'] . ':' . $hook['DoEvent'];

				$this->Application->delayUnitProcessing('registerHook', Array ($hook_event, $do_event, $hook['Mode'], $hook['Conditional']));
			}
		}
	}

	protected function parseAggregatedTags($prefix)
	{
		$config =& $this->configData[$prefix];
		$aggregated_tags = isset($config['AggregateTags']) ? $config['AggregateTags'] : Array ();

		foreach ($aggregated_tags as $aggregate_tag) {
			if ( isset($config['ParentPrefix']) ) {
				if ($aggregate_tag['AggregateTo'] == $config['ParentPrefix']) {
					trigger_error('Depricated Aggregate Tag Usage [prefix: <b>'.$config['Prefix'].'</b>; AggregateTo: <b>'.$aggregate_tag['AggregateTo'].'</b>] use <b>#PARENT#</b> as <b>AggregateTo</b> value, where AggregateTo is same as ParentPrefix', defined('E_USER_DEPRECATED') ? E_USER_DEPRECATED : E_USER_NOTICE);
				}

				if ($aggregate_tag['AggregateTo'] == '#PARENT#') {
					$aggregate_tag['AggregateTo'] = $config['ParentPrefix'];
				}
			}

			$aggregate_tag['LocalPrefix'] = $config['Prefix'];
			$this->Application->delayUnitProcessing('registerAggregateTag', Array ($aggregate_tag));
		}
	}

	function ValidateConfig($prefix)
	{
		global $debugger;

		$config =& $this->configData[$prefix];

		$tablename = $config['TableName'];
		$float_types = Array ('float', 'double', 'numeric');

		$table_found = $this->Conn->Query('SHOW TABLES LIKE "'.$tablename.'"');
		if (!$table_found) {
			// config present, but table missing, strange
			kUtil::safeDefine('DBG_RAISE_ON_WARNINGS', 1);
			$debugger->appendHTML("<b class='debug_error'>Config Warning: </b>Table <strong>$tablename</strong> missing, but prefix <b>".$config['Prefix']."</b> requires it!");
			$debugger->WarningCount++;

			return ;
		}

		$res = $this->Conn->Query('DESCRIBE '.$tablename);
		$config_link = $debugger->getFileLink(FULL_PATH.$this->prefixFiles[$config['Prefix']], 1, $config['Prefix']);

		$error_messages = Array (
			'field_not_found' => 'Field <strong>%s</strong> exists in the database, but <strong>is not defined</strong> in config',
			'default_missing' => 'Default value for field <strong>%s</strong> not set in config',
			'not_null_error1' => 'Field <strong>%s</strong> is NOT NULL in the database, but is not configured as not_null', // or required',
			'not_null_error2' => 'Field <strong>%s</strong> is described as NOT NULL in config, but <strong>does not have DEFAULT value</strong>',
			'not_null_error3' => 'Field <strong>%s</strong> is described as <strong>NOT NULL in config</strong>, but is <strong>NULL in db</strong>',
			'invalid_default' => '<strong>Default value</strong> for field %s<strong>%s</strong> not sync. to db (in config = %s, in db = %s)',
			'date_column_not_null_error' => 'Field <strong>%s</strong> must be NULL in config and database, since it contains date',
			'user_column_default_error' => 'Field <strong>%s</strong> must be have NULL as default value, since it holds user id',
			'type_missing' => '<strong>Type definition</strong> for field <strong>%s</strong> missing in config',
			'virtual_type_missing' => '<strong>Type definition</strong> for virtual field <strong>%s</strong> missing in config',
			'virtual_default_missing' => 'Default value for virtual field <strong>%s</strong> not set in config',
			'virtual_not_null_error' => 'Virtual field <strong>%s</strong> cannot be not null, since it doesn\'t exist in database',
			'invalid_calculated_field' => 'Calculated field <strong>%s</strong> is missing corresponding virtual field',
		);

		$config_errors = Array ();
		$tablename = preg_replace('/^'.preg_quote(TABLE_PREFIX, '/').'(.*)/', '\\1', $tablename); // remove table prefix

		// validate unit config field declaration in relation to database table structure
		foreach ($res as $field) {
			$f_name = $field['Field'];

			if (getArrayValue($config, 'Fields')) {
				if (preg_match('/l[\d]+_[\w]/', $f_name)) {
					// skip multilingual fields
					continue;
				}

				if (!array_key_exists ($f_name, $config['Fields'])) {
					$config_errors[] = sprintf($error_messages['field_not_found'], $f_name);
				}
				else {
					$db_default = $field['Default'];

					if (is_numeric($db_default)) {
						$db_default = preg_match('/[\.,]/', $db_default) ? (float)$db_default : (int)$db_default;
					}

					$default_missing = false;
					$options = $config['Fields'][$f_name];
					$not_null = isset($options['not_null']) && $options['not_null'];
					$formatter = array_key_exists('formatter', $options) ? $options['formatter'] : false;

					if (!array_key_exists('default', $options)) {
						$config_errors[] = sprintf($error_messages['default_missing'], $f_name);
						$default_missing = true;
					}

					if ($field['Null'] != 'YES') {
						// field is NOT NULL in database (MySQL5 for null returns "NO", but MySQL4 returns "")
						if ( $f_name != $config['IDField'] && !isset($options['not_null']) /*&& !isset($options['required'])*/ ) {
							$config_errors[] = sprintf($error_messages['not_null_error1'], $f_name);
						}
						if ($not_null && !isset($options['default']) ) {
							$config_errors[] = sprintf($error_messages['not_null_error2'], $f_name);
						}
					}
					elseif ($not_null) {
						$config_errors[] = sprintf($error_messages['not_null_error3'], $f_name);
					}

					if (($formatter == 'kDateFormatter') && $not_null) {
						$config_errors[] = sprintf($error_messages['date_column_not_null_error'], $f_name);
					}

					// columns, holding userid should have NULL as default value
					if (array_key_exists('type', $options) && !$default_missing) {
						// both type and default value set

						if (preg_match('/ById$/', $f_name) && $options['default'] !== null) {
							$config_errors[] = sprintf($error_messages['user_column_default_error'], $f_name);
						}
					}

					if (!array_key_exists('type', $options)) {
						$config_errors[] = sprintf($error_messages['type_missing'], $f_name);
					}

					if (!$default_missing && ($field['Type'] != 'text')) {
						if ( is_null($db_default) && $not_null ) {
							$db_default = $options['type'] == 'string' ? '' : 0;
						}

						if ($f_name == $config['IDField'] && $options['type'] != 'string' && $options['default'] !== 0) {
							$config_errors[] = sprintf($error_messages['invalid_default'], '<span class="debug_error">IDField</span> ', $f_name, $this->varDump($options['default']), $this->varDump($field['Default']));
						}
						else if (((string)$options['default'] != '#NOW#') && ($db_default !== $options['default']) && !in_array($options['type'], $float_types)) {
							$config_errors[] = sprintf($error_messages['invalid_default'], '', $f_name, $this->varDump($options['default']), $this->varDump($db_default));
						}
					}
				}
			}
		}

		// validate virtual fields
		if ( array_key_exists('VirtualFields', $config) ) {
			foreach ($config['VirtualFields'] as $f_name => $options) {
				if (!array_key_exists('type', $options)) {
					$config_errors[] = sprintf($error_messages['virtual_type_missing'], $f_name);
				}

				if (array_key_exists('not_null', $options)) {
					$config_errors[] = sprintf($error_messages['virtual_not_null_error'], $f_name);
				}

				if (!array_key_exists('default', $options)) {
					$config_errors[] = sprintf($error_messages['virtual_default_missing'], $f_name);
				}
			}
		}

		// validate calculated fields
		if ( array_key_exists('CalculatedFields', $config) ) {
			foreach ($config['CalculatedFields'] as $special => $calculated_fields) {
				foreach ($calculated_fields as $calculated_field => $calculated_field_expr) {
					if ( !isset($config['VirtualFields'][$calculated_field]) ) {
						$config_errors[] = sprintf($error_messages['invalid_calculated_field'], $calculated_field);
					}
				}
			}

			$config_errors = array_unique($config_errors);
		}

		if ($config_errors) {
			$error_prefix = '<strong class="debug_error">Config Error'.(count($config_errors) > 1 ? 's' : '').': </strong> for prefix <strong>'.$config_link.'</strong> ('.$tablename.') in unit config:<br />';
			$config_errors = $error_prefix.'&nbsp;&nbsp;&nbsp;'.implode('<br />&nbsp;&nbsp;&nbsp;', $config_errors);

			kUtil::safeDefine('DBG_RAISE_ON_WARNINGS', 1);
			$debugger->appendHTML($config_errors);
			$debugger->WarningCount++;
		}
	}

	function varDump($value)
	{
		return '<strong>'.var_export($value, true).'</strong> of '.gettype($value);
	}

	protected function ProcessDependencies($prefix)
	{
		$config =& $this->configData[$prefix];
		$dependencies = getArrayValue($config, '_Dependencies');
		/* @var $dependencies Array */

		if ( !$dependencies ) {
			return ;
		}

		foreach ($dependencies as $real_class => $requires) {
			foreach ($requires as $class) {
				$this->Application->registerDependency($real_class, $class);
			}
		}

		unset($config['_Dependencies']);
	}

	function postProcessConfig($prefix, $config_key, $dst_prefix_var)
	{
		$main_config =& $this->configData[$prefix];
		$sub_configs = isset($main_config[$config_key]) && $main_config[$config_key] ? $main_config[$config_key] : Array ();
		if ( !$sub_configs ) {
			return Array ();
		}
		unset($main_config[$config_key]);

		$processed = array();
		foreach ($sub_configs as $sub_prefix => $sub_config) {
			if ($config_key == 'AggregateConfigs' && !isset($this->configData[$sub_prefix])) {
				$this->loadConfig($sub_prefix);
			}
			$sub_config['Prefix'] = $sub_prefix;
			$this->configData[$sub_prefix] = kUtil::array_merge_recursive($this->configData[$$dst_prefix_var], $sub_config);

			// when merging empty array to non-empty results non-empty array, but empty is required
			foreach ($sub_config as $sub_key => $sub_value) {
				if (!$sub_value) {
					unset($this->configData[$sub_prefix][$sub_key]);
				}
			}
			if ($config_key == 'Clones') {
				$this->prefixFiles[$sub_prefix] = $this->prefixFiles[$prefix];
			}

			$this->postProcessConfig($sub_prefix, $config_key, $dst_prefix_var);
			if ($config_key == 'AggregateConfigs') {
				$processed = array_merge($this->postProcessConfig($sub_prefix, 'Clones', 'prefix'), $processed);
			}
			elseif ($this->ProcessAllConfigs) {
				$this->parseConfig($sub_prefix);
			}
			array_push($processed, $sub_prefix);
		}

		if (!$prefix) {
			// configs, that used only for cloning & not used ifself
			unset($this->configData[$prefix]);
		}
		return array_unique($processed);
	}

	function PreloadConfigFile($filename)
	{
		$config_found = file_exists(FULL_PATH . $filename) && $this->configAllowed($filename);

		if (defined('DEBUG_MODE') && DEBUG_MODE && defined('DBG_PROFILE_INCLUDES') && DBG_PROFILE_INCLUDES) {
			if ( in_array($filename, get_included_files()) ) {
				return '';
			}

			global $debugger;

			if ($config_found) {
				$file = FULL_PATH . $filename;
				$file_crc = crc32($file);

				$debugger->ProfileStart('inc_' . $file_crc, $file);
				include_once($file);
				$debugger->ProfileFinish('inc_' . $file_crc);
				$debugger->profilerAddTotal('includes', 'inc_' . $file_crc);
			}
		}
		elseif ($config_found) {
			include_once(FULL_PATH . $filename);
		}

		if ($config_found) {
			if (isset($config) && $config) {
				// config file is included for 1st time -> save it's content for future processing
				$prefix = array_key_exists('Prefix', $config) ? $config['Prefix'] : '';

				preg_match($this->_moduleFolderRegExp, $filename, $rets);
				$config['ModuleFolder'] = str_replace(DIRECTORY_SEPARATOR, '/', $rets[1]);
				$config['BasePath'] = dirname(FULL_PATH . $filename);

				if (array_key_exists('AdminTemplatePath', $config)) {
					// append template base folder for admin templates path of this prefix
					$module_templates = $rets[1] == 'core' ? '' : substr($rets[1], 8) . '/';
					$config['AdminTemplatePath'] = $module_templates . $config['AdminTemplatePath'];
				}

				if (array_key_exists($prefix, $this->prefixFiles) && ($this->prefixFiles[$prefix] != $filename)) {
					trigger_error(
						'Single unit config prefix "<strong>' . $prefix . '</strong>" ' .
						'is used in multiple unit config files: ' .
						'"<strong>' . $this->prefixFiles[$prefix] . '</strong>", "<strong>' . $filename . '</strong>"',
						E_USER_WARNING
					);
				}

				$this->configData[$prefix] = $config;
				$this->prefixFiles[$prefix] = $filename;

				return $prefix;
			}
			else {
				$prefix = array_search($filename, $this->prefixFiles);

				if ( $prefix ) {
					// attempt is made to include config file twice or more, but include_once prevents that,
					// but file exists on hdd, then it is already saved to all required arrays, just return it's prefix
					return $prefix;
				}
			}
		}

		return 'dummy';
	}

	function loadConfig($prefix)
	{
		if ( !isset($this->prefixFiles[$prefix]) ) {
			throw new Exception('Configuration file for prefix <strong>' . $prefix . '</strong> is unknown');

			return ;
		}

		$file = $this->prefixFiles[$prefix];
		$prefix = $this->PreloadConfigFile($file);

		if ($this->FinalStage) {
			// run prefix OnAfterConfigRead so all
			// hooks to it can define their clonses
			$this->runAfterConfigRead($prefix);
		}

		$clones = $this->postProcessConfig($prefix, 'AggregateConfigs', 'sub_prefix');
		$clones = array_merge($this->postProcessConfig($prefix, 'Clones', 'prefix'), $clones);

		if ($this->FinalStage) {
			$clones = array_unique($clones);

			foreach ($clones as $a_prefix) {
				$this->runAfterConfigRead($a_prefix);
			}
		}
	}

	function runAfterConfigRead($prefix)
	{
		if (in_array($prefix, $this->AfterConfigProcessed)) {
			return ;
		}

		$this->Application->HandleEvent( new kEvent($prefix . ':OnAfterConfigRead') );

		if (!(defined('IS_INSTALL') && IS_INSTALL)) {
			// allow to call OnAfterConfigRead multiple times during install
			array_push($this->AfterConfigProcessed, $prefix);
		}
	}

	/**
	 * Reads unit (specified by $prefix)
	 * option specified by $option
	 *
	 * @param string $prefix
	 * @param string $name
	 * @param mixed $default
	 * @return string
	 * @access public
	 */
	function getUnitOption($prefix, $name, $default = false)
	{
		if (preg_match('/(.*)\.(.*)/', $prefix, $rets)) {
			if (!isset($this->configData[$rets[1]])) {
				$this->loadConfig($rets[1]);
			}
			$ret = isset($this->configData[$rets[1]][$name][$rets[2]]) ? $this->configData[$rets[1]][$name][$rets[2]] : false;
//			$ret = getArrayValue($this->configData, $rets[1], $name, $rets[2]);
		}
		else {
			if (!isset($this->configData[$prefix])) {
				$this->loadConfig($prefix);
			}
			$ret = isset($this->configData[$prefix][$name]) ? $this->configData[$prefix][$name] : false;
//			$ret = getArrayValue($this->configData, $prefix, $name);
		}
		return $ret === false ? $default : $ret;
	}

	/**
	 * Read all unit with $prefix options
	 *
	 * @param string $prefix
	 * @return Array
	 * @access public
	 */
	function getUnitOptions($prefix)
	{
		if (!isset($this->configData[$prefix])) {
			$this->loadConfig($prefix);
		}

		return $this->configData[$prefix];
	}

	/**
	 * Set's new unit option value
	 *
	 * @param string $prefix
	 * @param string $name
	 * @param string $value
	 * @access public
	 */
	function setUnitOption($prefix, $name, $value)
	{
		if ( preg_match('/(.*)\.(.*)/', $prefix, $rets) ) {
			if ( !isset($this->configData[$rets[1]]) ) {
				$this->loadConfig($rets[1]);
			}

			$this->configData[$rets[1]][$name][$rets[2]] = $value;
		}
		else {
			if ( !isset($this->configData[$prefix]) ) {
				$this->loadConfig($prefix);
			}

			$this->configData[$prefix][$name] = $value;
		}
	}

	protected function getClasses($prefix)
	{
		$config =& $this->configData[$prefix];
		$class_params = Array ('ItemClass', 'ListClass', 'EventHandlerClass', 'TagProcessorClass');
		$register_classes = isset($config['RegisterClasses']) ? $config['RegisterClasses'] : Array ();

		foreach ($class_params as $param_name) {
			if ( !isset($config[$param_name]) ) {
				continue;
			}

			$config[$param_name]['pseudo'] = $this->getPseudoByOptionName($param_name, $prefix);
			$register_classes[] = $config[$param_name];
		}

		return $register_classes;
	}

	protected function getPseudoByOptionName($option_name, $prefix)
	{
		$pseudo_class_map = Array (
			'ItemClass' => '%s',
			'ListClass' => '%s_List',
			'EventHandlerClass' => '%s_EventHandler',
			'TagProcessorClass' => '%s_TagProcessor'
		);

		return sprintf($pseudo_class_map[$option_name], $prefix);
	}

	/**
	 * Get's config file name based
	 * on folder name supplied
	 *
	 * @param string $folderPath
	 * @return string
	 * @access private
	 */
	function getConfigName($folderPath)
	{
		return $folderPath . DIRECTORY_SEPARATOR . basename($folderPath) . '_config.php';
	}

	/**
	 * Checks if config file is allowed for includion (if module of config is installed)
	 *
	 * @param string $config_path relative path from in-portal directory
	 */
	function configAllowed($config_path)
	{
		static $module_paths = null;

		if (defined('IS_INSTALL') && IS_INSTALL) {
			// at installation start no modules in db and kernel configs could not be read
			return true;
		}

		if (preg_match('#^' . $this->_directorySeparator . 'core#', $config_path)) {
			// always allow to include configs from "core" module's folder
			return true;
		}

		if (!$this->Application->ModuleInfo) {
			return false;
		}

		if (!isset($module_paths)) {
			$module_paths = Array ();

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				$module_paths[] = str_replace('/', DIRECTORY_SEPARATOR, rtrim($module_info['Path'], '/'));
			}

			$module_paths = array_unique($module_paths);
		}

		preg_match($this->_moduleFolderRegExp, $config_path, $rets);

		// config file path starts with module folder path
		return in_array($rets[1], $module_paths);
	}

	/**
	 * Returns true if config exists and is allowed for reading
	 *
	 * @param string $prefix
	 * @return bool
	 */
	function prefixRegistred($prefix)
	{
		return isset($this->prefixFiles[$prefix]) ? true : false;
	}

	/**
	 * Returns config file for given prefix
	 *
	 * @param string $prefix
	 * @return string
	 */
	function getPrefixFile($prefix)
	{
		return array_key_exists($prefix, $this->prefixFiles) ? $this->prefixFiles[$prefix] : false;
	}

	function iterateConfigs($callback_function, $params)
	{
		$this->includeConfigFiles(MODULES_PATH); //make sure to re-read all configs
		$this->AfterConfigRead();

		foreach ($this->configData as $prefix => $config_data) {
			$callback_function[0]->$callback_function[1]($prefix, $config_data, $params);
		}
	}

}