<?php
/**
 * @brief		Templates Model
 * @author		<a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) 2001 - 2016 Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/legal/standards/
 * @package		IPS Community Suite
 * @subpackage	Content
 * @since		25 Feb 2014
 * @version		SVN_VERSION_NUMBER
 */

namespace IPS\cms;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * @brief	Template Model
 */
class _Templates extends \IPS\Patterns\ActiveRecord
{
	/**
	 * @brief	[ActiveRecord] Multiton Store
	 */
	protected static $multitons;
	
	/**
	 * @brief	[ActiveRecord] Database Prefix
	 */
	public static $databasePrefix = 'template_';
	
	/**
	 * @brief	[ActiveRecord] ID Database Table
	 */
	public static $databaseTable = 'cms_templates';
	
	/**
	 * @brief	[ActiveRecord] ID Database Column
	 */
	public static $databaseColumnId = 'id';
	
	/**
	 * @brief	[ActiveRecord] Database ID Fields
	 */
	protected static $databaseIdFields = array( 'template_key', 'template_id' );
	
	/**
	 * @brief	[ActiveRecord] Multiton Map
	 */
	protected static $multitonMap	= array();
		
	/**
	 * @brief	Retusn all types
	 */
	const RETURN_ALL = 1;
	
	/**
	 * @brief	Returns block templates
	 */
	const RETURN_BLOCK = 2;
	
	/**
	 * @brief	Return page templates
	 */
	const RETURN_PAGE = 4;
	
	/**
	 * @brief	Return database templates
	 */
	const RETURN_DATABASE = 8;

	/**
	 * @brief	Return just css type
	 */
	const RETURN_ONLY_CSS = 16;

	/**
	 * @brief	Return just js type
	 */
	const RETURN_ONLY_JS = 32;

	/**
	 * @brief	Return just template type
	 */
	const RETURN_ONLY_TEMPLATE = 64;

	/**
	 * @brief	Return just contents of cms_templates ignoring IN_DEV and DESIGNERS' MODE
	 */
	const RETURN_DATABASE_ONLY = 128;

	/**
	 * @brief	Return both IN_DEV and database templates
	 */
	const RETURN_DATABASE_AND_IN_DEV = 256;
	
	/**
	 * @brief	Default database template group names
	 */
	public static $databaseDefaults = array(
		'featured'   => 'category_articles',
		'form'	     => 'form',
		'display'    => 'display',
		'listing'    => 'listing',
		'categories' => 'category_index'
	);
	
	/**
	 * Ensure that the template is calling the correct groups
	 *
	 * @param	string	$group	Group to load templates from
	 * @return  void
	 */
	public static function fixTemplateTags( $group )
	{
		$templates = iterator_to_array( \IPS\Db::i()->select( '*', 'cms_templates', array( array( 'template_group=?', $group ) ) )->setKeyField('template_title') );
		
		foreach( $templates as $template )
		{
			$save = array();
			
			/* Make sure template tags call the correct group */
			if ( mb_stristr( $template['template_content'], '{template' ) )
			{
				preg_match_all( '/\{([a-z]+?=([\'"]).+?\\2 ?+)}/', $template['template_content'], $matches, PREG_SET_ORDER );

				/* Work out the plugin and the values to pass */
				foreach( $matches as $index => $array )
				{
					preg_match_all( '/(.+?)=' . $array[ 2 ] . '(.+?)' . $array[ 2 ] . '\s?/', $array[ 1 ], $submatches );

					$plugin = array_shift( $submatches[ 1 ] );
					if ( $plugin == 'template' )
					{
						$value   = array_shift( $submatches[ 2 ] );
						$options = array();

						foreach ( $submatches[ 1 ] as $k => $v )
						{
							$options[ $v ] = $submatches[ 2 ][ $k ];
						}

						if ( isset( $options['app'] ) and $options['app'] == 'cms' and isset( $options['location'] ) and $options['location'] == 'database' and isset( $options['group'] ) and $options['group'] != $template['template_original_group'] )
						{
							if ( in_array( $value, array_keys( $templates ) ) )
							{
								$options['group'] = $group;

								$replace = '{template="' . $value . '" app="' . $options['app'] . '" location="' . $options['location'] . '" group="' . $options['group'] . '" params="' . ( isset($options['params']) ? $options['params'] : NULL ) . '"}';
								$save['template_content'] = str_replace( $matches[$index][0], $replace, $template['template_content'] );
							}
						}
						
						if ( count( $save ) )
						{
							\IPS\Db::i()->update( 'cms_templates', $save, array( 'template_id=?', $template['template_id'] ) );
						}
					}
				}
			}
		}
	}
	
	/**
	 * Load Record
	 * Overloaded so we can force loading by key by default but still retain the template_id field as the primary key so
	 * save still updates the primary ID.
	 *
	 * @see		\IPS\Db::build
	 * @param	int|string	$id					ID
	 * @param	string		$idField			The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
	 * @param	mixed		$extraWhereClause	Additional where clause(s) (see \IPS\Db::build for details)
	 * @return	static
	 * @throws	\InvalidArgumentException
	 * @throws	\OutOfRangeException
	 */
	public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
	{
		if ( ! is_numeric( $id ) and $idField === NULL )
		{
			$idField = 'template_key';
		}

		try
		{
			return parent::load( $id, $idField, $extraWhereClause );
		}
		catch( \OutOfRangeException $ex )
		{
			if ( ! is_numeric( $id ) and ( \IPS\IN_DEV or \IPS\Theme::designersModeEnabled() ) )
			{
				$templates = \IPS\cms\Theme::i()->getRawTemplates( NULL, NULL, NULL, \IPS\cms\Theme::RETURN_ONLY_TEMPLATE | \IPS\cms\Theme::RETURN_AS_OBJECT );

				if ( isset( $templates[ $id ] ) )
				{
					return $templates[ $id ];
				}
			}

			throw $ex;
		}
	}
	
	/**
	 * Make a group_name readable (Group Name)
	 *
	 * @param	string	$name		Group name from the database
	 * @return	string
	 */
	public static function readableGroupName( $name )
	{
		if ( $name === 'js' )
		{
			return 'JS';
		}
		else if ( $name === 'css' )
		{
			return 'CSS';
		}

		return ucwords( str_replace( array( '-', '_' ), ' ', $name ) );
	}
	
	/**
	 * Get all template group
	 *
	 * @param	int|constant	$returnType		Determines the content returned
	 * @return	array
	 */
	public static function getGroups( $returnType=1 )
	{
		$where  = array();
		$return = array();
		$locations = NULL;
		
		if ( is_string( $returnType ) )
		{
			switch( $returnType )
			{
				case 'all':
					$returnType = self::RETURN_ALL;
				break;
				case 'block':
					$returnType = self::RETURN_BLOCK;
				break;
				case 'page':
					$returnType = self::RETURN_PAGE;
				break;
				case 'database':
					$returnType = self::RETURN_DATABASE;
				break;
			}
		}
		
		if ( $returnType & self::RETURN_ALL )
		{
			$where[] = array( 'template_location !=?', NULL );
		}
		else
		{
			$locations = array();
			
			if ( $returnType & self::RETURN_BLOCK )
			{
				$locations[] = 'block';
			}
			
			if ( $returnType & self::RETURN_PAGE )
			{
				$locations[] = 'page';
			}
			
			if ( $returnType & self::RETURN_DATABASE )
			{
				$locations[] = 'database';
			}
			
			if ( ! count( $locations ) )
			{
				throw new \UnexpectedValueException();
			}
			
			$where[] = array( "template_location IN ('" . implode( "','", $locations ) . "')" );
		}
		
		foreach( \IPS\Db::i()->select( 'template_group', static::$databaseTable, $where, 'template_group ASC', NULL, 'template_group' ) as $template )
		{
			$return[ $template ] = $template;
		}
		
		return $return;
	}
	
	/**
	 * Get all templates
	 *
	 * @param	int|constant	$returnType		Determines the content returned
	 * @return	array
	 */
	public static function getTemplates( $returnType=1 )
	{
		$where  = array();
		$return = array();
		$locations = NULL;

		if ( ( \IPS\IN_DEV or \IPS\Theme::designersModeEnabled() ) AND ( $returnType & self::RETURN_DATABASE_AND_IN_DEV ) AND ! ( $returnType & self::RETURN_DATABASE_ONLY ) )
		{
			$flags = \IPS\cms\Theme::RETURN_AS_OBJECT;

			if ( $returnType & self::RETURN_ONLY_TEMPLATE )
			{
				$flags += \IPS\cms\Theme::RETURN_ONLY_TEMPLATE;
			}
			else if ( $returnType & self::RETURN_ONLY_CSS )
			{
				$flags += \IPS\cms\Theme::RETURN_ONLY_CSS;
			}
			else if ( $returnType & self::RETURN_ONLY_JS )
			{
				$flags += \IPS\cms\Theme::RETURN_ONLY_JS;
			}
			else
			{
				if ( $returnType & self::RETURN_BLOCK )
				{
					$flags += \IPS\cms\Theme::RETURN_BLOCK;
				}

				if ( $returnType & self::RETURN_PAGE )
				{
					$flags += \IPS\cms\Theme::RETURN_PAGE;
				}

				if ( $returnType & self::RETURN_DATABASE )
				{
					$flags += \IPS\cms\Theme::RETURN_DATABASE;
				}
			}

			$return = \IPS\cms\Theme::i()->getRawTemplates( 'cms', NULL, NULL, $flags );
		}

		if ( ! ( \IPS\IN_DEV or \IPS\Theme::designersModeEnabled() ) OR ( $returnType & self::RETURN_DATABASE_AND_IN_DEV ) OR ( $returnType & self::RETURN_DATABASE_ONLY ) )
		{
			if ( $returnType & self::RETURN_ALL )
			{
				$where[] = array( 'template_location !=?', NULL );
			}
			else if ( $returnType & self::RETURN_ONLY_TEMPLATE )
			{
				$where[] = array( 'template_type = ?', 'template' );
			}
			else if ( $returnType & self::RETURN_ONLY_CSS )
			{
				$where[] = array( 'template_type = ?', 'css' );
			}
			else if ( $returnType & self::RETURN_ONLY_JS )
			{
				$where[] = array( 'template_type = ?', 'js' );
			}
			else
			{
				$locations = array();

				if ( $returnType & self::RETURN_BLOCK )
				{
					$locations[] = 'block';
				}

				if ( $returnType & self::RETURN_PAGE )
				{
					$locations[] = 'page';
				}

				if ( $returnType & self::RETURN_DATABASE )
				{
					$locations[] = 'database';
				}

				if ( !count( $locations ) )
				{
					throw new \UnexpectedValueException();
				}

				$where[] = array( "template_location IN ('" . implode( "','", $locations ) . "')" );
			}

			foreach ( \IPS\Db::i()->select( '*', static::$databaseTable, $where, 'template_user_edited DESC' ) as $template )
			{
				/* user_edited version is returned first, so only add to the array if the key isn't already in $return */
				if ( !isset( $return[ $template['template_key'] ] ) )
				{
					$return[ $template['template_key'] ] = static::constructFromData( $template );
				}
			}
		}

		return $return;
	}
	
	/**
	 * Construct Load Query
	 * Overloaded so we return the user_edited version where available
	 *
	 * @param	int|string	$id					ID
	 * @param	string		$idField			The database column that the $id parameter pertains to
	 * @param	mixed		$extraWhereClause	Additional where clause(s)
	 * @return	\IPS\Db\Select
	 */
	protected static function constructLoadQuery( $id, $idField, $extraWhereClause )
	{
		$where = array( array( $idField . '=?', $id ) );
		if( $extraWhereClause !== NULL )
		{
			if ( !is_array( $extraWhereClause ) or !is_array( $extraWhereClause[0] ) )
			{
				$extraWhereClause = array( $extraWhereClause );
			}
			$where = array_merge( $where, $extraWhereClause );
		}
	
		return static::db()->select( '*', static::$databaseTable, $where, 'template_user_edited DESC' );
	}

	/**
	 * Generate a tree of templates
	 *
	 * @param array $templates	Template data from the database
	 * @return array
	 */
	public static function buildTree( $templates )
	{
		$return = array();

		foreach( $templates as $id => $template )
		{
			$return[ $template->location ][ $template->group ][ $template->key ] = $template;
		}

		return $return;
	}
	
	/**
	 * Add a new template
	 * 
	 * @param	array	$template	Template Data
	 * @return	object	\IPS\cms\Templates
	 */
	public static function add( $template )
	{
		$newTemplate = new static;

		foreach( $template as $_k => $_v )
		{
			$newTemplate->$_k	= $_v;
		}

		$newTemplate->_new         = TRUE;
		$newTemplate->user_created = 1;
		$newTemplate->user_edited  = 1;
		$newTemplate->master       = 0;

		$newTemplate->save();

		/* Create a unique key */
		$newTemplate->key = $newTemplate->location . '_' . \IPS\Http\Url\Friendly::seoTitle( $newTemplate->title ) . '_' . $newTemplate->id;

		/* Make sure there's no double __ in there */
		foreach( array( 'group', 'title', 'key' ) as $field )
		{
			if ( mb_strstr( $newTemplate->$field, '__' ) )
			{
				$newTemplate->$field = str_replace( '__', '_', $newTemplate->$field );
			}
		}

		$newTemplate->save();
		
		return $newTemplate;
	}
	
	/**
	 * Removes all stored files so they can be rebuilt on the fly
	 *
	 * @return void
	 */
	public static function deleteCompiledFiles()
	{
		foreach( \IPS\Db::i()->select( '*', 'cms_templates', array( 'template_file_object IS NOT NULL' ) ) as $template )
		{
			try
			{
				\IPS\File::get( 'core_Theme', $template['template_file_object'] )->delete();
			}
			catch( \Exception $ex ) { }
		}
		
		\IPS\Db::i()->update( 'cms_templates', array( 'template_file_object' => NULL ) );
	}
	
	/**
	 * Is suitable to be used for a custom wrapper?
	 *
	 * @return boolean
	 */
	public function isSuitableForCustomWrapper()
	{
		if ( $this->location == 'page' and preg_match( '#<html([^>]+?)?>#', $this->content ) )
		{
			if ( preg_match( '#\$html(\s|=|,)#', $this->params ) and preg_match( '#\$title(\s|=|,|$)#', $this->params ) )
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Is suitable to be used for a builder column wrapper?
	 *
	 * @return boolean
	 */
	public function isSuitableForBuilderWrapper()
	{
		if ( $this->location == 'page' and mb_stristr( $this->content, '{template="widgetContainer"' ) )
		{
			return true;
		}

		return false;
	}

	/**
	 * Import templates from an XML file
	 *
	 * @param   string      $file       File to load from (data/ or tmp/)
	 * @param	int|null	$offset     Offset to begin import from
	 * @param	int|null	$limit	    Number of rows to import
	 * @param   boolean     $update   	If updating, files written as master templates
	 * @return	bool		Rows imported (true) or none imported (false)
	 */
	public static function importXml( $file, $offset=NULL, $limit=NULL, $update=TRUE )
	{
		$i		= 0;
		$worked	= false;
	
		if( file_exists( $file ) )
		{
			/* First, delete any existing skin data for this app. */
			if( $offset === NULL OR $offset === 0 )
			{
				if ( $update === TRUE )
				{
					\IPS\Db::i()->delete( 'cms_templates', array( 'template_master=1' ) );
					\IPS\cms\Theme::deleteCompiledTemplate( 'cms' );
				}
			}

			/* Open XML file */
			$xml = new \IPS\Xml\XMLReader;
			$xml->open( $file );
			$xml->read();

			while( $xml->read() )
			{
				if( $xml->nodeType != \XMLReader::ELEMENT )
				{
					continue;
				}

				$i++;

				if ( $offset !== null )
				{
					if ( $i - 1 < $offset )
					{
						$xml->next();
						continue;
					}
				}

				if( $xml->name == 'template' )
				{
					$save = array(
						'template_content'        => $xml->readString(),
						'template_master'         => 1,
					    'template_original_group' => $xml->getAttribute('template_group'),
					    'template_file_object'    => NULL
					);

					foreach( array('key', 'title', 'desc', 'location', 'group', 'params', 'app', 'type' ) as $field )
					{
						$save[ 'template_' . $field ] = $xml->getAttribute( 'template_' . $field );
					}

					\IPS\Db::i()->insert( 'cms_templates', $save );
					$worked	= true;
				}

				if( $limit !== null AND $i === ( $limit + $offset ) )
				{
					break;
				}
			}
		}

		return $worked;
	}

	/**
	 * Delete
	 * Overloaded to protect inheritence
	 * 
	 *  @return void
	 */
	public function delete()
	{
		\IPS\cms\Theme::deleteCompiledTemplate( 'cms', $this->location, $this->group );

		if ( $this->user_created )
		{
			\IPS\Db::i()->delete( 'cms_templates', array( 'template_key=? AND template_user_created=?', $this->key, 1 ) );

			if ( isset( static::$multitons[ $this->id ] ) )
			{
				unset( static::$multitons[ $this->id ] );
			}
		}
		else
		{
			if ( $this->user_edited )
			{
				\IPS\Db::i()->delete( 'cms_templates', array( 'template_key=? AND template_user_edited=?', $this->key, 1 ) );

				if ( isset( static::$multitons[ $this->id ] ) )
				{
					unset( static::$multitons[ $this->id ] );
				}
			}
			else
			{
				throw new \OutOfRangeException('CANNOT_DELETE');
			}
		}
	}

	/**
	 * Get the inherited string
	 *
	 * @return string
	 */
	public function get__inherited()
	{
		if ( $this->user_created )
		{
			return 'custom';
		}
		elseif ( $this->user_edited )
		{
			return 'changed';
		}

		return 'original';
	}

	/**
	 * Get the file object
	 *
	 * @return string
	 */
	public function get__file_object()
	{
		if ( ! $this->file_object )
		{
			$content = $this->content;
			
			/* Build on demand */
			if ( $this->type != 'js' and ( mb_stristr( $this->content, "{block=" ) or mb_stristr( $this->content, "{{if" ) ) )
			{
				$functionName = 'css_' . uniqid();
				\IPS\Theme::makeProcessFunction( str_replace( '\\', '\\\\', $content ), $functionName );
				$functionName = "IPS\Theme\\{$functionName}";
				$content = $functionName();
			}
			
			$this->file_object = (string) \IPS\File::create( 'cms_Pages', $this->title, $content, 'page_objects', TRUE );
			parent::save(); # Go to parent save to prevent $this->save() from wiping file objects
		}

		return $this->file_object;
	}

	/**
	 * Save
	 * 
	 * @return void
	 */
	public function save()
	{
		/* Trash file object if appropriate */
		if ( $this->file_object )
		{
			try
			{
				\IPS\File::get( 'cms_Pages', $this->file_object )->delete();
			}
			catch ( \Exception $e )
			{
				/* Just to be sure nothing is throw, we don't care too much if it's not deleted */
			}

			/* Trash all cached page file objects too */
			\IPS\cms\Pages\Page::deleteCachedIncludes( $this->file_object );

			$this->file_object = NULL;
		}

		/* Should we copy this to a new template and then save it? */
		if ( ! $this->user_edited )
		{
			/* Infinite loop is only cool as Apple's address */
			$clone = new \IPS\cms\Templates;
			
			$clone->_data   = $this->_data;
			$clone->changed = $this->changed;
			
			$clone->id = NULL;
			$clone->user_edited = 1;
			$clone->master = 0;
			$clone->_new = TRUE;
			$clone->save();

			$key = \strtolower( 'template_cms_' . \IPS\cms\Theme::makeBuiltTemplateLookupHash( 'cms', $clone->location, $clone->group ) . '_' . $clone->group );
		}
		else
		{
			$key = \strtolower( 'template_cms_' . \IPS\cms\Theme::makeBuiltTemplateLookupHash( 'cms', $this->location, $this->group ) . '_' . $this->group );

			parent::save();
		}

		/* Clear store */
		unset( \IPS\Data\Store::i()->$key );
	}
}