<?php
/**
* @version	$Id: install_toolkit.php 16156 2015-04-29 06:57:02Z 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!');

	/**
	 * Upgrade sqls are located using this mask
	 *
	 */
	define('UPGRADES_FILE', FULL_PATH.'/%sinstall/upgrades.%s');

	/**
	 * Prerequisit check classes are located using this mask
	 *
	 */
	define('PREREQUISITE_FILE', FULL_PATH.'/%sinstall/prerequisites.php');

	/**
	 * Format of version identificator in upgrade files (normal, beta, release candidate)
	 *
	 */
	define('VERSION_MARK', '# ===== v ([\d]+\.[\d]+\.[\d]+|[\d]+\.[\d]+\.[\d]+-B[\d]+|[\d]+\.[\d]+\.[\d]+-RC[\d]+) =====');

	if (!defined('GET_LICENSE_URL')) {
		/**
		 * Url used for retrieving user licenses from Intechnic licensing server
		 *
		 */
		define('GET_LICENSE_URL', 'http://www.in-portal.com/license.php');
	}

	/**
	 * Misc functions, that are required during installation, when
	 *
	 */
	class kInstallToolkit {

		/**
		 * Reference to kApplication class object
		 *
		 * @var kApplication
		 */
		var $Application = null;

		/**
		 * Connection to database
		 *
		 * @var IDBConnection
		 */
		var $Conn = null;

		/**
		 * System config object to access data from config.php
		 *
		 * @var Array
		 */
		public $systemConfig;

		/**
		 * Installator instance
		 *
		 * @var kInstallator
		 */
		var $_installator = null;

		function kInstallToolkit()
		{
			$this->systemConfig = new kSystemConfig(true, false);

			if ( class_exists('kApplication') ) {
				// auto-setup in case of separate module install
				$this->Application =& kApplication::Instance();
				$this->Application->Init(); // needed for standalone module install

				$this->Conn =& $this->Application->GetADODBConnection();
			}
		}

		/**
		 * Sets installator
		 *
		 * @param kInstallator $instance
		 */
		function setInstallator(&$instance)
		{
			$this->_installator =& $instance;
		}

		/**
		 * Checks prerequisities before module install or upgrade
		 *
		 * @param string $module_path
		 * @param string $versions
		 * @param string $mode upgrade mode = {install, standalone, upgrade}
		 * @return bool
		 */
		function CheckPrerequisites($module_path, $versions, $mode)
		{
			if ( !$versions ) {
				return Array ();
			}

			$prerequisite_object =& $this->getPrerequisiteObject($module_path);
			/* @var $prerequisite_object InPortalPrerequisites */

			// some errors possible
			return is_object($prerequisite_object) ? $prerequisite_object->CheckPrerequisites($versions, $mode) : Array ();
		}

		/**
		 * Call prerequisites method
		 *
		 * @param string $module_path
		 * @param string $method
		 * @return array
		 */
		function CallPrerequisitesMethod($module_path, $method)
		{
			$prerequisite_object =& $this->getPrerequisiteObject($module_path);
			/* @var $prerequisite_object InPortalPrerequisites */

			return is_object($prerequisite_object) ? $prerequisite_object->$method() : false;
		}

		/**
		 * Returns prerequisite object to be used for checks
		 *
		 * @param string $module_path
		 * @return kHelper
		 * @access protected
		 */
		protected function &getPrerequisiteObject($module_path)
		{
			static $prerequisite_classes = Array ();

			$prerequisites_file = sprintf(PREREQUISITE_FILE, $module_path);
			if ( !file_exists($prerequisites_file) ) {
				$false = false;

				return $false;
			}

			if ( !isset($prerequisite_classes[$module_path]) ) {
				// save class name, because 2nd time
				// (in after call $prerequisite_class variable will not be present)
				include_once $prerequisites_file;
				$prerequisite_classes[$module_path] = $prerequisite_class;
			}

			$prerequisite_object = new $prerequisite_classes[$module_path]();
			/* @var $prerequisite_object InPortalPrerequisites */

			if ( method_exists($prerequisite_object, 'setToolkit') ) {
				$prerequisite_object->setToolkit($this);
			}

			return $prerequisite_object;
		}

		/**
		 * Processes one license, received from server
		 *
		 * @param string $file_data
		 */
		function processLicense($file_data)
		{
			$modules_helper = $this->Application->recallObject('kModulesHelper');
			/* @var $modules_helper kModulesHelper */

			$file_data = explode('Code==:', $file_data);
			$file_data[0] = str_replace('In-Portal License File - do not edit!' . "\n", '', $file_data[0]);
			$file_data = array_map('trim', $file_data);

			if ($modules_helper->verifyLicense($file_data[0])) {
				$this->systemConfig->set('License', 'Intechnic', $file_data[0]);
				if (array_key_exists(1, $file_data)) {
					$this->systemConfig->set('LicenseCode', 'Intechnic', $file_data[1]);
				}
				else {
					$this->systemConfig->set('LicenseCode', 'Intechnic');
				}
				$this->systemConfig->save();
			}
			else {
				// invalid license received from licensing server
				$this->_installator->errorMessage = 'Invalid License File';
			}
		}

		/**
		 * Saves given configuration values to database
		 *
		 * @param Array $config
		 */
		function saveConfigValues($config)
		{
			foreach ($config as $config_var => $value) {
				$sql = 'UPDATE ' . TABLE_PREFIX . 'SystemSettings
						SET VariableValue = ' . $this->Conn->qstr($value) . '
						WHERE VariableName = ' . $this->Conn->qstr($config_var);
				$this->Conn->Query($sql);
			}
		}

		/**
		 * Sets module version to passed
		 *
		 * @param string $module_name
		 * @param string|bool $module_path
		 * @param string|bool $version
		 */
		function SetModuleVersion($module_name, $module_path = false, $version = false)
		{
			if ($version === false) {
				if (!$module_path) {
					throw new Exception('Module path must be given to "SetModuleVersion" method to auto-detect version');
				}

				$version = $this->GetMaxModuleVersion($module_path);
			}

			// get table prefix from config, because application may not be available here
			$table_prefix = $this->systemConfig->get('TablePrefix', 'Database');

			if ($module_name == 'kernel') {
				$module_name = 'in-portal';
			}

			$sql = 'UPDATE ' . $table_prefix . 'Modules
					SET Version = "' . $version . '", BuildDate = ' . time() . '
					WHERE LOWER(Name) = "' . strtolower($module_name) . '"';
			$this->Conn->Query($sql);
		}

		/**
		 * Sets module root category to passed
		 *
		 * @param string $module_name
		 * @param int $category_id
		 */
		function SetModuleRootCategory($module_name, $category_id = 0)
		{
			// get table prefix from config, because application may not be available here
			$table_prefix = $this->systemConfig->get('TablePrefix', 'Database');

			if ($module_name == 'kernel') {
				$module_name = 'in-portal';
			}

			$sql = 'UPDATE ' . $table_prefix . 'Modules
					SET RootCat = ' . $category_id . '
					WHERE LOWER(Name) = "' . strtolower($module_name) . '"';
			$this->Conn->Query($sql);
		}

		/**
		 * Returns maximal version of given module by scanning it's upgrade scripts
		 *
		 * @param string $module_path
		 * @return string
		 */
		function GetMaxModuleVersion($module_path)
		{
			$module_path = rtrim(mb_strtolower($module_path), '/');
			$upgrades_file = sprintf(UPGRADES_FILE, $module_path . '/', 'sql');

			if (!file_exists($upgrades_file)) {
				// no upgrade file
				return '5.0.0';
			}

			$sqls = file_get_contents($upgrades_file);
			$versions_found = preg_match_all('/'.VERSION_MARK.'/s', $sqls, $regs);
			if (!$versions_found) {
				// upgrades file doesn't contain version definitions
				return '5.0.0';
			}

			return end($regs[1]);
		}

		/**
		 * Runs SQLs from file
		 *
		 * @param string $filename
		 * @param mixed $replace_from
		 * @param mixed $replace_to
		 */
		function RunSQL($filename, $replace_from = null, $replace_to = null)
		{
			if (!file_exists(FULL_PATH.$filename)) {
				return ;
			}

			$sqls = file_get_contents(FULL_PATH.$filename);
			if (!$this->RunSQLText($sqls, $replace_from, $replace_to)) {
				if (is_object($this->_installator)) {
					$this->_installator->Done();
				}
				else {
					if (isset($this->Application)) {
						$this->Application->Done();
					}

					exit;
				}
			}
		}

		/**
		 * Runs SQLs from string
		 *
		 * @param string $sqls
		 * @param mixed $replace_from
		 * @param mixed $replace_to
		 * @param int $start_from
		 * @return bool
		 */
		function RunSQLText(&$sqls, $replace_from = null, $replace_to = null, $start_from = 0)
		{
			$table_prefix = $this->systemConfig->get('TablePrefix', 'Database');

			// add prefix to all tables
			if (strlen($table_prefix) > 0) {
				$replacements = Array ('INSERT INTO ', 'UPDATE ', 'ALTER TABLE ', 'DELETE FROM ', 'REPLACE INTO ');

				foreach ($replacements as $replacement) {
					$sqls = str_replace($replacement, $replacement . $table_prefix, $sqls);
				}
			}

			$sqls = str_replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS ' . $table_prefix, $sqls);
			$sqls = str_replace('DROP TABLE ', 'DROP TABLE IF EXISTS ' . $table_prefix, $sqls);
			$sqls = str_replace('<%TABLE_PREFIX%>', $table_prefix, $sqls);

			$primary_language = is_object($this->Application) ? $this->Application->GetDefaultLanguageId() : 1;
			$sqls = str_replace('<%PRIMARY_LANGUAGE%>', $primary_language, $sqls);

			if (isset($replace_from) && isset($replace_to)) {
				// replace something additionally, e.g. module root category
				$sqls = str_replace($replace_from, $replace_to, $sqls);
			}

			$sqls = str_replace("\r\n", "\n", $sqls);  // convert to linux line endings
			$no_comment_sqls = preg_replace("/#\s([^;]*?)\n/is", '', $sqls); // remove all comments "#" on new lines

			if ($no_comment_sqls === null) {
				// "ini.pcre.backtrack-limit" reached and error happened
				$sqls = explode(";\n", $sqls . "\n"); // ensures that last sql won't have ";" in it
				$sqls = array_map('trim', $sqls);

				// remove all comments "#" on new lines (takes about 2 seconds for 53000 sqls)
				$sqls = preg_replace("/#\s([^;]*?)/", '', $sqls);
			}
			else {
				$sqls = explode(";\n", $no_comment_sqls . "\n"); // ensures that last sql won't have ";" in it
				$sqls = array_map('trim', $sqls);
			}

			$sql_count = count($sqls);
			$db_collation = $this->systemConfig->get('DBCollation', 'Database');

			for ($i = $start_from; $i < $sql_count; $i++) {
				$sql = $sqls[$i];
				if (!$sql || (substr($sql, 0, 1) == '#')) {
					continue; // usually last line
				}

				if (substr($sql, 0, 13) == 'CREATE TABLE ' && $db_collation) {
					// it is CREATE TABLE statement -> add collation
					$sql .= ' COLLATE \'' . $db_collation . '\'';
				}

				$this->Conn->Query($sql);
				if ($this->Conn->getErrorCode() != 0) {
					if (is_object($this->_installator)) {
		  				$this->_installator->errorMessage = 'Error: ('.$this->Conn->getErrorCode().') '.$this->Conn->getErrorMsg().'<br /><br />Last Database Query:<br /><textarea cols="70" rows="10" readonly>'.htmlspecialchars($sql, ENT_QUOTES, 'UTF-8').'</textarea>';
		  				$this->_installator->LastQueryNum = $i + 1;
					}
	  				return false;
	    		}
			}
			return true;
		}

		/**
		 * Performs clean language import from given xml file
		 *
		 * @param string $lang_file
		 * @param bool $upgrade
		 * @todo Import for "core/install/english.lang" (322KB) takes 18 seconds to work on Windows
		 */
		function ImportLanguage($lang_file, $upgrade = false)
		{
			$lang_file = FULL_PATH.$lang_file.'.lang';
			if (!file_exists($lang_file)) {
				return ;
			}

			$language_import_helper = $this->Application->recallObject('LanguageImportHelper');
			/* @var $language_import_helper LanguageImportHelper */

			if ( !$upgrade ) {
				$language_import_helper->setOption(LanguageImportHelper::OVERWRITE_EXISTING);
			}

			$language_import_helper->performImport($lang_file, '|0|1|2|', '');
		}

		/**
		 * Converts module version in format X.Y.Z[-BN/-RCM] to signle integer
		 *
		 * @param string $version
		 * @return int
		 */
		function ConvertModuleVersion($version)
		{
			if (preg_match('/(.*)-(B|RC)([\d]+)/', $version, $regs)) {
				// -B<M> or RC-<N>
				$parts = explode('.', $regs[1]);

				$parts[] = $regs[2] == 'B' ? 1 : 2; // B reliases goes before RC releases
				$parts[] = $regs[3];
			}
			else {
				// releases without B/RC marks go after any B/RC releases
				$parts = explode('.', $version . '.3.100');
			}

			$bin = '';

			foreach ($parts as $part_index => $part) {
				if ($part_index == 3) {
					// version type only can be 1/2/3 (11 in binary form), so don't use padding at all
					$pad_count = 2;
				}
				else {
					$pad_count = 8;
				}

				$bin .= str_pad(decbin($part), $pad_count, '0', STR_PAD_LEFT);
			}

			return bindec($bin);
		}

		/**
		 * Returns themes, found in system
		 *
		 * @param bool $rebuild
		 * @return int
		 */
		function getThemes($rebuild = false)
		{
			if ($rebuild) {
				$this->rebuildThemes();
			}

			$theme_config = $this->Application->getUnitConfig('theme');
			$id_field = $theme_config->getIDField();

			$sql = 'SELECT Name, ' . $id_field . '
					FROM ' . $theme_config->getTableName() . '
					ORDER BY Name ASC';

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

		/**
		 * Checks if system config is present and is not empty
		 *
		 * @return bool
		 */
		function systemConfigFound()
		{
			return $this->systemConfig->exists();
		}

		/**
		 * Checks if given section is present in config
		 *
		 * @param string $section
		 * @return bool
		 */
		function sectionFound($section)
		{
			return $this->systemConfig->sectionFound($section);
		}

		/**
		 * Returns formatted module name based on it's root folder
		 *
		 * @param string $module_folder
		 * @return string
		 */
		function getModuleName($module_folder)
		{
			return implode('-', array_map('ucfirst', explode('-', $module_folder)));
		}

		/**
		 * Returns information about module (based on "install/module_info.xml" file)
		 *
		 * @param string $module_name
		 * @return Array
		 */
		function getModuleInfo($module_name)
		{
			if ( $module_name == 'core' ) {
				$info_file = FULL_PATH . '/' . $module_name . '/install/module_info.xml';
			}
			else {
				$info_file = MODULES_PATH . '/' . $module_name . '/install/module_info.xml';
			}

			if ( !file_exists($info_file) ) {
				return Array ();
			}

			$ret = Array ();
			$module_info = simplexml_load_file($info_file);

			if ( $module_info === false ) {
				// non-valid xml file
				return Array ();
			}

			foreach ($module_info as $node) {
				/* @var $node SimpleXMLElement */
				$ret[strtolower($node->getName())] = trim($node);
			}

			return $ret;
		}

		/**
		 * Returns nice module string to be used on install/upgrade screens
		 *
		 * @param string $module_name
		 * @param string $version_string
		 * @return string
		 */
		function getModuleString($module_name, $version_string)
		{
			// image (if exists) <description> (<name> <version>)

			$ret = Array ();
			$module_info = $this->getModuleInfo($module_name);

			if (array_key_exists('name', $module_info) && $module_info['name']) {
				$module_name = $module_info['name'];
			}
			else {
				$module_name = $this->getModuleName($module_name);
			}

			if (array_key_exists('image', $module_info) && $module_info['image']) {
				$image_src = $module_info['image'];

				if (!preg_match('/^(http|https):\/\//', $image_src)) {
					// local image -> make absolute url
					$image_src = $this->Application->BaseURL() . $image_src;
				}

				$ret[] = '<img src="' . $image_src . '" alt="' . htmlspecialchars($module_name, ENT_QUOTES, 'UTF-8') . '" title="' . htmlspecialchars($module_name, ENT_QUOTES, 'UTF-8') . '" style="vertical-align:middle; margin: 3px 0 3px 5px"/>';
			}

			if (array_key_exists('description', $module_info) && $module_info['description']) {
				$ret[] = $module_info['description'];
			}
			else {
				$ret[] = $module_name;
			}

			$ret[] = '(' . $module_name . ' ' . $version_string . ')';

			return implode(' ', $ret);
		}

		/**
		 * Creates module root category in "Home" category using given data and returns it
		 *
		 * @param string $name
		 * @param string $description
		 * @param string $category_template
		 * @param string $category_icon
		 * @return kDBItem
		 */
		function &createModuleCategory($name, $description, $category_template = null, $category_icon = null)
		{
			static $fields = null;

			if ( !isset($fields) ) {
				$ml_formatter = $this->Application->recallObject('kMultiLanguage');
				/* @var $ml_formatter kMultiLanguage */

				$fields['name'] = $ml_formatter->LangFieldName('Name');
				$fields['description'] = $ml_formatter->LangFieldName('Description');
			}

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

			$category_fields = Array (
				$fields['name'] => $name, 'Filename' => $name, 'AutomaticFilename' => 1,
				$fields['description'] => $description, 'Status' => STATUS_ACTIVE, 'Priority' => -9999,

				// prevents empty link to module category on spearate module install
				'NamedParentPath' => 'Content/' . $name,
			);

			$category_fields['ParentId'] = $this->Application->getBaseCategory();

			if ( isset($category_template) ) {
				$category_fields['Template'] = $category_template;
				$category_fields['CachedTemplate'] = $category_template;
			}

			if ( isset($category_icon) ) {
				$category_fields['UseMenuIconUrl'] = 1;
				$category_fields['MenuIconUrl'] = $category_icon;
			}

			$category->Clear();
			$category->SetDBFieldsFromHash($category_fields);

			$category->Create();

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

			$event = new kEvent('c:OnListBuild');

			// ensure, that newly created category has proper value in Priority field
			$priority_helper->recalculatePriorities($event, 'ParentId = ' . $category_fields['ParentId']);

			// update Priority field in object, becase "CategoriesItem::Update" method will be called
			// from "kInstallToolkit::setModuleItemTemplate" and otherwise will set 0 to Priority field
			$sql = 'SELECT Priority
					FROM ' . $category->TableName . '
					WHERE ' . $category->IDField . ' = ' . $category->GetID();
			$category->SetDBField('Priority', $this->Conn->GetOne($sql));

			return $category;
		}

		/**
		 * Sets category item template into custom field for given prefix
		 *
		 * @param kDBItem $category
		 * @param string $prefix
		 * @param string $item_template
		 */
		function setModuleItemTemplate(&$category, $prefix, $item_template)
		{
			$this->Application->removeObject('c-cdata');

			// recreate all fields, because custom fields are added during install script
			$category->Configure();

			$category->SetDBField('cust_' . $prefix  .'_ItemTemplate', $item_template);
			$category->Update();
		}

		/**
		 * Link custom field records with search config records + create custom field columns
		 *
		 * @param string $module_folder
		 * @param string $prefix
		 * @param int $item_type
		 */
		function linkCustomFields($module_folder, $prefix, $item_type)
		{
			$module_folder = strtolower($module_folder);
			$module_name = $module_folder;

			if ( $module_folder == 'kernel' ) {
				$module_name = 'in-portal';
				$module_folder = 'core';
			}

			$db =& $this->Application->GetADODBConnection();

			$sql = 'SELECT FieldName, CustomFieldId
					FROM ' . TABLE_PREFIX . 'CustomFields
					WHERE Type = ' . $item_type . ' AND IsSystem = 0'; // config is not read here yet :( $this->Application->getUnitConfig('p')->getItemType();
			$custom_fields = $db->GetCol($sql, 'CustomFieldId');

			foreach ($custom_fields as $cf_id => $cf_name) {
				$sql = 'UPDATE ' . TABLE_PREFIX . 'SearchConfig
						SET CustomFieldId = ' . $cf_id . '
						WHERE (TableName = "CustomFields") AND (LOWER(ModuleName) = "' . $module_name . '") AND (FieldName = ' . $db->qstr($cf_name) . ')';
				$db->Query($sql);
			}

			$this->Application->refreshModuleInfo(); // this module configs are now processed

			// because of configs was read only from installed before modules (in-portal), then reread configs
			$this->Application->UnitConfigReader->scanModules(MODULES_PATH . DIRECTORY_SEPARATOR . $module_folder);

			// create correct columns in CustomData table
			$ml_helper = $this->Application->recallObject('kMultiLanguageHelper');
			/* @var $ml_helper kMultiLanguageHelper */

			$ml_helper->createFields($prefix . '-cdata', true);
		}

		/**
		 * Deletes cache, useful after separate module install and installator last step
		 *
		 * @param bool $refresh_permissions
		 * @return void
		 */
		function deleteCache($refresh_permissions = false)
		{
			$this->Application->HandleEvent(new kEvent('adm:OnResetMemcache')); // not in DB = 100% invalidate
			$this->Application->HandleEvent(new kEvent('adm:OnResetConfigsCache'));
			$this->Application->HandleEvent(new kEvent('adm:OnResetSections'));
			$this->Application->HandleEvent(new kEvent('c:OnResetCMSMenuCache'));

			$this->Conn->Query('DELETE FROM ' . TABLE_PREFIX . 'CachedUrls');

			if ( $refresh_permissions ) {
				$rebuild_mode = $this->Application->ConfigValue('CategoryPermissionRebuildMode');

				if ( $rebuild_mode == CategoryPermissionRebuild::SILENT ) {
					// refresh permission without progress bar
					$updater = $this->Application->makeClass('kPermCacheUpdater');
					/* @var $updater kPermCacheUpdater */

					$updater->OneStepRun();
				}
				elseif ( $rebuild_mode == CategoryPermissionRebuild::AUTOMATIC ) {
					// refresh permissions with ajax progress bar (when available)
					$this->Application->setDBCache('ForcePermCacheUpdate', 1);
				}
			}
		}

		/**
		 * Deletes all temp tables (from active sessions too)
		 *
		 */
		function deleteEditTables()
		{
			$table_prefix = $this->systemConfig->get('TablePrefix', 'Database');

			$tables = $this->Conn->GetCol('SHOW TABLES');
			$mask_edit_table = '/' . $table_prefix . 'ses_(.*)_edit_(.*)/';
			$mask_search_table = '/' . $table_prefix . 'ses_(.*?)_(.*)/';

			foreach ($tables as $table) {
				if ( preg_match($mask_edit_table, $table, $rets) || preg_match($mask_search_table, $table, $rets) ) {
					$this->Conn->Query('DROP TABLE IF EXISTS ' . $table);
				}
			}
		}

		/**
		 * Perform redirect after separate module install
		 *
		 * @param string $module_folder
		 * @param bool $refresh_permissions
		 */
		function finalizeModuleInstall($module_folder, $refresh_permissions = false)
		{
			$this->SetModuleVersion(basename($module_folder), $module_folder);

			if (!$this->Application->GetVar('redirect')) {
				return ;
			}

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

			// use direct query, since module isn't yet in kApplication::ModuleInfo array
			$sql = 'SELECT Name
					FROM ' . TABLE_PREFIX . 'Modules
					WHERE Path = ' . $this->Conn->qstr(rtrim($module_folder, '/') . '/');
			$module_name = $this->Conn->GetOne($sql);

			$themes_helper->synchronizeModule($module_name);

			$ml_helper = $this->Application->recallObject('kMultiLanguageHelper');
			/* @var $ml_helper kMultiLanguageHelper */

			$ml_helper->massCreateFields();

			$this->deleteCache($refresh_permissions);

			$url_params = Array (
				'pass' => 'm', 'admin' => 1,
				'RefreshTree' => 1, 'index_file' => 'index.php',
			);
			$this->Application->Redirect('modules/modules_list', $url_params);
		}

		/**
		 * Performs rebuild of themes
		 *
		 */
		function rebuildThemes()
		{
			$this->Application->HandleEvent(new kEvent('adm:OnRebuildThemes'));
		}

		/**
		 * Checks that file is writable by group or others
		 *
		 * @param string $file
		 * @return boolean
		 */
		function checkWritePermissions($file)
		{
			if (DIRECTORY_SEPARATOR == '\\') {
				// windows doen't allow to check permissions (always returns null)
				return null;
			}

			$permissions = fileperms($file);
			return $permissions & 0x0010 || $permissions & 0x0002;
		}

		/**
		 * Upgrades primary skin to the latest version
		 *
		 * @param Array $module_info
		 * @return string|bool
		 */
		function upgradeSkin($module_info)
		{
			$upgrades_file = sprintf(UPGRADES_FILE, $module_info['Path'], 'css');
			$data = file_get_contents($upgrades_file);

			// get all versions with their positions in file
			$versions = Array ();
			preg_match_all('/(' . VERSION_MARK . ')/s', $data, $matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
			$from_version_int = $this->ConvertModuleVersion($module_info['FromVersion']);

			foreach ($matches as $index => $match) {
				$version_int = $this->ConvertModuleVersion($match[2][0]);

				if ( $version_int < $from_version_int ) {
					// only process versions, that were released after currently used version
					continue;
				}

				$start_pos = $match[0][1] + strlen($match[0][0]);
				$end_pos = array_key_exists($index + 1, $matches) ? $matches[$index + 1][0][1] : mb_strlen($data);
				$patch_data = str_replace("\r\n", "\n", substr($data, $start_pos, $end_pos - $start_pos));

				$versions[] = Array (
					'Version' => $match[2][0],
					// fixes trimmed leading spaces by modern text editor
					'Data' => ltrim( str_replace("\n\n", "\n \n", $patch_data) ),
				);
			}

			if ( !$versions ) {
				// not skin changes -> quit
				return true;
			}

			$primary_skin = $this->Application->recallObject('skin.primary', null, Array ('skip_autoload' => true));
			/* @var $primary_skin kDBItem */

			$primary_skin->Load(1, 'IsPrimary');

			if ( !$primary_skin->isLoaded() ) {
				// we always got primary skin, but just in case
				return false;
			}

			$temp_handler = $this->Application->recallObject('skin_TempHandler', 'kTempTablesHandler');
			/* @var $temp_handler kTempTablesHandler */

			// clone current skin
			$cloned_ids = $temp_handler->CloneItems('skin', '', Array ($primary_skin->GetID()));

			if ( !$cloned_ids ) {
				// can't clone
				return false;
			}

			$skin = $this->Application->recallObject('skin.tmp', null, Array ('skip_autoload' => true));
			/* @var $skin kDBItem */

			$skin->Load($cloned_ids[0]);

			// save css to temp file (for patching)
			$skin_file = tempnam('/tmp', 'skin_css_');
			$fp = fopen($skin_file, 'w');
			fwrite($fp, str_replace("\r\n", "\n", $skin->GetDBField('CSS')));
			fclose($fp);

			$output = Array ();
			$patch_file = tempnam('/tmp', 'skin_patch_');

			foreach ($versions as $version_info) {
				// for each left version get it's patch and apply to temp file
				$fp = fopen($patch_file, 'w');
				fwrite($fp, $version_info['Data']);
				fclose($fp);

				$output[ $version_info['Version'] ] = shell_exec('patch ' . $skin_file . ' ' . $patch_file . ' 2>&1') . "\n";
			}

			// place temp file content into cloned skin
			$skin->SetDBField('Name', 'Upgraded to ' . $module_info['ToVersion']);
			$skin->SetDBField('CSS', file_get_contents($skin_file));
			$skin->Update();

			unlink($skin_file);
			unlink($patch_file);

			$has_errors = false;

			foreach ($output as $version => $version_output) {
				$version_errors = trim(preg_replace("/(^|\n)(patching file .*?|Hunk #.*?\.)(\n|$)/m", '', $version_output));

				if ( $version_errors ) {
					$has_errors = true;
					$output[$version] = trim(preg_replace("/(^|\n)(patching file .*?)(\n|$)/m", '', $output[$version]));
				}
				else {
					unset($output[$version]);
				}
			}

			if ( !$has_errors ) {
				// copy patched css back to primary skin
				$primary_skin->SetDBField('CSS', $skin->GetDBField('CSS'));
				$primary_skin->Update();

				// delete temporary skin record
				$temp_handler->DeleteItems('skin', '', Array ($skin->GetID()));

				return true;
			}

			// put clean skin from new version
			$skin->SetDBField('CSS', file_get_contents(FULL_PATH . '/core/admin_templates/incs/style_template.css'));
			$skin->Update();

			// return output in case of errors
			return $output;
		}

		/**
		 * Returns cache handlers, that are working
		 *
		 * @param string $current
		 * @return Array
		 */
		public function getWorkingCacheHandlers($current = null)
		{
			if ( !isset($current) ) {
				$current = $this->systemConfig->get('CacheHandler', 'Misc');
			}

			$cache_handler = $this->Application->makeClass('kCache');

			$cache_handlers = Array (
				'Fake' => 'None', 'Memcache' => 'Memcached', 'XCache' => 'XCache', 'Apc' => 'Alternative PHP Cache'
			);

			foreach ($cache_handlers AS $class_prefix => $title) {
				$handler_class = $class_prefix . 'CacheHandler';

				if ( !class_exists($handler_class) ) {
					unset($cache_handlers[$class_prefix]);
				}
				else {
					$handler = new $handler_class($cache_handler, 'localhost:11211');
					/* @var $handler FakeCacheHandler */

					if ( !$handler->isWorking() ) {
						if ( $current == $class_prefix ) {
							$cache_handlers[$class_prefix] .= ' (offline)';
						}
						else {
							unset($cache_handlers[$class_prefix]);
						}
					}
				}
			}

			return $cache_handlers;
		}

		/**
		 * Returns compression engines, that are working
		 *
		 * @param string $current
		 * @return Array
		 */
		public function getWorkingCompressionEngines($current = null)
		{
			if ( !isset($current) ) {
				$current = $this->systemConfig->get('CompressionEngine', 'Misc');
			}

			$output = shell_exec('java -version 2>&1');
			$compression_engines = Array ('' => 'None', 'yui' => 'YUICompressor (Java)', 'php' => 'PHP-based');

			if ( stripos($output, 'java version') === false ) {
				if ( $current == 'yui' ) {
					$compression_engines['yui'] .= ' (offline)';
				}
				else {
					unset($compression_engines['yui']);
				}
			}

			return $compression_engines;
		}
	}