<?php
if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
   || #################################################################### ||
   || # vBulletin 5.1.1 - Licence Number VBCUP51ZOY
   || # ---------------------------------------------------------------- # ||
   || # Copyright ©2000-2014 vBulletin Solutions Inc. All Rights Reserved. ||
   || # This file may not be redistributed in whole or significant part. # ||
   || # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
   || # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
   || #################################################################### ||
   \*======================================================================*/


/**
 * vB_Library_Template
 *
 * @package vBApi
 * @access public
 */
class vB_Library_Template extends vB_Library
{
	private static $templatecache = array();
	private static $bbcode_style = array('code' => -1, 'html' => -1, 'php' => -1, 'quote' => -1);

	/**
	 * Fetch one template based on its name and style ID.
	 *
	 * @param string $template_name Template name.
	 * @param integer $styleid Style ID. If empty, this method will fetch template from default style.
	 * @return mixed
	 */
	public function fetch($template_name, $styleid = -1, $nopermissioncheck = false)
	{
		if (!empty(self::$templatecache[$template_name]))
		{
			return self::$templatecache[$template_name];
		}

		$fastDs = vB_FastDS::instance();

		if ($fastDs)
		{
			$cached = $fastDs->getTemplates($template_name, $styleid);

			if ($cached)
			{
				self::$templatecache[$template_name] = $cached;
				return $cached;
			}
		}
		$templates = $this->fetchBulk(array($template_name), $styleid, 'compiled', $nopermissioncheck);

		if ($templates[$template_name])
		{
			return $templates[$template_name];
		}

		return false;
	}

	/**
	 * Fetches a bulk of templates from the database
	 *
	 * @param array $template_names List of template names to be fetched.
	 * @param integer $styleid Style ID. If empty, this method will fetch template from default style.
	 *
	 * @return array Array of information about the imported style
	 */
	public function fetchBulk($template_names, $styleid = -1, $type = 'compiled', $nopermissioncheck = false)
	{
		if ($styleid == -1)
		{
			$vboptions = vB::getDatastore()->get_value('options');
			$styleid = $vboptions['styleid'];
		}
		$style = false;

		$response = array();

		if ($type == 'compiled')
		{
			$fastDs = vB_FastDS::instance();

			if ($fastDs)
			{
				$cached = vB_FastDS::instance()->getTemplates($template_names, $styleid);
			}
		}

		foreach ($template_names AS $template)
		{
			// see if we have it in cache already
			if ($type == 'compiled' AND !empty(self::$templatecache[$template]))
			{
				$response[$template] = self::$templatecache[$template];
				continue;
			}
			else if ($type == 'compiled' AND !empty($cached[$template]))
			{
				self::$templatecache[$template] = $cached[$template];
				$response[$template] = $cached[$template];
				continue;
			}
			// load the cache only(once) when we need it
			if (empty($style))
			{
				$style = vB_Library::instance('Style')->fetchStyleRecord($styleid, $nopermissioncheck);
				$templateassoc = $style['templatelist'];
			}
			//handle bad template names -- they should be blank by default.
			if (isset($templateassoc["$template"]))
			{
				$templateids[] = intval($templateassoc["$template"]);
			}
			else
			{
				// @todo: throw an exception if the template doesn't exist and we are in debug mode?
				$response[$template] = '';
			}
		}

		if (!empty($templateids))
		{
			$result = vB::getDbAssertor()->assertQuery('template', array('templateid' => $templateids));

			//vB::getDbAssertor()->assertQuery('template_fetchbyids', array('templateids' => $templateids));

			if ($result->valid())
			{
				$template = $result->current();

				while ($result->valid())
				{
					if ($type == 'compiled')
					{
						$response[$template['title']] = $template['template'];
					}
					else
					{
						$response[$template['title']] = $template['template_un'];
					}
					self::$templatecache[$template['title']] = $template['template'];
					$template = $result->next();
				}
			}
		}

		return $response;
	}

	/**
	 * Fetches a number of templates from the database and puts them into the templatecache
	 *
	 * @param	array	List of template names to be fetched
	 * @param	string	Serialized array of template name => template id pairs
	 * @param	bool	Whether to skip adding the bbcode style refs
	 * @param	bool	Whether to force setting the template
	 */
	public function cacheTemplates($templates, $templateidlist, $skip_bbcode_style = false, $force_set = false)
	{
		$vboptions = vB::getDatastore()->get_value('options');
		$templateassoc = unserialize($templateidlist);

		if ($vboptions['legacypostbit'] AND in_array('postbit', $templates))
		{
			$templateassoc['postbit'] = $templateassoc['postbit_legacy'];
		}

		foreach ($templates AS $template)
		{
			$templateids[] = intval($templateassoc["$template"]);
		}

		if (!empty($templateids))
		{
			// run query
			$temps = vB::getDbAssertor()->assertQuery("vBForum:fetchtemplates", array(
					vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED,
					'templateids' => $templateids,
			));

			// cache templates
			foreach ($temps as $temp)
			{
				if (empty(self::$templatecache["$temp[title]"]) OR $force_set)
				{
					self::$templatecache["$temp[title]"] = $temp['template'];
				}
			}
		}

		if (!$skip_bbcode_style)
		{
			self::$bbcode_style = array(
					'code'  => &$templateassoc['bbcode_code_styleid'],
					'html'  => &$templateassoc['bbcode_html_styleid'],
					'php'   => &$templateassoc['bbcode_php_styleid'],
					'quote' => &$templateassoc['bbcode_quote_styleid']
			);
		}
	}

	/**
	 *	Rewrites the file cache for the templates for all styles.
	 */
	public function saveAllTemplatesToFile()
	{
		$template_path = vB::getDatastore()->getOption('template_cache_path');

		$db = vB::getDBAssertor();
		$result = $db->select('template', array(), false, array('templateid', 'template'));

		foreach ($result AS $template)
		{
			$this->saveTemplateToFileSystem($template['templateid'], $template['template'], $template_path);
		}

		$fastDs = vB_FastDS::instance();

		//We want to force a fastDS rebuild, but we can't just call rebuild. There may be dual web servers,
		// and calling rebuild only rebuilds one of them.
		$options = vB::getDatastore()->getValue('miscoptions');
		$options['tmtdate'] = vB::getRequest()->getTimeNow();
		vB::getDatastore()->build('miscoptions', serialize($options), 1);
	}

	public function deleteAllTemplateFiles()
	{
		$template_path = vB::getDatastore()->getOption('template_cache_path');

		$db = vB::getDBAssertor();
		$result = $db->select('template', array(), false, array('templateid', 'template'));

		foreach ($result AS $template)
		{
			$this->deleteTemplateFromFileSystem($template['templateid'], $template_path);
		}
	}

	public function saveTemplateToFileSystem($templateid, $compiled_template, $template_path)
	{
		$template_name = "template$templateid.php";

		$real_path = realpath($template_path);
		if ($real_path === false)
		{
			$real_path = realpath(DIR . '/' . $template_path);
			if ($real_path === false)
			{
				throw new vB_Exception_Api('could_not_cache_template', array($templateid, $template_path, $template_name));
			}
		}

		$template_file = $real_path . "/$template_name";

		//determine if we can write to the provided location
		$can_write_template = false;

		//is writeable does not work properly on windows, see https://bugs.php.net/bug.php?id=54709
		//this is mostly used to avoid warnings when dealing with file_put_contents below so we'll skip the
		//checks for windows.
		if (!(strtoupper(substr(PHP_OS, 0, 3)) == 'WIN'))
		{
			//file is writable
			if (is_writable($template_file))
			{
				$can_write_template = true;
			}
			else
			{
				//file doesn't exist and directory is writeable
				if(!file_exists($template_file) AND is_writeable($real_path))
				{
					$can_write_template = true;
				}
			}
		}
		else
		{
			$can_write_template = true;
		}

		//if we can write, try to write
		$file = false;
		if ($can_write_template)
		{
			$file = fopen($template_file, 'w+');
			if ($file)
			{
				//hack to deal with the fact that the presentation layer has a separate runtime class.
				$compiled_template = str_replace('vB_Template_Runtime', 'vB5_Template_Runtime', $compiled_template);

				fwrite($file, "<?php \nif (!class_exists('vB5_Template', false)) throw new Exception('direct access error');\n");
				fwrite($file, $compiled_template);
				fclose($file);
			}
		}

		if (!$can_write_template OR !$file)
		{
			throw new vB_Exception_Api('could_not_cache_template', array($templateid, $template_path, $template_name));
		}
	}

	public function deleteTemplateFromFileSystem($templateid, $template_path)
	{
		$template_name = "template$templateid.php";

		$real_path = realpath($template_path);
		if ($real_path === false)
		{
			$real_path = realpath(DIR . '/' . $template_path);
			if ($real_path === false)
			{
				//fail quietly on delete, not much we can do about it.
				return;
			}
		}

		$template_file = $real_path . "/$template_name";

		//is_writable not reliable on windows.
		if (!(strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') AND !is_writable($template_file))
		{
			return;
		}

		if (file_exists($template_file))
		{
			unlink($template_file);
		}
	}

	/**
	 * Process the replacement variables.
	 *
	 * @param string The html to be processed
	 * @param integer The styleid to use.
	 *
	 * @return string The processed output
	 */
	public function processReplacementVars($html, $styleid = -1)
	{
		$style = vB_Library::instance('Style')->fetchStyleByID($styleid, false);

		if (!empty($style['replacements']))
		{
			if (!isset($replacementvars["$style[styleid]"]))
			{
				$replacementvars[$style['styleid']] = @unserialize($style['replacements']);
			}

			if (is_array($replacementvars[$style['styleid']]) AND !empty($replacementvars[$style['styleid']]))
			{
				$html = preg_replace(array_keys($replacementvars[$style['styleid']]), $replacementvars[$style['styleid']], $html);
			}
		}

		return $html;
	}


	/**
	 * Insert a new template
	 *
	 * @param integer $dostyleid Style ID which the new template belongs to.
	 * @param string $title Template name.
	 * @param string $content Template content.
	 * @param string $product The product ID which the template belongs to.
	 * @param boolean $savehistory Whether to save the change in template history.
	 * @param string $histcomment Comment of the change to be saved to template history.
	 *
	 * @return integer New inserted template ID.
	 */
	public function insert
	(
		$dostyleid,
		$title,
		$content,
		$product = 'vbulletin',
		$savehistory = false,
		$histcomment = '',
		$forcesaveonerror = false
	)
	{
		$dostyleid = intval($dostyleid);
		$title = trim($title);
		$content = trim($content);
		$product = trim($product);
		$histcomment = trim($histcomment);
		$userinfo = vB::getCurrentSession()->fetch_userinfo();
		$timenow = vB::getRequest()->getTimeNow();

		//bad things happen if we don't have a valid product at this point.
		//If its blank (which is itself an error) recover with a default that keeps thee
		//system functional
		if (!$product)
		{
			$product = 'vbulletin';
		}

		if (!$title)
		{
			throw new vB_Exception_Api('please_complete_required_fields');
		}

		require_once(DIR . '/includes/adminfunctions.php');
		require_once(DIR . '/includes/adminfunctions_template.php');

		// Compile template
		$template = $this->compile($content, $forcesaveonerror);
		// TODO: Product API
		$full_product_info = fetch_product_list(true);

		$result = vB::getDbAssertor()->assertQuery('template_get_existing', array('title' => $title));

		foreach ($result as $curtemplate)
		{
			$exists["$curtemplate[styleid]"] = $curtemplate;
		}

		// work out what we should be doing with the product field
		if ($exists['-1'] AND $dostyleid != -1)
		{
			// there is already a template with this name in the master set - don't allow a different product id
			$product = $exists['-1']['product'];
		}
		else if ($dostyleid != -1)
		{
			// we are not adding a new template to the master set - only allow the default product id
			$product = 'vbulletin';
		}
		else
		{
			// allow this - we are adding a totally new template to the master set
		}

		$stylelib = vB_Library::instance('Style');

		// check if template already exists
		if (!$exists[$dostyleid])
		{
			$templateid = $this->saveTemplate(
				$title,
				$template,
				$content,
				$timenow,
				$userinfo['username'],
				$full_product_info[$product]['version'],
				$product,
				null,
				null,
				$dostyleid,
				$savehistory,
				$histcomment
			);

			// now to update the template id list for this style and all its dependents...
			$stylelib->buildStyle($dostyleid, $title, array(
				'docss' => 0,
				'dostylevars' => 0,
				'doreplacements' => 0,
				'doposteditor' => 0
			), false);

			vB_Library::instance('Style')->buildStyleDatastore();
		}
		else
		{
			throw new vB_Exception_Api('template_x_exists_error', array($title));
		}

		if ($savehistory)
		{
			$result = vB::getDbAssertor()->assertQuery('template_savehistory', array(
				'dostyleid' 	=> $dostyleid,
				'title' 		=> $title,
				'template_un' 	=> $content,
				'dateline'		=> $timenow,
				'username'		=> $userinfo['username'],
				'version'		=> $full_product_info[$product]['version'],
				'comment'		=> $histcomment,
			));
		}

		$stylelib->buildStyleDatastore();
		return $templateid;
	}


	/**
	 * Compile a template.
	 *
	 * @param string $template_un The uncompiled content of a template.
	 */
	public function compile($template, $forcesaveonerror)
	{
		// @todo
		// Incorrect hack warning!!!
		// The legacy code in class_template_parser.php needs this to be set
		// but it apparrently does not actually need to be an instance of the
		// legacy db class for purposes of compiling a template.
		if (empty($GLOBALS['vbulletin']->db))
		{
			$GLOBALS['vbulletin']->db = false;
		}

		require_once(DIR . '/includes/class_template_parser.php');
		$parser = new vB_TemplateParser($template);

		try
		{
			$parser->validate($errors);
		}
		catch (vB_Exception_TemplateFatalError $e)
		{
			throw new vB_Exception_Api($e->getMessage());
		}

		$template = $parser->compile();

		// This is a comment from vB4 moved here.  Need to figure out what replace_template_variables
		// is supposed to do.
		// TODO: Reimplement these - if done, $session[], $bbuserinfo[], $vboptions
		// will parse in the template without using {vb:raw, which isn't what we
		// necessarily want to happen
		/*
		if (!function_exists('replace_template_variables'))
		{
			require_once(DIR . '/includes/functions_misc.php');
		}
		$template = replace_template_variables($template, false);
		*/

		if (function_exists('verify_demo_template'))
		{
			verify_demo_template($template);
		}

		// Legacy Hook 'template_compile' Removed //

		if (!$forcesaveonerror AND !empty($errors))
		{
			throw new vB_Exception_Api('template_compile_error', array($errors));
		}

		//extra set of error checking.  This can be skipped in many situations.
		if (!$forcesaveonerror)
		{
			$errors = check_template_errors($template);
			if (!empty($errors))
			{
				$vb5_config = &vB::getConfig();
				if (!is_array($errors) AND $vb5_config['Misc']['debug'])
				{
					// show compiled template code with line numbers to debug the problem
					$errors .= '<h4>Compiled Template Code:</h4><div style="height:200px; overflow:auto; border:1px solid silver; font-style:normal; font-family:Courier New;"><ol><li>' . implode('</li><li>', explode("\n", htmlspecialchars($template))) . '</li></ol></div>';
				}

				throw new vB_Exception_Api('template_eval_error', array($errors));
			}
		}

		return $template;
	}

	/**
	 * Update a template
	 *
	 * @param integer $templateid Template ID to be updated
	 * @param string $title Template name.
	 * @param string $content Template content.
	 * @param string $product The product ID which the template belongs to.
	 * @param string $oldcontent The content of the template at the time it was loaded.  This is used to prevent
	 *	cases where the template was changed while editing. Pass false to force an update.
	 * @param boolean $savehistory Whether to save the change in template history.
	 * @param string $histcomment Comment of the change to be saved to template history.
	 * @param boolean $forcesaveonerror save the template even though there are errors.
	 */
	public function update
	(
		$templateid,
		$title,
		$content,
		$product,
		$oldcontent,
		$savehistory,
		$histcomment,
		$forcesaveonerror = false
	)
	{
		$templateid = intval($templateid);
		$title = trim($title);
		$content = trim($content);
		$product = trim($product);
		$histcomment = trim($histcomment);
		$userinfo = vB::getCurrentSession()->fetch_userinfo();
		$timenow = vB::getRequest()->getTimeNow();

		require_once(DIR . '/includes/adminfunctions.php');
		require_once(DIR . '/includes/adminfunctions_template.php');

		// Compile template
		$template = $this->compile($content, $forcesaveonerror);

		// TODO: Product API
		$full_product_info = fetch_product_list(true);

		if (!$forcesaveonerror)
		{
			$errors = check_template_conflict_error($template);
			if (!empty($errors))
			{
				throw new vB_Exception_Api('template_conflict_errors', array($errors));
			}
		}

		$old_template = $this->fetchByID($templateid);

		// Test whether the template exists if new template title is not the same as old one's
		if (strtolower($title) != strtolower($old_template['title']))
		{
			$result = vB::getDbAssertor()->assertQuery('template_fetchbystyleandtitle', array(
				'styleid' => $old_template['styleid'],
				'title' => $title,
			));

			if ($result->valid())
			{
				throw new vB_Exception_Api('invalidid', array('templateid'));
			}
		}

		if ($oldcontent === false)
		{
			$hash = md5($old_template['template_un']);
		}
		else
		{
			$hash = md5($oldcontent);
		}

		$result = $this->saveTemplate(
			$title,
			$template,
			$content,
			$timenow,
			$userinfo['username'],
			$full_product_info[$product]['version'],
			$product,
			$templateid,
			$hash,
			$old_template['styleid'],
			$savehistory,
			$histcomment
		);

		if ($result == 0)
		{
			// we have an edit conflict
			throw new vB_Exception_Api('edit_conflict');
		}
		else
		{
			// Remove templatemerge record
			vB::getDbAssertor()->assertQuery('templatemerge',
				array(vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_DELETE,	'templateid' => $templateid));

			// update any customized templates to reflect a change of product id
			if ($old_template['styleid'] == -1 AND $product != $old_template['product'])
			{
				$result = vB::getDbAssertor()->assertQuery('template_updatecustom_product', array(
					'product'	=> $product,
					'title' 	=> $title,
				));
			}

			//we need to rebuild the style if a css template is changed, we may need to republish.
			if (preg_match('#\.css$#i', $title))
			{
				build_style($old_template['styleid'], $title, array(
					'docss' => 0,
					'dostylevars' => 0,
					'doreplacements' => 0,
					'doposteditor' => 0
				), '', '', false, false);
			}

			return true;
		}
	}

	/**
	 * Fetch template by its ID
	 *
	 * @param integer $templateid Template ID.
	 *
	 * @return array Return template array if $templateid is valid.
	 */
	public function fetchByID($templateid)
	{
		$templateid = intval($templateid);
		$result = vB::getDbAssertor()->assertQuery('template_fetchbyid', array('templateid' => $templateid));

		if ($result->valid())
		{
			return $result->current();
		}
		else
		{
			throw new vB_Exception_Api('invalidid', array('templateid'));
		}
	}


	/**
	 *	Save a template and handle all common operations between an insert and an update
	 *	caller is responsible for determining if a update or an insert is needed (via
	 *	providing the existing templateid for the record to be updated)
	 *
	 *	@param $title string.  The title of the template
	 *	@param $template string.  Compiled template text
	 *	@param $content string. Uncompiled template text
	 *	@param $timenow int.  Current time as a datestamp
	 *	@param $username string. Username of the user saving the template
	 *	@param $version string. The version of the product the template belongs to.
	 *	@param $product string. The product that the template belongs to.
	 *	@param $templateid int. The id of the template being saved, null if this is a new template
	 *	@param $hash string.  The md5 hash of the original text of the template being updated. This is used to
	 *		avoid conflicting edits.  Null if this is a new template.
	 *	@param $styleid int.  The ID of the style the template is being saved to.
	 *	@param $savehistory bool. Whether to save this edit to the template history -- valid for new templates
	 *	@param $hiscomment string.  A comment on the edit to save with the history
	 *
	 */
	protected function saveTemplate
	(
		$title,
		$template,
		$content,
		$timenow,
		$username,
		$version,
		$product,
		$templateid,
		$hash,
		$styleid,
		$savehistory,
		$histcomment
	)
	{
		$fields = array(
			'title' => $title,
			'template' => $template,
			'template_un' => $content,
			'dateline' => $timenow,
			'username' => $username,
			'version' => $version,
			'product' => $product,
		);

		//update
		if($templateid)
		{
			$fields['templateid'] = $templateid;
			$fields['hash'] = $hash;
			$queryid = 'template_update';
		}
		//insert
		else
		{
			$fields['dostyleid'] = $styleid;
			$queryid = 'template_insert';
		}

		// Do update
		$result = vB::getDbAssertor()->assertQuery($queryid, $fields);

		//a non positive result indicates failure
		if ($result)
		{
			// now update the file system if we setup to do so and we are in the master style
			if (defined('DEV_AUTOEXPORT') AND DEV_AUTOEXPORT AND $styleid == -1)
			{
				require_once(DIR . '/includes/functions_filesystemxml.php');
				autoexport_write_template($title, $content, $product, $version, $username, $timenow);
			}

			if ($savehistory)
			{
				vB::getDbAssertor()->assertQuery('template_savehistory', array(
					'dostyleid' => $styleid,
					'title' => $title,
					'template_un' => $content,
					'dateline' => $timenow,
					'username' => $username,
					'version' => $version,
					'comment' => $histcomment,
				));
			}

			//if this is a new template the return from the insert query is the templateid
			if (!$templateid)
			{
				$templateid = $result;
			}
			//if we are storing the templates on the file systems
			$options = vB::getDatastore()->getValue('options');

			if ($options['cache_templates_as_files'] AND $options['template_cache_path'])
			{
				$this->saveTemplateToFileSystem($templateid, $template, $options['template_cache_path']);
			}

			vB_Library::instance('Style')->setCssDate();

			// we need to reset the template fastDS cache.
			$options = vB::getDatastore()->getValue('miscoptions');
			$options['tmtdate']  = vB::getRequest()->getTimeNow();
			vB::getDatastore()->build('miscoptions', serialize($options), 1);
		}

		return $result;
	}

	/**
	 * Get template ID by its template name and style id
	 *
	 * @param $template_name the name of the template
	 * @param $styleid
	 */
	public function getTemplateID($template_name, $styleid = -1)
	{
		$result = $this->getTemplateIds(array($template_name), $styleid);
		return $result['ids'][$template_name];
	}

	/**
	 * Get a list of template IDs by thier template names and style id
	 *
	 * @param array $template_names -- a list of template names
	 * @param array $styleid -- must be a style the user has access to.  If not specified, the default style is used.
	 * @return array array('ids' => $ids) where $ids is a map of names to the template id for that name.  If the name is not
	 * 	found, the entry for that name in the map will be false.
	 */
	public function getTemplateIds($template_names, $styleid = -1)
	{
		$cleaner = vB::getCleaner();
		$template_names = $cleaner->clean($template_names, vB_Cleaner::TYPE_ARRAY);
		$styleid = $cleaner->clean($styleid, vB_Cleaner::TYPE_INT);
		$stylelib = vB_Library::instance('style');

		$style = $stylelib->fetchStyleRecord($styleid, true);
		$ids = array();
		foreach($template_names AS $name)
		{
			if (isset($style['templatelist'][$name]))
			{
				$ids[$name] =  $style['templatelist'][$name];
			}
			else
			{
				$ids[$name] = false;
			}
		}

		return array('ids' => $ids);
	}
}

