Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Content * @since 25 Feb 2014 */ 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'; } 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_AS_OBJECT ); if ( isset( $templates[ $id ] ) ) { return $templates[ $id ]; } } try { return parent::load( $id, $idField, $extraWhereClause ); } catch( \OutOfRangeException $ex ) { 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( '#]+?)?>#', $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 ); } }