<?php
/**
* @version	$Id: template_helper.php 12893 2009-11-10 12:27:10Z 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.
*/

	class TemplateHelper extends kHelper {

		/**
		 * parser element location information
		 *
		 * @var Array
		 */
		var $_blockLocation = Array ();

		/**
		 * Block name, that will be used
		 *
		 * @var string
		 */
		var $_blockName = '';

		/**
		 * Function name, that represents compiled block
		 *
		 * @var sting
		 */
		var $_functionName = '';

		/**
		 * Errors found during template parsing
		 *
		 * @var Array
		 */
		var $_parseErrors = Array ();

		/**
		 * Source template, that is being edited
		 *
		 * @var string
		 */
		var $_sourceTemplate = '';

		var $_initMade = false;

		/**
		 * Performs init ot helper
		 *
		 * @param kDBItem $object
		 */
		function InitHelper(&$object)
		{
			if ($this->_initMade) {
				return ;
			}

			define('DBG_IGNORE_FATAL_ERRORS', 1);

			// 1. get block information
			$block_info = $this->Application->GetVar('block');
			list ($this->_blockName, $this->_functionName) = explode(':', $block_info);

			$this->_parseTemplate($object);

			if (array_key_exists($this->_functionName, $this->Application->Parser->ElementLocations)) {
				$this->_blockLocation = $this->Application->Parser->ElementLocations[$this->_functionName];
			}

			$this->_initMade = true;
		}

		function _getSourceTemplate()
		{
			// get source template
			$t = $this->Application->GetVar('source');

			if (!$this->Application->TemplatesCache->TemplateExists($t)) {
				$cms_handler =& $this->Application->recallObject('st_EventHandler');
				/* @var $cms_handler StructureEventHandler */

				$t = ltrim($cms_handler->GetDesignTemplate($t), '/');
			}

			$this->_sourceTemplate = $t;
		}

		function _getThemeName()
		{
			$theme_id = (int)$this->Application->GetVar('theme_id');

			$sql = 'SELECT Name
					FROM ' . $this->Application->getUnitOption('theme', 'TableName') . '
					WHERE ' . $this->Application->getUnitOption('theme', 'IDField') . ' = ' . $theme_id;
			return $this->Conn->GetOne($sql);
		}

		/**
		 * Render source template to get parse errors OR it's element locations
		 *
		 * @param kDBItem $object
		 * @param string $append
		 * @return bool
		 */
		function _parseTemplate(&$object, $append = '')
		{
			// 1. set internal error handler to catch all parsing errors
			$error_handlers = $this->Application->errorHandlers;
			$this->Application->errorHandlers = Array (
				Array (&$this, '_saveError'),
			);

			// 2. parse template
			$this->Application->InitParser( $this->_getThemeName() ); // we have no parser when saving block content

			$this->_getSourceTemplate();

			// design templates have leading "/" in the beginning
			$this->Application->Parser->Run($this->_sourceTemplate . $append);

			// 3. restore original error handler
			$this->Application->errorHandlers = $error_handlers;

			if ($this->_parseErrors) {
				if ($this->_isMainTemplate()) {
					// 3.1. delete temporary file, that was parsed
					$filename = $this->_getTemplateFile(false, $append . '.tpl');
					if (!unlink($filename)) {
						$error_file = $this->_getTemplateFile(true, $append . '.tpl');
						$object->SetError('FileContents', 'template_delete_failed', '+Failed to delete temporary template "<strong>' . $error_file . '</strong>"');
						return false;
					}
				}
				else {
					// 3.2. restore backup
					if (!rename($this->_getTemplateFile(false, '.tpl.bak'), $this->_getTemplateFile(false))) {
						$error_file = $this->_getTemplateFile(true);
						$object->SetError('FileContents', 'template_restore_failed', '+Failed to restore template "<strong>' . $error_file . '</strong>" from backup.');
						return false;
					}
				}

				return false;
			}

			return true;
		}

		/**
		 * Move elements in template and save changes, when possible
		 *
		 * @param Array $target_order
		 * @return bool
		 */
		function moveTemplateElements($target_order)
		{
			// 2. parse template
			$this->Application->InitParser(); // we have no parser when saving block content

			$this->_getSourceTemplate();

			$filename = $this->Application->TemplatesCache->GetRealFilename($this->_sourceTemplate) . '.tpl';
			if (!is_writable($filename)) {
				// we can't save changes, don't bother calculating new template contents
				return false;
			}

			$data = file_get_contents($filename);

			$line_ending = strpos($data, "\r") !== false ? "\r\n" : "\n";

			// 1. get location of movable areas
			$mask = '';
			$start_pos = 0;
			$elements = Array ();
			$areas = $this->_getDivPairs($data, 'movable-area');
			foreach ($areas as $area_index => $area) {
				// 1.1. get locations of all movable elements inside given area
				$area_content = substr($area['data'], $area['open_len'], -$area['close_len']);
				$elements = array_merge($elements, $this->_getDivPairs($area_content, 'movable-element', $area_index, $area['open_pos'] + $area['open_len']));

				// 1.2. prepare mask to place movable elements into (don't include movable area div ifself)
				$mask .= "\t" . substr($data, $start_pos, $area['open_pos'] + $area['open_len'] - $start_pos) . $line_ending . "\t\t" . '#AREA' . $area_index . '#' . $line_ending;
				$start_pos = $area['close_pos'] - $area['close_len'];
			}
			$mask = trim($mask . "\t" . substr($data, $area['close_pos'] - $area['close_len']));

			if (!$elements) {
				// no elements found
				return false;
			}

			foreach ($areas as $area_index => $area) {
				$area_content = '';
				$target_elements = $target_order[$area_index];
				foreach ($target_order[$area_index] as $old_location) {
					$area_content .= $elements[$old_location]['data'] . $line_ending . "\t\t";
				}

				$mask = str_replace('#AREA' . $area_index . '#', trim($area_content), $mask);
			}

			$fp = fopen($filename, 'w');
			fwrite($fp, $mask);
			fclose($fp);

			return true;
		}

		/**
		 * Extracts div pairs with given class from given text
		 *
		 * @param string $data
		 * @param string $class
		 * @param int $area
		 * @param int $offset
		 * @return Array
		 */
		function _getDivPairs(&$data, $class, $area = null, $offset = 0)
		{
			preg_match_all('/(<div[^>]*>)|(<\/div>)/s', $data, $divs, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);

			$deep_level = 0;

			$pairs = Array ();
			$skip_count = Array (); // by deep level!

			foreach ($divs as $div) {
				if (strpos($div[0][0], '/') === false) {
					// opening div
					$skip_count[$deep_level] = 0;

					if (strpos($div[0][0], $class) !== false) {
						// ours opening (this deep level) -> save
						$pair = Array ('open_pos' => $div[0][1], 'open_len' => strlen($div[0][0]));
					}
					else {
						// not ours opening -> skip next closing (this deep level)
						$skip_count[$deep_level]++;
					}

					$deep_level++;
				}
				else {
					// closing div
					$deep_level--;

					if ($skip_count[$deep_level] == 0) {
						// nothing to skip (this deep level) -> save
						$pair['close_len'] = strlen($div[0][0]);
						$pair['close_pos'] = $div[0][1] + $pair['close_len'];
						$pair['data'] = substr($data, $pair['open_pos'], $pair['close_pos'] - $pair['open_pos']);

						if (isset($area)) {
							$pair['open_pos'] += $offset;
							$pair['close_pos'] += $offset;
							// index indicates area
							$pairs['a' . $area . 'e' . count($pairs)] = $pair;
						}
						else {
							$pairs[] = $pair;
						}
					}
					else {
						// skip closing div as requested
						$skip_count[$deep_level]--;
					}
				}
			}

			return $pairs;
		}

		/**
		 * Returns information about parser element locations in template
		 *
		 * @param Array $params
		 * @return mixed
		 */
		function blockInfo($info_type)
		{
			switch ($info_type) {
				case 'block_name':
					return $this->_blockName;
					break;

				case 'function_name':
					return $this->_functionName;
					break;

				case 'start_pos':
				case 'end_pos':
				case 'template':
					if (!array_key_exists($info_type, $this->_blockLocation)) {
						// invalid block name
						return 'invalid block name';
					}

					return $this->_blockLocation[$info_type];
					break;

				case 'template_file':
					return $this->_getTemplateFile(true);
					break;

				case 'content':
					$template_body = file_get_contents( $this->_getTemplateFile() );
					$length = $this->_blockLocation['end_pos'] - $this->_blockLocation['start_pos'];

					return substr($template_body, $this->_blockLocation['start_pos'], $length);
					break;
			}

			return 'undefined';
		}

		/**
		 * Main template being edited (parse copy, instead of original)
		 *
		 * @return bool
		 */
		function _isMainTemplate()
		{
			return $this->_blockLocation['template'] == $this->_sourceTemplate;
		}

		/**
		 * Returns filename, that contains template, where block is located
		 *
		 * @return string
		 */
		function _getTemplateFile($relative = false, $extension = '.tpl')
		{
			$filename = $this->Application->TemplatesCache->GetRealFilename( $this->_blockLocation['template'] ) . $extension;

			if ($relative) {
				$filename = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $filename, 1);
			}

			return $filename;
		}

		/**
		 * Saves new version of block to template, where it's located
		 *
		 * @param kDBItem $object
		 */
		function saveBlock(&$object)
		{
			$main_template = $this->_isMainTemplate();
			$filename = $this->_getTemplateFile(false);

			// 1. get new template content
			$new_template_body = $this->_getNewTemplateContent($object, $filename, $lines_before);
			if (is_bool($new_template_body) && ($new_template_body === true)) {
				// when nothing changed -> stop processing
				return true;
			}

			// 2. backup original template
			if (!$main_template && !copy($filename, $filename . '.bak')) {
				// backup failed
				$error_file = $this->_getTemplateFile(true, '.tpl.bak');
				$object->SetError('FileContents', 'template_backup_failed', '+Failed to create backup template "<strong>' . $error_file . '</strong>" backup.');
				return false;
			}

			// 3. save changed template
			$save_filename = $this->_getTemplateFile(false, $main_template ? '.tmp.tpl' : '.tpl');
			$fp = fopen($save_filename, 'w');
			if (!$fp) {
				// backup template create failed OR existing template save
				$error_file = $this->_getTemplateFile(true, $main_template ? '.tmp.tpl' : '.tpl');
				$object->SetError('FileContents', 'template_changes_save_failed', '+Failed to save template "<strong>' . $error_file . '</strong>" changes.');
				return false;
			}
			fwrite($fp, $new_template_body);
			fclose($fp);

			// 3. parse template to check for errors
			$this->_parseTemplate($object, $main_template ? '.tmp' : '');

			if ($this->_parseErrors) {
				$error_msg = Array ();
				foreach ($this->_parseErrors as $error_data) {
					if (preg_match('/line ([\d]+)/', $error_data['msg'], $regs)) {
						// another line number inside message -> patch it
						$error_data['msg'] = str_replace('line ' . $regs[1], 'line ' . ($regs[1] - $lines_before), $error_data['msg']);
					}

					$error_msg[] = $error_data['msg'] . ' at line ' . ($error_data['line'] - $lines_before);
				}

				$object->SetError('FileContents', 'template_syntax_error', '+Template syntax errors:<br/>' . implode('<br/>', $error_msg));
				return false;
			}

			if ($main_template) {
				// 4.1. replace original file with temporary
				if (!rename($this->_getTemplateFile(false, '.tmp.tpl'), $filename)) {
					// failed to save new content to original template
					$error_file = $this->_getTemplateFile(true);
					$object->SetError('FileContents', 'template_save_failed', '+Failed to save template "<strong>' . $error_file . '</strong>".');
					return false;
				}
			}
			else {
				// 4.2. delete backup
				unlink( $this->_getTemplateFile(false, '.tpl.bak') );
			}

			return true;
		}

		/**
		 * Returns new template content of "true", when nothing is changed
		 *
		 * @param kDBItem $object
		 * @param string $filename
		 * @param int $lines_before
		 * @return mixed
		 */
		function _getNewTemplateContent(&$object, $filename, &$lines_before)
		{
			$new_content = $object->GetDBField('FileContents');

			$template_body = file_get_contents($filename);
			$lines_before = substr_count(substr($template_body, 0,  $this->_blockLocation['start_pos']), "\n");

			$new_template_body = 	substr($template_body, 0,  $this->_blockLocation['start_pos']) .
									$new_content .
									substr($template_body, $this->_blockLocation['end_pos']);

			return crc32($template_body) == crc32($new_template_body) ? true : $new_template_body;
		}

		function _saveError($errno, $errstr, $errfile, $errline, $errcontext)
		{
			if ($errno != E_USER_ERROR) {
				// ignore all minor errors, except fatals from parser
				return true;
			}

			/*if (defined('E_STRICT') && ($errno == E_STRICT)) {
				// always ignore strict errors here (specially when not in debug mode)
				return true;
			}*/

			$this->_parseErrors[] = Array ('msg' => $errstr, 'file' => $errfile, 'line' => $errline);
			return true;
		}
	}