<?php
if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
   || #################################################################### ||
   || # vBulletin 5.0.0 Beta 10
   || # ---------------------------------------------------------------- # ||
   || # Copyright ?2000-2012 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_Style
 *
 * @package vBApi
 * @access public
 */
class vB_Library_Style extends vB_Library
{
	private $stylecache = array();
	private $stylesById = array();


	/**
	 * Contains styles that were forced in a channel
	 * @var array
	 */
	protected $forcedStyles = NULL;

	protected function __construct()
	{
	}

	/**
	 * This is intended to be used in tests, to be able to refresh the cache
	 */
	public function resetForcedStyles()
	{
		$this->forcedStyles = NULL;
	}

	/**
	 * Returns a valid style to be used from the candidates
	 *
	 * @param array $stylePreference - Style candidates ordered by preference
	 * @return int
	 */
	public function getValidStyleFromPreference($stylePreference)
	{
		$styleId = FALSE;

		if (is_array($stylePreference) AND !empty($stylePreference))
		{
			// fetch info and verify styles exist
			$styles = array();
			$result = vB::getDbAssertor()->assertQuery('style', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				vB_dB_Query::CONDITIONS_KEY => array(
					array('field' => 'styleid', 'value' => array_map('intval', $stylePreference))
				),
				vB_Db_Query::COLUMNS_KEY => array('styleid', 'userselect')
			));
			
			foreach ($result as $style)
			{
				$styles[$style['styleid']] = $style;
			}

			reset($stylePreference);
			$style = current($stylePreference);
			while ($style !== FALSE AND $styleId === FALSE)
			{
				if (isset($styles[$style]))
				{
					if ($styles[$style]['userselect'] OR vB::getUserContext()->isAdministrator())
					{
						$styleId = $styles[$style]['styleid'];
					}
					else
					{
						/* We cannot be certain that the user is actually looking at a specific channel,
						so if a user doesn't have permission for certain style and that style is among
						the forced ones, we still let the user get it. This will reduce the window for
						spoofing. */
						if ($this->forcedStyles === NULL)
						{
							$this->forcedStyles = array();

							$result = vB::getDbAssertor()->assertQuery('vBForum:fetchForcedStyles', array(
								'styles' => array_keys($styles)
							));
							foreach($result as $st)
							{
								$this->forcedStyles[] = $st['styleid'];
							}
						}

						if (in_array($styles[$style]['styleid'], $this->forcedStyles))
						{
							$styleId = $styles[$style]['styleid'];
						}
					}
				}

				// go to next style candidate
				$style = next($stylePreference);
			}
		}

		return ($styleId !== FALSE) ? $styleId : 0;
	}

	/**
	 * Loads style information (selected style and style vars)
	 *
	 * This is different from fetchStyleByID(). The style fetched
	 * by this method may not be the style specified in $styleid parameter.
	 *
	 * If the style with $styleid doesn't allow user to use (The user isn't admin either),
	 * default style specified in vBulletin Settings will be returned.
	 *
	 *
	 * @param int $styleid
	 *
	 * @return array Style information.
	 */
	public function fetchStyleRecord($styleid)
	{
		$isAdmin = vB::getUserContext()->hasAdminPermission('cancontrolpanel');
		$thisStyle = null;
		if (isset($this->stylesById[$styleid]))
		{
			$thisStyle = $this->stylesById[$styleid];
			if($isAdmin || $thisStyle['userselect'])
			{
				return $thisStyle;
			}
		}

		$options = vB::getDatastore()->getValue('options');
		$defaultStyleId =  $options['styleid'];

		$defaultStyle = null;
		if (isset($this->stylesById[$defaultStyleId]))
		{
			$defaultStyle = $this->stylesById[$defaultStyleId];

			//if we have a cached style and we got here its because we examined the requested style
			//and rejected it.
			if ($thisStyle)
			{
				return $defaultStyle;
		}
		}

		//determine which styles we need to look up.
		$styleids = array();
		if (!$thisStyle)
		{
			$styleids[] = $styleid;
		}	

		if ($defaultStyle)
		{
			$styleids[] = $defaultStyleId;
		}

		$conditions = array();
		$conditions['styleid'] = $styleids;

		if (!$isAdmin)
		{
			$conditions['userselect'] = 1;
		}

		//reset and reload
		$thisStyle = null;
		$defaultStyle = null;
		$result =  vB::getDbAssertor()->select('style', $conditions);
		foreach($result as $style)
		{
			$this->stylesById[$style['styleid']] = $style;
			if ($style['styleid'] == $styleid)
			{
				$thisStyle = $style;
	}
			else if($style['styleid'] == $defaultStyleId)
			{
				$defaultStyle = $style;
			}
		}

		if ($thisStyle)
		{
			if($isAdmin || $thisStyle['userselect'])
			{
				return $thisStyle;
			}
		}
		
		if ($defaultStyle)
		{
				return $defaultStyle;
		}

		//if we don't have anything.
		return false;	
	}

	/**
	 * Import style from XML Data
	 *
	 * @param string $xmldata XML Data to be imported as style.
	 * @param string $title Style title.
	 * @param integer $parentid Parent style ID.
	 * @param integer $overwritestyleid Style ID to be overwritten.
	 * @param boolean $anyversion Whether to ignore style version.
	 * @param integer $displayorder Style display order.
	 * @param boolean $userselect Whether the style allows user selection.
	 */
	public function importStyleFromXML($xmldata, $title, $parentid, $overwritestyleid, $anyversion, $displayorder, $userselect, $scilent = false)
	{
		require_once(DIR . '/includes/adminfunctions_template.php');
		$imported = xml_import_style($xmldata,
			$overwritestyleid, $parentid, $title,
			$anyversion, $displayorder, $userselect,
			null, null, $scilent
		);

		return $imported;
	}

	/**
	 * Import style from Server File
	 *
	 * @param string $serverfile Server file name to be imported.
	 * @param string $title Style title.
	 * @param integer $parentid Parent style ID.
	 * @param integer $overwritestyleid Style ID to be overwritten.
	 * @param boolean $anyversion Whether to ignore style version.
	 * @param integer $displayorder Style display order.
	 * @param boolean $userselect Whether the style allows user selection.
	 */
	public function importStyleFromServer($serverfile, $title, $parentid, $overwritestyleid, $anyversion, $displayorder, $userselect)
	{
		require_once(DIR . '/includes/adminfunctions.php');

		if (file_exists($serverfile))
		{
			$xml = file_read($serverfile);
		}
		else
		{
			throw new vB_Exception_Api('no_file_uploaded_and_no_local_file_found_gerror');
		}

		return $this->importStyleFromXML($xml, $title, $parentid, $overwritestyleid, $anyversion, $displayorder, $userselect);
	}

	/**
	 * Returns an array of all styles that are parents to the style specified
	 *
	 * @param	integer	Style ID
	 *
	 * @return	array
	 */
	public function fetchTemplateParentlist($styleid)
	{
		static $ts_arraycache = array();
		if (empty($styleid))
		{
			return '';
		}

		if (isset($ts_arraycache["$styleid"]))
		{
			return $ts_arraycache["$styleid"];
		}

		$ts_info = vB::getDbAssertor()->getRow('style', array('styleid' => $styleid));
		$ts_array = $styleid;

		if ($ts_info['parentid'] >= 0)
		{
			$parentlist = $this->fetchTemplateParentlist($ts_info['parentid']);
			if (!empty($parentlist))
			{
				$ts_array .= ',' . $parentlist;
			}
		}

		if (substr($ts_array, -2) != '-1')
		{
			$ts_array .= ',-1';
		}
		return $ts_arraycache["$styleid"] = $ts_array;
	}

	/**
	 * Fetch style information by its ID.
	 *
	 * @param integer $styleid Style ID.
	 *
	 * @return array style information
	 */
	public function fetchStyleByID($styleid)
	{
		if (!isset($this->stylesById[$styleid]))
		{
			$style = vB::getDbAssertor()->getRow('style', array('styleid' => $styleid));
			if (!$style)
		{
			throw new vB_Exception_Api('invalidid', array('styleid'));
		}
			$this->stylesById[$styleid] = $style;
	}
		return $this->stylesById[$styleid];
	}

	/**
	 * Builds the $stylecache array
	 *
	 * This is a recursive function - call it with no arguments
	 *
	 * @param boolean $styleid Style ID to start with
	 * @param integer $depth Current depth
	 * @param array $styles Array of styles to load (overrides $styleid ?)
	 * @return none
	 */
	private function cacheStyles($styleid = -1, $depth = 0, $styles = false)
	{
		static $cache = array();
		static $loaded = array();

		// forcing the reload of styles
		if (!empty($styles))
		{
			$cache = array();
		}

		// check to see if we have already got the results from the database
		if (empty($cache))
		{
			$counter = 0;
			if (empty($loaded))
			{
				$styles = vB::getDbAssertor()->getRows('style', array(), 'displayorder');
				foreach ($styles as $style)
				{
					$loaded[$style['styleid']] = true;

					//if we don't have the parent list, rebuild it.
					if (trim($style['parentlist']) == '')
					{
						$parentlist = $this->fetchTemplateParentlist($style['styleid']);
						vB::getDbAssertor()->assertQuery('vBForum:updatestyleparent', array(
								'parentlist' => $parentlist,
								'styleid' => intval($style['styleid']),
						));
						$style['parentlist'] = $parentlist;
					}


					//if we don't have the template list, rebuild it.
					if (trim($style['templatelist']) == '')
					{
						$style['templatelist'] = $this->buildTemplateIdCache($style['styleid'], true, $style['parentlist']);
						vB::getDbAssertor()->assertQuery('vBForum:updatestyletemplatelist', array(
								'templatelist' => $style['templatelist'],
								'styleid' => intval($style['styleid']),
						));
					}

					$loaded[$style['styleid']] = true;
					$cache[$style['parentid']][$style['displayorder']][$style['styleid']] = $style;
					$counter ++;
				}
			}

			foreach ($cache as $parentid => &$styles)
			{
				ksort($styles);
			}

			if (!defined('STYLECOUNT'))
			{
				define('STYLECOUNT', $counter);
			}
		}

		// database has already been queried
		if (!empty($cache["$styleid"]) AND is_array($cache["$styleid"]))
		{
			foreach ($cache["$styleid"] AS $holder)
			{
				foreach ($holder AS $style)
				{
					$this->stylecache["$style[styleid]"] = $style;
					$this->stylecache["$style[styleid]"]['depth'] = $depth;
					$this->cacheStyles($style['styleid'], $depth + 1, false);
				}
			}
		}
	}

	/**
	 * Fetch All styles
	 *
	 * @param bool $nocache Refresh Styles from database
	 *
	 * @return array All styles' information
	 */
	public function fetchStyles($nocache = false)
	{
		if (empty($this->stylecache) OR $nocache)
		{
			$this->stylecache = array();
			$this->cacheStyles();
		}
		return $this->stylecache;
	}

	/**
	 * Clear style in class cache.
	 * Needed for unit testing
	 *
	 */
	public function clearStyleCache()
	{
		$this->stylecache = array();
		$this->stylesById = array();
	}

	/**
	 * Checks if a styleid is valid
	 *
	 * @param int $styleid
	 * @return bool
	 */
	public function validStyle($styleid)
	{
		$this->fetchStyles();

		return isset($this->stylecache[$styleid]);
	}


	/**
	 *	Switch the style for rendering
	 *	This really should be part of the bootstrap code except:
	 *	1) We don't actually load the bootstrap in the admincp
	 * 2) There is a lot to the style load that isn't easy to redo (header/footer templates for example)
	 *
	 * This handles the stylevars and template lists -- including reloading the template cache.
	 * This is enough to handle the css template rendering, but probably won't work for anything
	 * more complicated.
	 */
	function switchCssStyle($styleid, $templates)
	{
		$style = $this->fetchStyleByID($styleid);

		if (empty($style))
		{
			return false;
		}

		$this->cacheStyles();

		fetch_stylevars($style, vB::getCurrentSession()->fetch_userinfo());

		//clear the template cache, otherwise we might get old templates
		vB_Api::instanceInternal('template')->cacheTemplates($templates, $style['templatelist'], false, true);
	}

	/**
	* Fetches a list of template IDs for the specified style
	* @deprecated
	* @param	integer	Style ID
	* @param	boolean	If true, returns a list of template ids; if false, goes ahead and runs the update query
	* @param	mixed	A comma-separated list of style parent ids (if false, will query to fetch the list)
	*
	* @return	mixed	Either the list of template ids, or nothing
	*/
	public function buildTemplateIdCache($styleid, $doreturn = false, $parentids = false)
	{
		if ($styleid == -1)
		{
			// doesn't have a cache
			return '';
		}

		//this is done as an array for historical reasons
		if ($parentids == 0)
		{
			$style['parentlist'] = $this->fetchTemplateParentlist($styleid);
		}
		else
		{
			$style['parentlist'] = $parentids;
		}

		$parents = explode(',', $style['parentlist']);
		$totalparents = sizeof($parents);

		$bbcodestyles = array();
		$templatelist = array();
		$assertor = vB::getDbAssertor();
		$templates = $assertor->assertQuery('vBForum:fetchTemplateIdsByParentlist', array(
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD,
				'parentlist' => $style['parentlist'],
		));
		foreach ($templates as $template)
		{
			for ($tid = $totalparents; $tid > 0; $tid--)
			{
				if ($template["templateid_$tid"])
				{
					$templatelist["$template[title]"] = $template["templateid_$tid"];
					if (preg_match('#^bbcode_[code|html|php|quote]+$#si', trim($template['title'])))
					{
						$bbcodetemplate = $template['title'] . '_styleid';
						if ($template["styleid_$tid"])
						{
							$templatelist["$bbcodetemplate"] = $template["styleid_$tid"];
						}
						else
						{
							$templatelist["$bbcodetemplate"] = -1;
						}
					}
					break;
				}
			}
		}

		$customdone = array();
		$customtemps = $assertor->assertQuery('vBForum:fetchCustomtempsByParentlist', array(
				'parentlist' => $style['parentlist'],
		));

		foreach ($customtemps as $template)
		{
			if ($customdone["$template[title]"])
			{
				continue;
			}
			$customdone["$template[title]"] = 1;
			$templatelist["$template[title]"] = $template['templateid'];

			if (preg_match('#^bbcode_[code|html|php|quote]+$#si', trim($template['title'])))
			{
				$bbcodetemplate = $template['title'] . '_styleid';
				$templatelist["$bbcodetemplate"] = $template['styleid'];
			}
		}

		$templatelist = serialize($templatelist);

		if (!$doreturn)
		{
			$assertor->update(
					'template',
					array('templatelist' => $templatelist),
					array('styleid' => $styleid)
			);
		}
		else
		{
			return $templatelist;
		}
	}

	/**
	* Resets the css cachebuster date.
	*/
	public function setCssDate()
	{
		$options = vB::get_datastore()->get_value('miscoptions');
		$options['cssdate'] = vB::getRequest()->getTimeNow();
		vB::getDatastore()->build('miscoptions', serialize($options), 1);
	}

	/**
	* Rebuild the style datastore.
	*/
	function buildStyleDatastore()
	{
		$this->setCssDate();
		$stylecache = $this->fetchStyles(true);
		vB::getDatastore()->build('stylecache', serialize($stylecache), 1);
		return $stylecache;
	}
}
