<?php
/**
* @version	$Id: themes_helper.php 14241 2011-03-16 20:24:35Z 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 kThemesHelper extends kHelper {

		/**
		 * Where all themes are located
		 *
		 * @var string
		 */
		var $themesFolder = '';

		/**
		 * List of theme names, found on system
		 *
		 * @var Array
		 */
		var $_themeNames = Array ();

		/**
		 * Temporary array when all theme files from db are stored
		 *
		 * @var Array
		 */
		var $themeFiles = Array ();

		function kThemesHelper()
		{
			parent::kHelper();
			$this->themesFolder = FULL_PATH.'/themes';
		}

		/**
		 * Updates file system changes to database for selected theme
		 *
		 * @param string $theme_name
		 *
		 * @return mixed returns ID of created/used theme or false, if none created
		 */
		function refreshTheme($theme_name)
		{
			if (!file_exists($this->themesFolder . '/' . $theme_name)) {
				// requested theme was not found on hdd
				return false;
			}

			$id_field = $this->Application->getUnitOption('theme', 'IDField');
			$table_name = $this->Application->getUnitOption('theme', 'TableName');

			$sql = 'SELECT *
					FROM ' . $table_name . '
					WHERE Name = ' . $this->Conn->qstr($theme_name);
			$theme_info = $this->Conn->GetRow($sql);

			if ($theme_info) {
				$theme_id = $theme_info[$id_field];
				$theme_enabled = $theme_info['Enabled'];
			}
			else {
				$theme_id = $theme_enabled = false;
			}

			$this->themeFiles = Array ();
			if ($theme_id) {
				if (!$theme_enabled) {
					// don't process existing theme files, that are disabled
					return $theme_id;
				}

				// reset found mark for every themes file (if theme is not new)
				$sql = 'UPDATE '.TABLE_PREFIX.'ThemeFiles
						SET FileFound = 0
						WHERE ThemeId = '.$theme_id;
				$this->Conn->Query($sql);

				// get all theme files from db
				$sql = 'SELECT FileId, CONCAT(FilePath, "/", FileName) AS FullPath
						FROM '.TABLE_PREFIX.'ThemeFiles
						WHERE ThemeId = '.$theme_id;
				$this->themeFiles = $this->Conn->GetCol($sql, 'FullPath');
			}
			else {
				// theme was not found in db, but found on hdd -> create new
				$theme_info = Array (
					 'Name' => $theme_name,
					 'Enabled' => 0,
					 'Description' => $theme_name,
					 'PrimaryTheme' => 0,
					 'CacheTimeout' => 3600, // not in use right now
					 'StylesheetId' => 0,	  // not in use right now
					 'LanguagePackInstalled' => 0
				);

				$this->Conn->doInsert($theme_info, $table_name);
				$theme_id = $this->Conn->getInsertID();

				if (!$theme_enabled) {
					// don't process newly created theme files, because they are disabled
					return $theme_id;
				}
			}

			$this->_themeNames[$theme_id] = $theme_name;
			$theme_path = $this->themesFolder.'/'.$theme_name;
			$this->FindThemeFiles('', $theme_path, $theme_id); // search from base theme directory

			// delete file records from db, that were not found on hdd
			$sql = 'DELETE FROM '.TABLE_PREFIX.'ThemeFiles
					WHERE ThemeId = '.$theme_id.' AND FileFound = 0';
			$this->Conn->Query($sql);

			// install language packs, associated with theme (if any found and wasn't aready installed)
			if (!$theme_info['LanguagePackInstalled']) {
				$this->installThemeLanguagePack($theme_path);

				$fields_hash = Array (
					'LanguagePackInstalled' => 1,
				);

				$this->Conn->doUpdate($fields_hash, $table_name, $id_field . ' = ' . $theme_id);
			}

			$fields_hash = Array (
				'TemplateAliases' => serialize( $this->getTemplateAliases($theme_id, $theme_path) ),
			);

			$this->Conn->doUpdate($fields_hash, $table_name, $id_field . ' = ' . $theme_id);

			return $theme_id;
		}

		/**
		 * Installs module(-s) language pack for given theme
		 *
		 * @param string $theme_path
		 * @param string $module_name
		 */
		function installThemeLanguagePack($theme_path, $module_name = false)
		{
			if ($module_name === false) {
				$modules = $this->Application->ModuleInfo;
			}
			else {
				$modules = Array ($module_name => $this->Application->ModuleInfo[$module_name]);
			}

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

			foreach ($modules as $module_name => $module_info) {
				if ($module_name == 'In-Portal') {
					continue;
				}

				$lang_file = $theme_path . '/' . $module_info['TemplatePath'] . '_install/english.lang';

				if (file_exists($lang_file)) {
					$language_import_helper->performImport($lang_file, '|0|', '', LANG_SKIP_EXISTING);
				}
			}
		}

		/**
		 * Returns template aliases from "/_install/theme.xml" files in theme
		 *
		 * @param int $theme_id
		 * @param string $theme_path
		 */
		function getTemplateAliases($theme_id, $theme_path)
		{
			$template_aliases = Array ();

			$xml_parser =& $this->Application->recallObject('kXMLHelper');
			/* @var $xml_parser kXMLHelper */

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				if ($module_name == 'In-Portal') {
					continue;
				}

				$xml_file = $theme_path . '/' . $module_info['TemplatePath'] . '_install/theme.xml';

				if (file_exists($xml_file)) {
					$xml_data = file_get_contents($xml_file);
					$root_node =& $xml_parser->Parse($xml_data);

					if (!is_object($root_node) || !is_a($root_node, 'kXMLNode') || !$root_node->Children) {
						// broken xml OR no aliases defined
						continue;
					}

					$current_node =& $root_node->firstChild;

					do {
						$template_path = trim($current_node->Data);
						$alias = '#' . $module_info['TemplatePath'] . strtolower($current_node->Name). '#';

						// remember alias in global theme mapping
						$template_aliases[$alias] = $template_path;

						// store alias in theme file record to use later in design dropdown
						$t_parts = Array (
							'path' => dirname($template_path) == '.' ? '' : '/' . dirname($template_path),
							'file' => basename($template_path),
						);

						$sql = 'UPDATE ' . TABLE_PREFIX . 'ThemeFiles
								SET TemplateAlias = ' . $this->Conn->qstr($alias) . '
								WHERE (ThemeId = ' . $theme_id . ') AND (FilePath = ' . $this->Conn->qstr($t_parts['path']) . ') AND (FileName = ' . $this->Conn->qstr($t_parts['file'] . '.tpl') . ')';
						$this->Conn->Query($sql);
					} while (($current_node =& $current_node->NextSibling()));
				}
			}

			return $template_aliases;
		}

		/**
		 * Installs given module language pack and refreshed it from all themes
		 *
		 * @param string $module_name
		 */
		function syncronizeModule($module_name)
		{
			$sql = 'SELECT `Name`, ThemeId
					FROM ' . TABLE_PREFIX . 'Theme';
			$themes = $this->Conn->GetCol($sql, 'ThemeId');

			if (!$themes) {
				return ;
			}

			foreach ($themes as $theme_id => $theme_name) {
				$theme_path = $this->themesFolder . '/' . $theme_name;

				// install language pack
				$this->installThemeLanguagePack($theme_path, $module_name);

				// update TemplateAliases mapping
				$fields_hash = Array (
					'TemplateAliases' => serialize( $this->getTemplateAliases($theme_id, $theme_path) ),
				);

				$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Theme', 'ThemeId = ' . $theme_id);
			}
		}

		/**
		 * Searches for new templates (missing in db) in spefied folder
		 *
		 * @param string $folder_path subfolder of searchable theme
		 * @param string $theme_path theme path from web server root
		 * @param int $theme_id id of theme we are scanning
		 */
		function FindThemeFiles($folder_path, $theme_path, $theme_id, $auto_structure_mode = 1)
		{
			$fh = opendir($theme_path.$folder_path.'/');

			// always ingore design and element templates
			$ignore = Array ('^CVS$', '^\.svn$', '\.des\.tpl$', '\.elm\.tpl$');

			$sms_ingore = $theme_path . $folder_path . '/.smsignore';

			if (file_exists($sms_ingore)) {
				$ignore = array_merge($ignore, file($sms_ingore));
			}

			while (($filename = readdir($fh))) {
				if ($filename == '.' || $filename == '..') continue;

				$auto_structure = $auto_structure_mode;
				foreach ($ignore as $pattern) {
					if (preg_match('/'.str_replace('/', '\\/', trim($pattern)).'/', $filename)) {
						$auto_structure = 2;
						break;
					}
				}

				$full_path = $theme_path.$folder_path.'/'.$filename;
				if (is_dir($full_path)) {
					$this->FindThemeFiles($folder_path.'/'.$filename, $theme_path, $theme_id, $auto_structure);
				}
				elseif (substr($filename, -4) == '.tpl') {
					$file_path = $folder_path.'/'.$filename;

					$meta_info = $this->_getTemplateMetaInfo(trim($file_path, '/'), $theme_id);
					$file_id = isset($this->themeFiles[$file_path]) ? $this->themeFiles[$file_path] : false;
					$file_description = array_key_exists('desc', $meta_info) ? $meta_info['desc'] : '';

					if ($file_id) {
						// file was found in db & on hdd -> mark as existing
						$fields_hash = Array (
							'FileFound' => 1,
							'Description' => $file_description,
							'FileType' => $auto_structure,
							'FileMetaInfo' => serialize($meta_info),
						);

						$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'ThemeFiles', 'FileId = ' . $file_id);
					}
					else {
						// file was found on hdd, but missing in db -> create new file record
						$fields_hash = Array (
							'ThemeId' => $theme_id,
							'FileName' => $filename,
							'FilePath' => $folder_path,
							'Description' => $file_description,
							'FileType' => $auto_structure, // 1 - built-in, 0 - custom (not in use right now), 2 - skipped in structure
							'FileMetaInfo' => serialize($meta_info),
							'FileFound' => 1,
						);

						$this->Conn->doInsert($fields_hash, TABLE_PREFIX.'ThemeFiles');
						$this->themeFiles[$file_path] = $this->Conn->getInsertID();
					}
//					echo 'FilePath: [<strong>'.$folder_path.'</strong>]; FileName: [<strong>'.$filename.'</strong>]; IsNew: [<strong>'.($file_id > 0 ? 'NO' : 'YES').'</strong>]<br />';
				}

			}
		}

		/**
		 * Returns template information (name, description, path) from it's header comment
		 *
		 * @param string $template
		 * @param int $theme_id
		 * @return Array
		 */
		function _getTemplateMetaInfo($template, $theme_id)
		{
			static $init_made = false;
			if (!$init_made) {
				$this->Application->InitParser(true);
				$init_made = true;
			}

			$template = 'theme:' . $this->_themeNames[$theme_id] . '/' . $template;
			$template_file = $this->Application->TemplatesCache->GetRealFilename($template); // ".tpl" was added before

			return $this->parseTemplateMetaInfo($template_file);
		}

		function parseTemplateMetaInfo($template_file)
		{
			if (!file_exists($template_file)) {
				// when template without info it's placed in top category
				return Array ();
			}

			$template_data = file_get_contents($template_file);

			if (substr($template_data, 0, 6) == '<!--##') {
				// template starts with comment in such format
				/*<!--##
				<NAME></NAME>
				<DESC></DESC>
				<SECTION>||</SECTION>
				##-->*/

				$comment_end = strpos($template_data, '##-->');
				if ($comment_end === false) {
					// badly formatted comment
					return Array ();
				}

				$comment = trim( substr($template_data, 6, $comment_end - 6) );
				if (preg_match_all('/<(NAME|DESC|SECTION)>(.*?)<\/(NAME|DESC|SECTION)>/is', $comment, $regs)) {
					$ret = Array ();
					foreach ($regs[1] as $param_order => $param_name) {
						$ret[ strtolower($param_name) ] = trim($regs[2][$param_order]);
					}

					if (array_key_exists('section', $ret) && $ret['section']) {
						$category_path = explode('||', $ret['section']);
						$category_path = array_map('trim', $category_path);
						$ret['section'] = implode('||', $category_path);
					}

					return $ret;
				}
			}

			return Array ();
		}

		/**
		 * Updates file system changes to database for all themes (including new ones)
		 *
		 */
		function refreshThemes()
		{
			$themes_found = Array();
			$skip_filenames = Array ('.', '..', 'CVS', '.svn');

			$fh = opendir($this->themesFolder.'/');
			while (($filename = readdir($fh))) {
				if (in_array($filename, $skip_filenames)) {
					continue;
				}

				if (is_dir($this->themesFolder.'/'.$filename)) {
					$theme_id = $this->refreshTheme($filename);
					if ($theme_id) {
						$themes_found[] = $theme_id;
						// increment serial of updated themes
						$this->Application->incrementCacheSerial('theme', $theme_id);
					}
				}
			}

			$id_field = $this->Application->getUnitOption('theme', 'IDField');
			$table_name = $this->Application->getUnitOption('theme', 'TableName');

			// 1. only one theme found -> enable it and make primary
			/*if (count($themes_found) == 1) {
				$sql = 'UPDATE ' . $table_name . '
						SET Enabled = 1, PrimaryTheme = 1
						WHERE ' . $id_field . ' = ' . current($themes_found);
				$this->Conn->Query($sql);
			}*/

			// 2. if none themes found -> delete all from db OR delete all except of found themes
			$sql = 'SELECT '.$id_field.'
					FROM '.$table_name;
			if ($themes_found) {
				$sql .= ' WHERE '.$id_field.' NOT IN ('.implode(',', $themes_found).')';
			}
			$theme_ids = $this->Conn->GetCol($sql);
			$this->deleteThemes($theme_ids);

			foreach ($theme_ids as $theme_id) {
				// increment serial of deleted themes
				$this->Application->incrementCacheSerial('theme', $theme_id);
			}

			$this->Application->incrementCacheSerial('theme');
			$this->Application->incrementCacheSerial('theme-file');

			$minify_helper =& $this->Application->recallObject('MinifyHelper');
			/* @var $minify_helper MinifyHelper */

			$minify_helper->delete();
		}

		/**
		 * Deletes themes with ids passed from db
		 *
		 * @param Array $theme_ids
		 */
		function deleteThemes($theme_ids)
		{
			if (!$theme_ids) {
				return ;
			}

			$id_field = $this->Application->getUnitOption('theme', 'IDField');
			$table_name = $this->Application->getUnitOption('theme', 'TableName');

			$sql = 'DELETE FROM '.$table_name.'
					WHERE '.$id_field.' IN ('.implode(',', $theme_ids).')';
			$this->Conn->Query($sql);

			$sql = 'DELETE FROM '.TABLE_PREFIX.'ThemeFiles
					WHERE '.$id_field.' IN ('.implode(',', $theme_ids).')';
			$this->Conn->Query($sql);
		}

		/**
		 * Returns current theme (also works in admin)
		 *
		 * @return int
		 */
		function getCurrentThemeId()
		{
			static $theme_id = null;

			if (isset($theme_id)) {
				return $theme_id;
			}

			if ($this->Application->isAdmin) {
				// get theme, that user selected in catalog
				$theme_id = $this->Application->RecallVar('theme_id');

				if ($theme_id === false) {
					// query, because "m_theme" is always empty in admin
					$id_field = $this->Application->getUnitOption('theme', 'IDField');
					$table_name = $this->Application->getUnitOption('theme', 'TableName');

					$sql = 'SELECT ' . $id_field . '
							FROM ' . $table_name . '
							WHERE (PrimaryTheme = 1) AND (Enabled = 1)';
					$theme_id = $this->Conn->GetOne($sql);
				}

				return $theme_id;
			}

			// use current theme, because it's available on Front-End
			$theme_id = $this->Application->GetVar('m_theme');
			if (!$theme_id) {
				// happens in mod-rewrite mode, then requested template is not found
				$theme_id = $this->Application->GetDefaultThemeId();
			}

			return $theme_id;
		}

		/**
		 * Returns page id based on given template
		 *
		 * @param string $template
		 * @param int $theme_id
		 * @return int
		 */
		function getPageByTemplate($template, $theme_id = null)
		{
			if (!isset($theme_id)) {
				// during mod-rewrite url parsing current theme
				// is not available to kHTTPQuery class, so don't use it
				$theme_id = (int)$this->getCurrentThemeId();
			}

			$sql = 'SELECT ' . $this->Application->getUnitOption('c', 'IDField') . '
					FROM ' . $this->Application->getUnitOption('c', 'TableName') . '
					WHERE
						(
							(NamedParentPath = ' . $this->Conn->qstr('Content/' . $template) . ') OR
							(`Type` = ' . PAGE_TYPE_TEMPLATE . ' AND CachedTemplate = ' . $this->Conn->qstr($template) . ')
						)
						AND (ThemeId = ' . $theme_id . ($theme_id > 0 ? ' OR ThemeId = 0' : '') . ')';

			return $this->Conn->GetOne($sql);
		}
	}