<?php
/**
* @version	$Id: themes_helper.php 16588 2017-07-19 07:36:53Z 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 ();

		public function __construct()
		{
			parent::__construct();

			$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 ();
			$theme_path = $this->themesFolder . '/' . $theme_name;

			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
				$config = $this->getConfiguration($theme_path);

				$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,
					 'StylesheetFile' => isset($config['stylesheet_file']) ? $config['stylesheet_file'] : '',
				);

				$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;
			$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|bool $module_name
		 * @return void
		 */
		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]);
			}

			/** @var LanguageImportHelper $language_import_helper */
			$language_import_helper = $this->Application->recallObject('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
		 * @return Array
		 * @access protected
		 */
		protected function getTemplateAliases($theme_id, $theme_path)
		{
			$template_aliases = Array ();

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				$xml_file = $theme_path . '/' . $module_info['TemplatePath'] . '_install/theme.xml';

				if ( $module_name == 'In-Portal' || !file_exists($xml_file) ) {
					continue;
				}

				$theme = simplexml_load_file($xml_file);

				if ( $theme === false ) {
					// broken xml OR no aliases defined
					continue;
				}

				foreach ($theme as $design) {
					/** @var SimpleXMLElement $design */

					$template_path = trim($design);
					$module_override = (string)$design['module'];

					if ( $module_override ) {
						// allow to put template mappings form all modules into single theme.xml file
						$module_folder = $this->Application->findModule('Name', $module_override, 'TemplatePath');
					}
					else {
						// no module specified -> use module based on theme.xml file location
						$module_folder = $module_info['TemplatePath'];
					}

					// only store alias, when template exists on disk
					if ( $this->getTemplateId($template_path, $theme_id) ) {
						$alias = '#' . $module_folder . strtolower($design->getName()) . '#';

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

						// store alias in theme file record to use later in design dropdown
						$this->updateTemplate($template_path, $theme_id, Array ('TemplateAlias' => $alias));
					}
				}
			}

			return $template_aliases;
		}

		/**
		 * Returns theme configuration.
		 *
		 * @param string $theme_path Absolute path to theme.
		 *
		 * @return array
		 */
		protected function getConfiguration($theme_path)
		{
			$xml_file = $theme_path . '/_install/theme.xml';

			if ( !file_exists($xml_file) ) {
				return array();
			}

			$theme = simplexml_load_file($xml_file);

			if ( $theme === false ) {
				// broken xml OR no aliases defined
				return array();
			}

			$ret = array();

			foreach ( $theme->attributes() as $name => $value ) {
				$ret[(string)$name] = (string)$value;
			}

			return $ret;
		}

		/**
		 * Returns ID of given physical template (relative to theme) given from ThemeFiles table
		 * @param string $template_path
		 * @param int $theme_id
		 * @return int
		 * @access public
		 */
		public function getTemplateId($template_path, $theme_id)
		{
			$template_path = $this->Application->getPhysicalTemplate($template_path);

			$sql = 'SELECT FileId
					FROM ' . TABLE_PREFIX . 'ThemeFiles
					WHERE ' . $this->getTemplateWhereClause($template_path, $theme_id);

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

		/**
		 * Updates template record with a given data
		 *
		 * @param string $template_path
		 * @param int $theme_id
		 * @param Array $fields_hash
		 * @return void
		 * @access public
		 */
		public function updateTemplate($template_path, $theme_id, $fields_hash)
		{
			$where_clause = $this->getTemplateWhereClause($template_path, $theme_id);
			$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'ThemeFiles', $where_clause);
		}

		/**
		 * Returns where clause to get associated record from ThemeFiles table for given template path
		 * @param string $template_path
		 * @param int $theme_id
		 * @return string
		 * @access protected
		 */
		protected function getTemplateWhereClause($template_path, $theme_id)
		{
			$folder = dirname($template_path);

			$where_clause = Array (
				'ThemeId = ' . $theme_id,
				'FilePath = ' . $this->Conn->qstr($folder == '.' ? '' : '/' . $folder),
				'FileName = ' . $this->Conn->qstr(basename($template_path) . '.tpl'),
			);

			return '(' . implode(') AND (', $where_clause) . ')';
		}

		/**
		 * Installs given module language pack and refreshed it from all themes
		 *
		 * @param string $module_name
		 */
		function synchronizeModule($module_name)
		{
			$sql = 'SELECT `Name`, ThemeId
					FROM ' . TABLE_PREFIX . 'Themes';
			$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 . 'Themes', 'ThemeId = ' . $theme_id);
			}
		}

		/**
		 * Searches for new templates (missing in db) in specified 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
		 * @param int $auto_structure_mode
		 */
		function FindThemeFiles($folder_path, $theme_path, $theme_id, $auto_structure_mode = 1)
		{
			$ignore_regexp = $this->getIgnoreRegexp($theme_path . $folder_path);

			$iterator = new DirectoryIterator($theme_path . $folder_path . '/');
			/** @var DirectoryIterator $file_info */

			foreach ($iterator as $file_info) {
				$filename = $file_info->getFilename();
				$auto_structure = preg_match($ignore_regexp, $filename) ? 2 : $auto_structure_mode;
				$file_path = $folder_path . '/' . $filename; // don't pass path to theme top folder!

				if ( $file_info->isDir() && !$file_info->isDot() && $filename != 'CVS' && $filename != '.svn' ) {
					$this->FindThemeFiles($file_path, $theme_path, $theme_id, $auto_structure);
				}
				elseif ( pathinfo($filename, PATHINFO_EXTENSION) == 'tpl' ) {
					$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 single regular expression to match all ignore patters, that are valid for given folder
		 *
		 * @param string $folder_path
		 * @return string
		 */
		protected function getIgnoreRegexp($folder_path)
		{
			// always ignore design and element templates
			$ignore = '\.des\.tpl$|\.elm\.tpl$';
			$sms_ignore_file = $folder_path . '/.smsignore';

			if ( file_exists($sms_ignore_file) ) {
				$manual_patterns = array_map('trim', file($sms_ignore_file));

				foreach ($manual_patterns as $manual_pattern) {
					$ignore .= '|' . str_replace('/', '\\/', $manual_pattern);
				}
			}

			return '/' . $ignore . '/';
		}

		/**
		 * 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 ();

			try {
				$iterator = new DirectoryIterator($this->themesFolder . '/');
				/** @var DirectoryIterator $file_info */

				foreach ($iterator as $file_info) {
					$filename = $file_info->getFilename();

					if ( $file_info->isDir() && !$file_info->isDot() && $filename != '.svn' && $filename != 'CVS' ) {
						$theme_id = $this->refreshTheme($filename);

						if ( $theme_id ) {
							$themes_found[] = $theme_id;
							// increment serial of updated themes
							$this->Application->incrementCacheSerial('theme', $theme_id);
						}
					}
				}
			}
			catch ( UnexpectedValueException $e ) {
			}

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

		/**
		 * 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();
			}

			$template_crc = kUtil::crc32(mb_strtolower($template));

			$sql = 'SELECT ' . $this->Application->getUnitOption('c', 'IDField') . '
					FROM ' . $this->Application->getUnitOption('c', 'TableName') . '
					WHERE
						(
							(NamedParentPathHash = ' . $template_crc . ') OR
							(`Type` = ' . PAGE_TYPE_TEMPLATE . ' AND CachedTemplateHash = ' . $template_crc . ')
						)
						AND (ThemeId = ' . $theme_id . ($theme_id > 0 ? ' OR ThemeId = 0' : '') . ')';

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