Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

-47
View File
@@ -1,47 +0,0 @@
<?php
/**
* @brief Magic Template Class for advanced theme (designers) mode
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 06 Aug 2013
*/
namespace IPS\Theme\Advanced;
/* 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;
}
/**
* Magic Template Class for advanced theme (designers) mode
*/
class _Template extends \IPS\Theme\Dev\Template
{
/**
* @brief Source Folder
*/
public $sourceFolder = NULL;
/**
* Contructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
{
parent::__construct( $app, $templateLocation, $templateName );
if ( \IPS\Theme\Advanced\Theme::$buildingFiles !== true )
{
$this->sourceFolder = \IPS\ROOT_PATH . "/themes/" . \IPS\Theme\Advanced\Theme::$currentThemeId . "/html/{$app}/{$templateLocation}/{$templateName}/";
}
}
}
-712
View File
@@ -1,712 +0,0 @@
<?php
/**
* @brief Advanced theming mode
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 06 Aug 2013
*/
namespace IPS\Theme\Advanced;
/* 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;
}
/**
* Advanced theming mode
*/
class _Theme extends \IPS\Theme\Dev\Theme
{
/**
* [Brief] Currently building /theme/ files boolean
*/
public static $buildingFiles = false;
/**
* [Brief] Currently selected theme ID
*/
public static $currentThemeId = NULL;
/**
* [Brief] Themes that need writing out as flat files.
*/
public static $toBuild = null;
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* Constructor
*
* @return void
*/
public function __construct()
{
if ( \IPS\Member::loggedIn()->skin and array_key_exists( \IPS\Member::loggedIn()->skin, \IPS\Theme::themes() ) )
{
$setId = \IPS\Member::loggedIn()->skin;
if ( \IPS\Theme::load( $setId )->canAccess() !== true )
{
$setId = \IPS\Theme::defaultTheme();
/* Restore default theme for member */
\IPS\Member::loggedIn()->$column = $setId;
\IPS\Member::loggedIn()->save();
}
}
else
{
$setId = \IPS\Theme::defaultTheme();
}
static::$currentThemeId = ( static::$currentThemeId ) ? static::$currentThemeId : $setId;
/* Check to make sure the files are there */
if ( static::$buildingFiles === FALSE and ( ! is_dir( static::_getHtmlPath('core') ) OR ! is_dir( static::_getCssPath('core') ) OR ! is_dir( static::_getResourcePath('core') ) ) )
{
/* Toggle the setting as something has gone wrong */
\IPS\Settings::i()->changeValues( array( 'theme_designers_mode' => 0 ) );
/* Redirect to an error (redirect so we get a proper theme object after switching off designers mode) */
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation == 'admin' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=customization&controller=themes&do=designersmode' ) );
}
else
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=designermode&do=missing&id=' . static::$currentThemeId, 'front' ) );
}
}
}
/**
* Load the custom language strings/values
*
* @param int $id Theme ID
* @return void
*/
public static function loadLanguage( $id )
{
if ( file_exists( \IPS\ROOT_PATH . "/themes/{$id}/lang.php" ) )
{
require \IPS\ROOT_PATH . "/themes/{$id}/lang.php";
foreach ( $lang as $k => $v )
{
\IPS\Member::loggedIn()->language()->words[ $k ] = $v;
}
}
}
/**
* (re)import HTML templates into the template DB
*
* @param string $app Application Key
* @param int $id Theme Set Id (0 if IN_DEV and not in advanced theming mode)
*
* @return void
*/
public static function importDevHtml( $app, $id )
{
static::$currentThemeId = $id;
return parent::importDevHtml( $app, $id );
}
/**
* (re)import CSS into the CSS DB
*
* @param string $app Application Key
* @param int $id Theme Set Id (0 if IN_DEV and not in advanced theming mode)
*
* @return void
*/
public static function importDevCss( $app, $id )
{
static::$currentThemeId = $id;
return parent::importDevCss( $app, $id );
}
/**
* Build Resources ready for non IN_DEV use
* Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
*
* @param string|array $app App (e.g. core, forum)
* @param int $id Theme Set Id (0 if IN_DEV and not in advanced theming mode)
*
* @return void
*/
public static function importDevResources( $app, $id )
{
static::$currentThemeId = $id;
return parent::importDevResources( $app, $id );
}
/**
* Get a template
*
* @param string $group Template Group
* @param string $app Application key (NULL for current application)
* @param string $location Template Location (NULL for current template location)
* @return \IPS\Output\Template
*/
public function getTemplate( $group, $app=NULL, $location=NULL )
{
if ( static::$buildingFiles === true )
{
return ( \IPS\IN_DEV ) ? \IPS\Theme\Dev\Theme::getTemplate( $group, $app, $location ) : \IPS\Theme::getTemplate( $group, $app, $location );
}
return parent::getTemplate( $group, $app, $location );
}
/**
* Returns a list of theme IDs that need building
*
* @return array
*/
public static function getToBuild()
{
if ( static::$toBuild === null )
{
foreach ( \IPS\Theme::themes() as $id => $set )
{
/* @todo consider using filestamps or compare template contents to determine if a theme ID needs updating */
static::$toBuild[] = $id;
}
}
return static::$toBuild;
}
/**
* Determines if we're currently in a multiple redirect during file building
*
* @return boolean
*/
public static function buildingInProgress()
{
if ( \IPS\Dispatcher::i()->controllerLocation == 'front' AND \IPS\Request::i()->mr )
{
$data = json_decode( urldecode( base64_decode( \IPS\Request::i()->mr ) ), true );
if ( isset( $data['buildingDesignersFiles'] ) )
{
return true;
}
}
return false;
}
/**
* Add a template
* As templates have inheritance, this will always go to theme set 0. A check is first made to
* ensure we're not overwriting an existing master template bit.
*
* @param array $data Data to insert (app, location, group, name, variables, content, [added_to])
* @throws \InvalidArgumentException
* @throws \OverflowException
* @return int Insert Id
*/
public static function addTemplate( $data )
{
$insertId = NULL;
try
{
$insertId = parent::addTemplate( $data );
}
catch ( \OverflowException $e )
{
/* Do nothing. It exists */
throw new \OverflowException;
}
catch ( \InvalidArgumentException $e )
{
throw new \InvalidArgumentException;
}
$currentThemeId = static::$currentThemeId;
foreach( \IPS\Theme::themes() as $theme )
{
if ( is_dir( \IPS\ROOT_PATH . "/themes/" . $theme->id ) )
{
static::$currentThemeId = $theme->id;
static::_writeThemeContainerDirectory( $data['app'], 'html' );
static::_writeThemePathDirectory( $data['app'], 'html', $data['app'] . '/' . $data['location'] );
static::_writeThemePathDirectory( $data['app'], 'html', $data['app'] . '/' . $data['location'] . '/' . $data['group'] );
$pathToWrite = static::_getHtmlPath( $data['app'], $data['location'], $data['group'] );
$write = '<ips:template parameters="' . $data['variables'] . '" />' . "\n";
$write .= $data['content'];
if ( ! @\file_put_contents( $pathToWrite . $data['name'] . '.phtml', $write ) )
{
throw new \RuntimeException('core_theme_dev_cannot_write_template,' . $pathToWrite . $data['name'] . '.phtml' );
}
else
{
@chmod( $pathToWrite . '/' . $data['name'] . '.phtml', 0777 );
}
}
}
static::$currentThemeId = $currentThemeId;
return $insertId;
}
/**
* Add CSS.
* As CSS files have inheritance, this will always go to theme set 0. A check is first made to
* ensure we're not overwriting an existing master CSS file.
*
* @param array $data Data to insert (app, location, path, name, content, [added_to], [plugin])
* @throws \InvalidArgumentException
* @throws \OverflowException
* @return int Insert Id
*/
public static function addCss( $data )
{
$insertId = NULL;
try
{
$insertId = parent::addCss( $data );
}
catch ( \OverflowException $e )
{
/* Do nothing. It exists */
#throw new \OverflowException;
}
catch ( \InvalidArgumentException $e )
{
throw new \InvalidArgumentException;
}
$currentThemeId = static::$currentThemeId;
foreach( \IPS\Theme::themes() as $theme )
{
if ( is_dir( \IPS\ROOT_PATH . "/themes/" . $theme->id ) )
{
static::$currentThemeId = $theme->id;
static::_writeThemeContainerDirectory( $data['app'], 'css' );
static::_writeThemePathDirectory( $data['app'], 'css', $data['app'] . '/' . $data['location'] );
static::_writeThemePathDirectory( $data['app'], 'css', $data['app'] . '/' . $data['location'] . '/' . $data['path'] );
$pathToWrite = static::_getCssPath( $data['app'], $data['location'], $data['path'] );
$write = ( empty( $data['content'] ) ) ? '/* No Content */' : $data['content'];
if ( ! @\file_put_contents( $pathToWrite . '/' . $data['name'], $write ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_css', '4S142/10', 403, '' );
}
else
{
@chmod( $pathToWrite . '/' . $data['name'], 0777 );
}
}
}
static::$currentThemeId = $currentThemeId;
return $insertId;
}
/**
* Writes the /theme/{id}/ directory
*
* @return string Path created
*/
protected static function _writeThemeIdDirectory()
{
$dirToWrite = \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId;
if ( ! is_dir( $dirToWrite ) )
{
if ( ! @mkdir( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_theme_id', '4S142/3', 403, '' );
}
else
{
@chmod( $dirToWrite, \IPS\IPS_FOLDER_PERMISSION );
}
}
/* Check its writeable */
if ( ! is_writeable( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_into_theme_id', '4S142/4', 403, '' );
}
/* Make sure previous versions of this file are removed */
if ( file_exists( $dirToWrite . '/lang.php' ) )
{
unlink( $dirToWrite . '/lang.php' );
}
$languageStrings = \IPS\Db::i()->select( 'word_key, word_default', 'core_sys_lang_words', array( 'word_theme=? and lang_id=?', static::$currentThemeId, \IPS\Lang::defaultLanguage() ) )->setKeyField('word_key')->setValueField('word_default');
if( \count( $languageStrings ) )
{
$langToWrite = "\$lang = array(\n";
foreach( $languageStrings as $key => $string )
{
$langToWrite .= "'{$key}'\t\t=> '" . str_replace( "'", "\\'", $string ) . "',\n";
}
$langToWrite .= ");";
}
else
{
$langToWrite = "\$lang = array(\n\t\n);";
}
@\file_put_contents( $dirToWrite . '/lang.php', '<?' . "php\n\n{$langToWrite}\n" );
@chmod( $dirToWrite . '/lang.php', \IPS\IPS_FILE_PERMISSION );
return $dirToWrite;
}
/**
* Writes the /application/{app}/dev/{container}/ directory
*
* @param string $app Application Directory
* @param string $container Container directory (e.g. html/css/resources)
* @return string Path created
* @throws \RuntimeException
*/
protected static function _writeThemeContainerDirectory( $app, $container )
{
$dirToWrite = \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId . '/' . $container;
if ( ! is_dir( $dirToWrite ) )
{
if ( ! @mkdir( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_theme_id', '4S142/5', 403, '' );
}
else
{
@chmod( $dirToWrite, \IPS\IPS_FOLDER_PERMISSION );
}
}
/* Check its writeable */
if ( ! is_writeable( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_into_theme_id', '4S142/6', 403, '' );
}
return $dirToWrite;
}
/**
* Writes the /application/{app}/dev/{container}/{path} directory
*
* @param string $app Application Directory
* @param string $container Path to create (e.g. admin, front)
* @param string $path Path
* @return string Path created
* @throws \RuntimeException
*/
protected static function _writeThemePathDirectory( $app, $container, $path )
{
$dirToWrite = \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId . '/' . $container . '/' . $path;
if ( ! is_dir( $dirToWrite ) )
{
if ( ! @mkdir( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_theme_id', '4S142/7', 403, '' );
}
else
{
@chmod( $dirToWrite, \IPS\IPS_FOLDER_PERMISSION );
}
}
/* Check its writeable */
if ( ! is_writeable( $dirToWrite ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_into_theme_id', '4S142/8', 403, '' );
}
return $dirToWrite;
}
/**
* Write skin resources
* Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
*
* @param string $app Application Directory
* @return void
* @throws \RuntimeException
*/
public static function exportResources( $app )
{
static::_writeThemeIdDirectory();
static::_writeThemeContainerDirectory( $app, 'resources');
foreach( \IPS\Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_location!=?', static::$currentThemeId, 'admin' ) )->setKeyField('resource_id') as $resourceId => $resource )
{
static::_writeThemePathDirectory( $app, 'resources', $resource['resource_app'] );
$pathToWrite = static::_writeThemePathDirectory( $app, 'resources', $resource['resource_app'] . '/' . $resource['resource_location'] );
if ( $resource['resource_path'] != '/' )
{
$_path = '';
foreach( explode( '/', trim( $resource['resource_path'], '/' ) ) as $dir )
{
$_path .= '/' . trim( $dir, '/' );
$pathToWrite = static::_writeThemePathDirectory( $app, 'resources', $resource['resource_app'] . '/' . $resource['resource_location'] . $_path );
}
}
if ( ! \file_put_contents( $pathToWrite . '/' . $resource['resource_name'], $resource['resource_data'] ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_img', '4S142/A', 403, '' );
}
else
{
@chmod( $pathToWrite . '/' . $resource['resource_name'], 0777 );
}
}
}
/**
* Write CSS into the appropriate theme directory as plain text CSS ({resource="foo.png"} intact)
*
* @param string $app Application Directory
* @return void
* @throws \RuntimeException
*/
public static function exportCss( $app )
{
static::_writeThemeIdDirectory();
static::_writeThemeContainerDirectory( $app, 'css');
$css = static::load( static::$currentThemeId )->getRawCss( array(), array( 'global', 'front' ) );
foreach( $css as $app => $data )
{
static::_writeThemePathDirectory( $app, 'css', $app );
foreach( $css[ $app ] as $location => $data )
{
$pathToWrite = static::_writeThemePathDirectory( $app, 'css', $app . '/' . $location );
foreach( $css[ $app ][ $location ] as $path => $data )
{
if ( $path != '.' )
{
$_path = $path;
if ( \strstr( $path, '/' ) )
{
$_path = '';
foreach( explode( '/', $path ) as $dir )
{
$_path .= '/' . trim( $dir, '/' );
$pathToWrite = static::_writeThemePathDirectory( $app, 'css', $app . '/' . $location . $_path );
}
}
else
{
$pathToWrite = static::_writeThemePathDirectory( $app, 'css', $app . '/' . $location . '/' . $path );
}
}
foreach( $css[ $app ][ $location ][ $path ] as $name => $data )
{
$params = array();
$write = '';
if ( $data['css_hidden'] )
{
$params[] = 'hidden="1"';
}
if ( \count( $params ) )
{
$write .= '/*<ips:css ' . implode( ' ', $params ) . ' />*/' . "\n";
}
$write .= ( empty( $data['css_content'] ) ) ? '/* No Content */' : $data['css_content'];
if ( ! @\file_put_contents( $pathToWrite . '/' . $data['css_name'], $write ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_css', '4S142/10', 403, '' );
}
else
{
@chmod( $pathToWrite . '/' . $data['css_name'], 0777 );
}
}
}
}
}
}
/**
* Write templates into the appropriate theme directory as plain text templates ({{logic}} intact)
*
* @param string $app Application Directory
* @return void
* @throws \RuntimeException
*/
public static function exportTemplates( $app )
{
static::_writeThemeIdDirectory();
static::_writeThemeContainerDirectory( $app, 'html');
$templates = static::load( static::$currentThemeId )->getRawTemplates( $app, array( 'global', 'front' ) );
foreach( $templates as $app => $data )
{
static::_writeThemePathDirectory( $app, 'html', $app );
foreach( $templates[ $app ] as $location => $data )
{
static::_writeThemePathDirectory( $app, 'html', $app . '/' . $location );
foreach( $templates[ $app ][ $location ] as $group => $data )
{
$pathToWrite = static::_writeThemePathDirectory( $app, 'html', $app . '/' . $location . '/' . $group );
/* On case sensitive file systems, ensure that a template with the same name but different case doesn't exist */
$fileMap = array();
foreach( new \DirectoryIterator( $pathToWrite ) as $file )
{
if ( $file->isDot() || mb_substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
{
continue;
}
$fileMap[ mb_strtolower( $file->getFilename() ) ][] = $file->getFilename();
}
foreach( $templates[ $app ][ $location ][ $group ] as $name => $data )
{
$write = '<ips:template parameters="' . $data['template_data'] . '" />' . "\n";
$write .= $data['template_content'];
if ( isset( $fileMap[ mb_strtolower( $data['template_name'] . '.phtml' ) ] ) )
{
foreach( $fileMap[ mb_strtolower( $data['template_name'] . '.phtml' ) ] as $file )
{
@unlink( $pathToWrite . '/' . $file );
}
}
if ( ! @\file_put_contents( $pathToWrite . '/' . $data['template_name'] . '.phtml', $write ) )
{
\IPS\Output::i()->error( 'theme_dm_err_cant_write_template', '4S142/9', 403, '' );
}
else
{
@chmod( $pathToWrite . '/' . $data['template_name'] . '.phtml', 0777 );
}
}
}
}
}
}
/**
* Returns the namespace for the template class
*
* @return string
*/
protected static function _getTemplateNamespace()
{
if ( static::$buildingFiles !== true )
{
return 'IPS\\Theme\\Advanced\\';
}
else
{
return parent::_getTemplateNamespace();
}
}
/**
* Returns the path for the IN_DEV .phtml files
*
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @return string
*/
protected static function _getHtmlPath( $app, $location=null, $path=null )
{
if ( static::$buildingFiles !== true )
{
return rtrim( \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId . "/html/{$app}/{$location}/{$path}", '/' ) . '/';
}
else
{
return parent::_getHtmlPath( $app, $location, $path );
}
}
/**
* Returns the path for the IN_DEV CSS file
*
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @return string
*/
protected static function _getCssPath( $app, $location=null, $path=null )
{
if ( static::$buildingFiles !== true )
{
return rtrim( \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId . "/css/{$app}/{$location}/{$path}", '/' ) . ( \stristr( $path, '.css' ) ? '' : '/' );
}
else
{
return parent::_getCssPath( $app, $location, $path );
}
}
/**
* Returns the path for the IN_DEV resource files
*
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @return string
*/
protected static function _getResourcePath( $app, $location=null, $path=null )
{
if ( static::$buildingFiles !== true )
{
return rtrim( \IPS\ROOT_PATH . "/themes/" . static::$currentThemeId . "/resources/{$app}/{$location}/{$path}", '/' ) . ( \stristr( $path, '.' ) ? '' : '/' );
}
else
{
return parent::_getResourcePath( $app, $location, $path );
}
}
}
View File
Whitespace-only changes.
-191
View File
@@ -1,191 +0,0 @@
<?php
/**
* @brief Magic Template Class for cachable templates
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 06 Aug 2013
*/
namespace IPS\Theme\Cache;
/* 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;
}
/**
* Magic Template Class for cachable templates
*/
class _Template extends \IPS\Theme\Template
{
/**
* @brief Source File
*/
protected static $file = NULL;
/**
* @brief Template object
*/
protected static $object = NULL;
/**
* @brief Theme object
*/
protected static $theme = NULL;
/**
* @brief Class name
*/
protected static $className = NULL;
/**
* Constructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
{
parent::__construct( $app, $templateLocation, $templateName );
static::$theme = ( \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'admin' ) ? \IPS\Theme::$adminSkin : \IPS\Theme::$memberTheme;
if( \IPS\IN_DEV )
{
static::$theme = \IPS\Theme::$inDevTheme;
}
if ( ! static::$theme->cache_key )
{
/* This theme has never received a cache key, so lets create one now */
static::$theme->resetCacheKey();
}
static::$className = 'class_' . $app . '_' . $templateLocation . '_' . $templateName;
$key = \strtolower( 'template_' . static::$theme->id . '_' . \IPS\Theme::makeBuiltTemplateLookupHash( $app, $templateLocation, $templateName ) . '_' . \IPS\Theme::cleanGroupName( $templateName ) );
static::$file = \IPS\Settings::i()->theme_disk_cache_path . "/" . $key . ".php";
}
/**
* Does this template bit exist
* This checks that it exists and also that the theme key matches
*
* @return boolean
*/
public function exists()
{
if ( file_exists( static::$file ) )
{
/* For FLOCK to work when setting, we need to flock when reading https://bugs.php.net/bug.php?id=52895 */
try
{
$f = fopen( static::$file, 'r' );
if( $f === FALSE )
{
throw new \RuntimeException();
}
flock( $f, LOCK_SH );
include_once( static::$file );
flock( $f, LOCK_UN );
fclose( $f );
}
catch( \ParseError | \RuntimeException )
{
@unlink( static::$file );
/* Clear zend opcache if enabled */
if ( \function_exists( 'opcache_invalidate' ) )
{
@opcache_invalidate( static::$file );
}
return false;
}
$class = "\\IPS\\Theme\\Cache\\" . \IPS\Theme::overloadHooks( static::$className, "IPS\\Theme\\Cache" );
if ( class_exists( $class ) )
{
static::$object = new $class( $this->app, $this->templateLocation, $this->templateName );
if ( static::$theme->cache_key != static::$object->cache_key )
{
@unlink( static::$file );
/* Clear zend opcache if enabled */
if ( \function_exists( 'opcache_invalidate' ) )
{
@opcache_invalidate( static::$file );
}
static::$object = NULL;
return false;
}
return true;
}
}
return false;
}
/**
* Return the template object
*
* @throws \UnderflowException if the template bit does not exist
* @return boolean
*/
public function get()
{
if ( ! static::$object and ! $this->exists() )
{
throw new \UnderflowException;
}
return static::$object;
}
/**
* Write the template object
* @param string $template PHP $template data to write
*
* @throws \RuntimeException If the template bit cannot be written
* @return void
*/
public function set( $template )
{
if ( ! is_writable( \IPS\Settings::i()->theme_disk_cache_path ) )
{
throw new \RuntimeException;
}
$template = "<?php\n" . $template;
$result = (bool) @\file_put_contents( static::$file, $template, LOCK_EX );
/* Sometimes LOCK_EX is unavailable and throws file_put_contents(): Exclusive locks are not supported for this stream.
While we would prefer an exclusive lock, it would be better to write the file if possible. */
if( ! $result )
{
@unlink( static::$file );
$result = (bool) @\file_put_contents( static::$file, $template );
}
@chmod( static::$file, \IPS\IPS_FILE_PERMISSION );
/* Clear zend opcache if enabled */
if ( \function_exists( 'opcache_invalidate' ) )
{
@opcache_invalidate( static::$file );
}
}
}
View File
Whitespace-only changes.
+13 -8
View File
@@ -11,20 +11,24 @@
namespace IPS\Theme\Compile;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* @deprecated
* Theme Hook skeleton class
*/
class _ThemeHook
class ThemeHook
{
/**
* @param $name
* @param $arguments
* @param string $name
* @param array $arguments
* @return string
*/
public function __call( string $name, array $arguments ): string
@@ -33,8 +37,8 @@ class _ThemeHook
}
/**
* @param $name
* @param $arguments
* @param string $name
* @param array $arguments
* @return string
*/
public static function __callStatic( string $name, array $arguments ): string
@@ -43,7 +47,8 @@ class _ThemeHook
}
/**
* @param $name
* @param string $name
* @param mixed $value
* @return void
*/
public function __set( string $name, mixed $value ): void
+911
View File
@@ -0,0 +1,911 @@
<?php
/**
* @brief Custom Template Class
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 2nd May 2023
*/
namespace IPS\Theme;
/* To prevent PHP errors (extending class does not exist) revealing path */
use ErrorException;
use InvalidArgumentException;
use IPS\Application;
use IPS\Data\Store;
use IPS\DateTime;
use IPS\Db;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Codemirror;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Log;
use IPS\Member;
use IPS\Node\Model;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Http\Url;
use IPS\Request;
use IPS\Theme;
use LogicException;
use OutOfRangeException;
use ParseError;
use UnderflowException;
use UnexpectedValueException;
use function array_merge;
use function count;
use function defined;
use function file_get_contents;
use function implode;
use function in_array;
use function preg_match;
use function str_replace;
use const IPS\DEBUG_CUSTOM_TEMPLATES;
use const IPS\IN_DEV;
use const IPS\DEBUG_TEMPLATES;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Custom Template Class
*/
class CustomTemplate extends Model
{
/**
* @brief Multiton Store
*/
protected static array $multitons = array();
/**
* @brief [ActiveRecord] Database Table
*/
public static ?string $databaseTable = 'core_theme_templates_custom';
/**
* @brief [ActiveRecord] ID Database Column
*/
public static string $databaseColumnId = 'id';
/**
* @brief [ActiveRecord] Database ID Fields
*/
protected static array $databaseIdFields = ['template_key'];
/**
* @brief Database Prefix
*/
public static string $databasePrefix = 'template_';
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static array $multitonMap = array();
/**
* @brief [Node] Node Title
*/
public static string $nodeTitle = 'custom_templates';
/**
* @brief [Node] Sortable
*/
public static bool $nodeSortable = TRUE;
/**
* @brief [Node] Positon Column
*/
public static ?string $databaseColumnOrder = 'order';
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected array $caches = array( 'custom_templates', 'custom_templates_rows' );
/**
* @brief Templates already loaded and evald
*/
public static array $calledTemplates = array();
/**
* @brief Current scope? (app/theme)
*/
public static string $scopeKey = '';
public static string $scopeValue = '';
/**
* Return the cleaned name, suitable for a PHP function
*
* @return string
*/
public function cleanName(): string
{
return Theme::cleanGroupName( $this->name );
}
/**
* Return all templates
*
* @return ActiveRecordIterator
*/
public static function all(): ActiveRecordIterator
{
return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, NULL, static::$databasePrefix . static::$databaseColumnOrder ), 'IPS\Theme\CustomTemplate' );
}
/**
* Get all custom templates that match a main template path (eg core/front/global/globalTemplate)
*
* @param $templatePath
* @return ActiveRecordIterator
*/
public static function byHookableTemplatePath( $templatePath ): ActiveRecordIterator
{
return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, [ Db::i()->like( 'template_hookpoint', $templatePath ) ] ), 'IPS\Theme\CustomTemplate' );
}
/**
* Get the template this custom template hooks into
*
* @param string|null $fullPath
* @return array|null
*/
public function hookedTemplate( ?string $fullPath=NULL ): ?array
{
$fullPath = $fullPath ?: $this->hookpoint;
[ $path, $hook ] = explode( ':', $fullPath );
$pathData = CustomTemplate::pathToArray( $path );
if ( IN_DEV )
{
$template = Theme::i()->getTemplate( $pathData['group'], $pathData['app'], $pathData['location'] );
$html = @file_get_contents( $template->sourceFolder . $pathData['templateName'] . '.phtml' );
if ( ! $html )
{
return null;
}
/* Parse the header tag */
preg_match( '/^<ips:template parameters="(.+?)?"(.+?)?\/>(\r\n?|\n)/', $html, $params );
/* Strip it */
$html = ( isset($params[0]) ) ? str_replace( $params[0], '', $html ) : $html;
return [
'template_content' => $html,
'template_app' => $pathData['app'],
'template_location'=> $pathData['location'],
'template_group' => $pathData['group'],
'template_name' => $pathData['templateName'],
'template_data' => ( isset( $params[1] ) ) ? $params[1] : '',
'template_set_id' => 0
];
}
else
{
try
{
return Db::i()->select( '*', 'core_theme_templates', array('template_set_id=? and template_app=? and template_location=? and template_group=? and template_name=?', 0, $pathData['app'], $pathData['location'], $pathData['group'], $pathData['templateName']) )->first();
}
catch ( UnderflowException $e )
{
return null;
}
}
}
/**
* We want a single template PHP function to be grouped by a common name
* The common name would be something like core_front_global_globalTemplate__hookpoint__hookPointType
*
* @return string
*/
public function groupedPhpFunctionName(): string
{
[ $path, $hook ] = explode( ':', $this->hookpoint );
return str_replace( '/', '_', $path ) . '__' . $hook . '__' . str_replace( '-', '', $this->hookpoint_type );
}
/**
* Convert the path and hookpoint to a PHP function name
* @param string $path
* @param string $hookpoint
* @param string $hooktype
* @return string
*/
public static function pathAndHookPointToFunctionName( string $path, string $hookpoint, string $hooktype ): string
{
return str_replace( '/', '_', $path ) . '__' . $hookpoint . '__' . str_replace( '-', '', $hooktype );
}
/**
* Return the available params for the template this hooks into
*
* @param string|null $params
* @param bool $returnVariablesOnly Only return the variable names, not the ='foo' defaults
* @return array|null
*/
public function availableParams( ?string $params=null, bool $returnVariablesOnly = false ): array|null
{
if ( $template = $this->hookedTemplate() and $template['template_content'] )
{
$params = ( $params ) ?: $template['template_data'];
if ( $returnVariablesOnly )
{
$matches = [];
/* We just want the $varname, not the ='foo' defaults */
preg_match_all( '/(\$[a-zA-Z0-9_]+)/', $params, $matches );
return array_unique( $matches[1] );
}
else
{
// Extract key-value pairs using regular expression
preg_match_all('/(\$[a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s,]*))?/', $params, $matches);
$results = [];
foreach( $matches[0] as $row ) {
$results[] = $row;
}
return $results;
}
}
return NULL;
}
/**
* Convert the stored path string to an array of app/location/group/name params
*
* @param string $path
* @return array
*/
public static function pathToArray( string $path ): array
{
[ $app, $location, $group, $templateName ] = explode( '/', $path );
return [
'app' => $app,
'location' => $location,
'group' => $group,
'templateName' => $templateName
];
}
/**
* Compile custom templates into a single data store object
*
* @return boolean|null
*/
public static function compileCustomTemplates(): ?bool
{
$flagKey = 'template_compiling_custom';
if ( Theme::checkLock( $flagKey ) )
{
return NULL;
}
Theme::lock( $flagKey );
$functions = [];
$organised = [];
foreach( static::all() as $template )
{
if ( ! $template->hookpoint )
{
if ( $template->key )
{
/* This isn't using a hookpoint, so save it as a function we can call directly */
$organised[ 'custom_nohook_' . $template->key ] = [ 'content' => $template->content, 'params' => '' ];
}
}
else
{
if ( ! isset( $organised[ $template->groupedPhpFunctionName() ] ) )
{
$paramList = '';
if ( $template->availableParams() )
{
$paramList = implode( ',', $template->availableParams() );
}
$organised[ $template->groupedPhpFunctionName() ] = [ 'params' => $paramList, 'content' => $template->contentForPhpFunction() ];
}
else
{
$organised[ $template->groupedPhpFunctionName() ]['content'] .= $template->contentForPhpFunction();
}
}
}
foreach( $organised as $functionName => $data )
{
$functions[] = Theme::_compileTemplate( $data['content'], $functionName, TRUE, FALSE, $data['params'] );
}
/* Put them in a class */
$template = <<<EOF
namespace _NAMESPACE_;
class CustomTemplateClass
{
EOF;
$template .= implode( "\n\n", $functions );
$template .= <<<EOF
}
EOF;
Store::i()->custom_templates = str_replace( 'namespace _NAMESPACE_', 'namespace IPS\Theme', $template );
Theme::unlock( $flagKey );
return TRUE;
}
public function contentForPhpFunction(): string
{
$header = "\n<!--CT: " . $this->name . "-->\n";
if ( $this->set_id )
{
/* Is this for a specific theme set? */
$content = $header . "{{if \IPS\Theme::i()->id == {$this->set_id}}}\n" . $this->content . "\n{{endif}}";
}
elseif ( $this->app )
{
/* Is this for a specific app? */
$content = $header . "{{if \IPS\Application::appIsEnabled( '{$this->app}' )}}\n" . $this->content . "\n{{endif}}";
}
else
{
/* This is a global template */
$content = $header . $this->content;
}
return $content;
}
/**
* @param string $name core/front/global/globalTemplate
* @param string $hookPoint body:inside-start
* @return string
* @throws ErrorException
*/
public static function getCustomTemplatesForHookPoint( string $name, string $hookPoint ): string
{
if ( $hookPoint )
{
[$hookName, $hookType] = explode( ':', $hookPoint );
}
else
{
$hookName = null;
$hookType = null;
}
$args = [];
/* The {customtemplate..} tag when automatically added via static::expandHookPoints() will pass in the arguments
to the function, so we need to grab them here. This allows those scoped variables to be used
in the custom templates we create */
if ( func_num_args() > 2 )
{
/* The forth argument is an array of the template_data params */
$args = func_get_arg(2);
}
if ( !in_array( 'custom_templates', array_keys( static::$calledTemplates ) ) )
{
/* If we don't have a compiled template, do that now */
if ( !isset( Store::i()->custom_templates ) )
{
/* It can take a few seconds for templates to finish compiling if initiated elsewhere, so let's try a few times sleeping 1 second between attempts
to give the compilation time to finish */
$attempts = 0;
while( $attempts < 6 )
{
$built = CustomTemplate::compileCustomTemplates();
if ( $built === NULL )
{
$attempts++;
sleep(1);
}
else
{
break;
}
}
/* Still no key? */
if ( ! isset( Store::i()->custom_templates ) )
{
Log::log( "Custom templates store missing", "template_store_missing" );
throw new ErrorException( 'template_store_missing' );
}
}
if ( ! class_exists( '\IPS\Theme\CustomTemplateClass', false ) )
{
$storedTemplate = Store::i()->custom_templates;
if ( DEBUG_TEMPLATES )
{
Theme::runDebugTemplate( 'custom_template', $storedTemplate );
}
else
{
try
{
if ( @eval( $storedTemplate ) === FALSE )
{
if ( IN_DEV )
{
throw new TemplateException( 'Invalid Custom Template' );
}
}
}
catch ( ParseError $e )
{
throw new UnexpectedValueException;
}
}
/* Init */
static::$calledTemplates['custom_template'] = new \IPS\Theme\CustomTemplateClass;
}
}
if ( $hookPoint )
{
$functionName = CustomTemplate::pathAndHookPointToFunctionName( $name, $hookName, $hookType );
}
else
{
$functionName = "custom_nohook_" . $name;
}
/* If we're debugging, unset the store on shutdown */
if ( DEBUG_CUSTOM_TEMPLATES )
{
register_shutdown_function( function() {
unset( Store::i()->custom_templates );
} );
}
if ( method_exists( static::$calledTemplates['custom_template'], $functionName ) )
{
return static::$calledTemplates['custom_template']->$functionName( ...$args );
}
else
{
return '';
}
}
/* Node class handler stuff */
/**
* Fetch All Root Nodes
*
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
* @param Member|NULL $member The member to check permissions for or NULL for the currently logged in member
* @param mixed|array $where Additional WHERE clause
* @param array|NULL $limit Limit/offset to use, or NULL for no limit (default)
* @return array
*/
public static function roots( ?string $permissionCheck ='view', $member=NULL, $where=array(), $limit=NULL ): array
{
if ( !count( $where ) )
{
$return = array();
foreach( static::getStore() AS $node )
{
if ( static::$scopeKey === 'set_id' )
{
if ( $node['template_set_id'] != static::$scopeValue )
{
continue;
}
}
$return[ $node['template_id'] ] = static::constructFromData( $node );
}
return $return;
}
else
{
return parent::roots( $permissionCheck, $member, $where, $limit );
}
}
/**
* Get data store
*
* @return array
* @note Note that all records are returned, even disabled promotion rules. Enable status needs to be checked in userland code when appropriate.
*/
public static function getStore(): array
{
if ( !isset( Store::i()->custom_templates_rows) )
{
$select = [
'template_id',
'template_name',
'template_hookpoint',
'template_updated',
'template_version',
'template_set_id',
'template_app',
'template_hookpoint_type',
'template_key',
'template_order'
];
Store::i()->custom_templates_rows = iterator_to_array( Db::i()->select( implode( ', ', $select ), static::$databaseTable, NULL, "template_order ASC" )->setKeyField( 'template_id' ) );
}
return Store::i()->custom_templates_rows;
}
/**
* [Node] Get Title
*
* @return string
*/
protected function get__title(): string
{
return $this->name;
}
/**
* [Node] Return the custom badge for each row
*
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
*/
protected function get__badge(): ?array
{
/* Is there an update to show? */
$badge = [];
if ( $this->hookpoint and $this->hookpoint_type )
{
$badge = array(
0 => 'style2',
1 => $this->hookpoint . ' ' . $this->hookpoint_type
);
}
else if ( $this->key )
{
$badge = array(
0 => 'style3',
1 => $this->key
);
}
return $badge;
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
*
* @param Url $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( Url $url, bool $subnode=FALSE ): array
{
$buttons = parent::getButtons( $url, $subnode );
if ( isset( $buttons['copy'] ) )
{
unset( $buttons['copy'] );
}
$buttons['edit']['link'] = Url::internal( "app=core&module=customization&controller=customtemplates&do=form&" . static::$scopeKey. "=" . static::$scopeValue . "&id=" . $this->id );
/* Remove the modal for editing */
unset( $buttons['edit']['data']['ipsDialog'], $buttons['edit']['data']['ipsDialog-title'] );
return $buttons;
}
/**
* [Node] Does the currently logged-in user have permission to edit permissions for this node?
* @note We currently do not use the permission system for templates.
*
* @return bool
*/
public function canManagePermissions(): bool
{
return FALSE;
}
/**
* [Node] Does the currently logged-in user have permission to copy this node?
*
* @return bool
*/
public function canCopy(): bool
{
return FALSE;
}
/**
* Form
*
* @param Form $form The form
* @return void
*/
public function form( Form &$form ): void
{
//$form->class = 'ipsForm--vertical ipsForm--custom-template ipsForm_fullWidth';
$form->attributes['data-controller'] = 'core.admin.customization.customTemplatesForm';
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_customization.js', 'core', 'admin' ) );
$applications = [];
$hookPointsSelect = [];
$pathNamesSelect = [];
foreach( Application::applications() as $application )
{
foreach ( Theme::i()->getHookPoints( [ $application->directory ] ) as $path => $hookPointNames )
{
$applications[ $application->directory ] = $application->_title;
foreach ( $hookPointNames as $hookPointName )
{
$fixedPath = preg_replace( '#^' . $application->directory . '/#', '', $path );
$pathNamesSelect[ $application->directory ][ str_replace( '/', '_', $fixedPath ) ] = $fixedPath;
$hookPointsSelect[ $application->directory . '_' . str_replace( '/', '_', $fixedPath ) ][ $hookPointName ] = $hookPointName;
}
}
}
/* Work out some defaults */
$defaultHookapp = 'core';
$defaultPath = '';
$defaultTemplateHook = '';
if ( $this->id and $this->hookpoint )
{
$array = static::pathToArray( $this->hookpoint );
if ( isset( $array['app'] ) )
{
$defaultHookapp = $array['app'];
}
if ( isset( $array['group'] ) and isset( $array['templateName'] ) )
{
[ $templateName, $defaultTemplateHook ] = explode( ':', $array['templateName'] );
$defaultPath = $array['location'] . '_' . $array['group'] . '_' . $templateName;
}
}
$form->add( new Text( 'template_name', $this->id ? $this->name : NULL, true ) );
$form->add( new Radio( 'template_type', ( $this->id and $this->hookpoint ) ? 'hook' : 'custom', true, array( 'options' => array( 'hook' => 'template_type_hook', 'custom' => 'template_type_custom' ) ), NULL, NULL, NULL, 'template_type' ) );
$form->add( new Select( 'template_hookapp', $defaultHookapp, FALSE, array( 'options' => $applications ), NULL, NULL, NULL, 'template_hookapp' ) );
foreach( $pathNamesSelect as $applicationDirectory => $path )
{
$form->add( new Select( 'template_path_' . $applicationDirectory, $defaultPath, FALSE, array( 'options' => $path, 'parse' => 'normal' ), NULL, NULL, Theme::i()->getTemplate( 'customization', 'core', 'admin' )->previewTemplateLink(), 'template_path_' . $applicationDirectory ) );
Member::loggedIn()->language()->words[ 'template_path_' . $applicationDirectory ] = Application::load( $applicationDirectory )->_title;
}
foreach( $hookPointsSelect as $data => $hookPoints )
{
[ $applicationDirectory, $path ] = explode( '_', $data, 2 );
$form->add( new Select( 'template_hookpoint_' . $applicationDirectory . '_' . $path, $defaultTemplateHook, FALSE, array( 'options' => $hookPoints, 'parse' => 'normal' ), NULL, NULL, NULL, 'template_hookpoint_' . $applicationDirectory . '_' . $path ) );
Member::loggedIn()->language()->words[ 'template_hookpoint_' . $applicationDirectory . '_' . $path ] = Member::loggedIn()->language()->get('template_hookpoint');
}
$form->add( new Select( 'template_hookpoint_type', $this->id ? $this->hookpoint_type : 'before', FALSE, array( 'options' => [
'before' => 'template_custom_before',
'inside-start' => 'template_custom_inside_start',
'inside-end' => 'template_custom_inside_end',
'after' => 'template_custom_after',
] ), NULL, NULL, NULL, 'template_hookpoint_type' ) );
$form->add( new Text( 'template_key', $this->id ? $this->key : NULL, FALSE, [], function( $val )
{
try
{
if ( ! $val )
{
if ( Request::i()->template_type == 'custom' )
{
/* This is required if we're not using a hookpoint */
throw new InvalidArgumentException('custom_template_key_cant_be_empty');
}
return true;
}
$val = Theme::cleanGroupName( $val );
try
{
$template = CustomTemplate::load( $val, 'template_key');
}
catch( OutOfRangeException $ex )
{
/* Doesn't exist? Good! */
return true;
}
/* It's taken... */
if ( Request::i()->id == $template->id )
{
/* But it's this one so that's ok */
return true;
}
/* and if we're here, it's not... */
throw new InvalidArgumentException('custom_template_key_not_unique');
}
catch ( OutOfRangeException $e )
{
/* Slug is OK as load failed */
return true;
}
}, NULL, NULL, 'template_key' ) );
$form->add( new Codemirror( 'template_content', $this->id ? $this->content : null, FALSE, [ 'codeModeAllowedLanguages' => [ 'ipsphtml' ] ], function( $val )
{
/* Test */
try
{
$currentApp = Request::i()->template_hookapp;
$params = '';
if ( $currentApp !== 'none' )
{
$path = Request::i()->{'template_path_' . $currentApp};
$hook = Request::i()->{'template_hookpoint_' . $currentApp . '_' . $path};
$template = new CustomTemplate();
$template->hookpoint = $currentApp . '/' . str_replace( '_', '/', $path ) . ':' . $hook;
$template->hookpoint_type = Request::i()->template_hookpoint_type;
$params = implode( ', ', $template->availableParams() );
}
Theme::checkTemplateSyntax( $val, $params );
}
catch( LogicException $e )
{
throw new LogicException('template_error_bad_syntax');
}
} ) );
/* If we are editing, we can save and reload */
if( $this->id )
{
$form->canSaveAndReload = true;
}
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( array $values ): array
{
$values['template_updated'] = DateTime::create()->getTimestamp();
$values['template_version'] = Application::load('core')->long_version;
$values['template_key'] = $values['template_key'] ? Theme::cleanGroupName( $values['template_key'] ) : null;
if ( $values['template_type'] === 'hook' and isset( $values[ 'template_path_' . $values['template_hookapp'] ] ) )
{
$currentApp = $values['template_hookapp'];
$path = $values[ 'template_path_' . $currentApp ];
$hook = $values[ 'template_hookpoint_' . $currentApp . '_' . $path ];
$values['template_hookpoint'] = $currentApp . '/' . str_replace( '_', '/', $path ) . ':' . $hook;
}
else
{
$values['template_hookpoint'] = null;
}
if ( static::$scopeKey === 'set_id' )
{
$values['template_set_id'] = (int) static::$scopeValue;
}
else if ( static::$scopeKey === 'appKey' )
{
$values['template_app'] = static::$scopeValue;
}
/* Unset some things we don't need */
foreach( $values as $k => $v )
{
if( $k == 'template_hookpoint_type' )
{
continue;
}
if ( stristr( $k, 'template_hookpoint_' ) or stristr( $k, 'template_path_' ) )
{
unset( $values[ $k ] );
}
}
unset( $values['template_hookapp'], $values['template_type'] );
return parent::formatFormValues( $values );
}
/**
* Import custom templates from a json file
*
* @param string $file
* @param string $appKey
* @param int|null $offset
* @param int|null $limit
* @return int
*/
public static function importFromFile( string $file, string $appKey, ?int $offset=null, ?int $limit=null ) : int
{
$templates = json_decode( file_get_contents( $file ), true );
$keys = array_keys( $templates );
$offset = $offset ?? 0;
if( $limit !== null )
{
$keys = array_slice( $keys, $offset, $limit );
}
$current = iterator_to_array(
Db::i()->select( '*', static::$databaseTable, [ 'template_app=?', $appKey ] )
->setKeyField( 'template_name' )
);
$imported = 0;
foreach( $keys as $name )
{
$data = $templates[ $name ];
if( isset( $current[ $name ] ) )
{
Db::i()->update( static::$databaseTable, [
'template_hookpoint' => $data['hookpoint'],
'template_hookpoint_type' => $data['type'],
'template_key' => $data['key'],
'template_version' => $data['version'],
'template_content' => $data['content'],
'template_updated' => time()
], [ 'template_id=?', $current[ $name ]['template_id'] ] );
unset( $current[ $name ] );
}
else
{
Db::i()->insert( static::$databaseTable, [
'template_app' => $appKey,
'template_name' => $name,
'template_hookpoint' => $data['hookpoint'],
'template_hookpoint_type' => $data['type'],
'template_key' => $data['key'],
'template_version' => $data['version'],
'template_content' => $data['content'],
'template_updated' => time()
]);
}
$imported++;
}
Db::i()->delete( static::$databaseTable, [
[ 'template_app=?', $appKey ],
[ Db::i()->in( 'template_name', array_keys( $current ) ) ]
]);
return $imported;
}
}
+36 -56
View File
@@ -11,87 +11,74 @@
namespace IPS\Theme\Dev;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use BadMethodCallException;
use IPS\Request;
use IPS\Theme;
use IPS\Theme\Template as ThemeTemplate;
use function defined;
use function function_exists;
use function get_called_class;
use function in_array;
use const IPS\DEBUG_TEMPLATES;
use const IPS\IN_DEV;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Magic Template Class for IN_DEV mode
*/
class _Template extends \IPS\Theme\Template
class Template extends ThemeTemplate
{
/**
* @brief Source Folder
*/
public $sourceFolder = NULL;
public ?string $sourceFolder = NULL;
/**
* Contructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
public function __construct( string $app, string $templateLocation, string $templateName )
{
parent::__construct( $app, $templateLocation, $templateName );
$this->app = $app;
$this->templateLocation = $templateLocation;
$this->templateName = $templateName;
if ( $this->app === 'core' and $this->templateLocation === 'global' and $this->templateName === 'plugins' )
{
$this->sourceFolder = \IPS\ROOT_PATH . '/plugins';
}
else
{
$this->sourceFolder = \IPS\ROOT_PATH . "/applications/{$app}/dev/html/{$templateLocation}/{$templateName}/";
}
$this->sourceFolder = \IPS\ROOT_PATH . "/applications/{$app}/dev/html/{$templateLocation}/{$templateName}/";
}
/**
* Magic Method: Call Template Bit
*
* @param string $bit Template Bit Name
* @param array $params Parameters
* @param string $bit Template Bit Name
* @param array $params Parameters
* @return string
*/
public function __call( $bit, $params )
public function __call( string $bit, array $params )
{
/* What are we calling this? */
$functionName = "theme_{$this->app}_{$this->templateLocation}_{$this->templateName}_{$bit}";
/* If it doesn't exist, build it */
if( !\function_exists( 'IPS\\Theme\\'.$functionName ) )
if( !function_exists( 'IPS\\Theme\\'.$functionName ) )
{
/* Find the file */
$file = NULL;
if ( $this->sourceFolder === \IPS\ROOT_PATH . '/plugins' )
{
foreach ( new \GlobIterator( $this->sourceFolder . '/*/dev/html/' . $bit . '.phtml' ) as $file )
{
break;
}
}
else
{
$file = $this->sourceFolder . $bit . '.phtml';
}
$file = $this->sourceFolder . $bit . '.phtml';
/* Get the content or return an BadMethodCallException if the template doesn't exist */
if ( $file === NULL or !file_exists( $file ) )
if ( !file_exists( $file ) )
{
if ( !$file AND $this->sourceFolder === \IPS\ROOT_PATH . '/plugins' )
{
throw new \BadMethodCallException( 'NO_PLUGIN_TEMPLATE_FILE - ' . $bit );
}
else
{
throw new \BadMethodCallException( 'NO_TEMPLATE_FILE - ' . $file );
}
throw new BadMethodCallException( 'NO_TEMPLATE_FILE - ' . $file );
}
$output = file_get_contents( $file );
@@ -99,18 +86,18 @@ class _Template extends \IPS\Theme\Template
/* Parse the header tag */
if ( !preg_match( '/^<ips:template parameters="(.+?)?"(\s+)?\/>(\r\n?|(\r\n?|\n))/', $output, $matches ) )
{
throw new \BadMethodCallException( 'NO_HEADER - ' . $file );
throw new BadMethodCallException( 'NO_HEADER - ' . $file );
}
/* Strip it */
$output = preg_replace( '/^<ips:template parameters="(.+?)?"(\s+)?\/>(\r\n?|\n)/', '', $output );
if ( \IPS\DEBUG_TEMPLATES and ! \IPS\Request::i()->isAjax() )
if ( DEBUG_TEMPLATES and ! Request::i()->isAjax() )
{
$output = "<!-- " . $functionName . " -->" . $output;
}
if ( \IPS\IN_DEV AND \get_called_class() !== 'IPS\Theme\System\Template' )
if ( IN_DEV AND get_called_class() !== 'IPS\Theme\System\Template' )
{
/* Template names that will allow inline style="" attributes */
$allowedInlineStyle = array(
@@ -136,11 +123,11 @@ class _Template extends \IPS\Theme\Template
);
/* Check we're not being naughty */
if( preg_match( "/<.+?style=['\"].+?>/i", $output ) and !\in_array( $functionName, $allowedInlineStyle ) && $this->app != 'documentation' )
if( preg_match( "/<.+?style=['\"].+?>/i", $output ) and !in_array( $functionName, $allowedInlineStyle ) && $this->app != 'documentation' )
{
//trigger_error( "There is inline CSS in {$functionName}. Please move all styling into CSS files.", E_USER_ERROR );
}
if( !\in_array( $bit, $allowedStyleBlocks ) and preg_match( "/<style.*?>/i", $output ) && $this->app != 'documentation' )
if( !in_array( $bit, $allowedStyleBlocks ) and preg_match( "/<style.*?>/i", $output ) && $this->app != 'documentation' )
{
//trigger_error( "There is a style block in {$functionName}. Please move all styling into CSS files.", E_USER_ERROR );
}
@@ -148,21 +135,14 @@ class _Template extends \IPS\Theme\Template
{
//trigger_error( "There is a inline JavaScript {$functionName}. Please move all JavaScript into JS files.", E_USER_ERROR );
}
if( !\in_array( $bit, $allowedScriptBlocks ) and $this->templateName !== 'embed' and preg_match( "/<script((?!src).)*>/i", $output ) && $this->app != 'documentation' )
if( !in_array( $bit, $allowedScriptBlocks ) and $this->templateName !== 'embed' and preg_match( "/<script((?!src).)*>/i", $output ) && $this->app != 'documentation' )
{
//trigger_error( "There is a script block in {$functionName}. Please move all JavaScript into JS files.", E_USER_ERROR );
}
/* Hooks */
$hookData = static::hookData();
if ( isset( $hookData[ $bit ] ) )
{
$output = \IPS\Theme::themeHooks( $output, $hookData[ $bit ] );
}
}
/* Make it into a lovely function */
\IPS\Theme::makeProcessFunction( $output, $functionName, ( isset( $matches[1] ) ? $matches[1] : '' ), TRUE );
Theme::makeProcessFunction( $output, $functionName, ( $matches[1] ?? '' ) );
}
/* Run it */
+303 -340
View File
File diff suppressed because it is too large. Load diff
+496
View File
@@ -0,0 +1,496 @@
<?php
/**
* @brief Category
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 2/14/2024
*/
namespace IPS\Theme\Editor;
/* To prevent PHP errors (extending class does not exist) revealing path */
use InvalidArgumentException;
use IPS\Application;
use IPS\Dispatcher;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Icon;
use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Translatable;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Node\Model;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use const IPS\IN_DEV;
use function substr;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class Category extends Model
{
/**
* @brief [ActiveRecord] Multiton Store
* @note This needs to be declared in any child classes as well, only declaring here for editor code-complete/error-check functionality
*/
protected static array $multitons = array();
/**
* @brief [ActiveRecord] Database Prefix
*/
public static string $databasePrefix = 'cat_';
/**
* @brief [ActiveRecord] Database table
* @note This MUST be over-ridden
*/
public static ?string $databaseTable = 'core_theme_editor_categories';
/**
* @brief [ActiveRecord] Database ID Fields
* @note If using this, declare a static $multitonMap = array(); in the child class to prevent duplicate loading queries
*/
protected static array $databaseIdFields = array( 'cat_key' );
/**
* @brief [Node] Order Database Column
*/
public static ?string $databaseColumnOrder = 'position';
/**
* @brief [Node] Parent ID Database Column
*/
public static ?string $databaseColumnParent = 'parent';
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}" as the key
*/
public static ?string $titleLangPrefix = 'themeeditor_cat_';
/**
* @brief [Node] Show forms modally?
*/
public static bool $modalForms = true;
/**
* @brief [ActiveRecord] Multiton Map
*/
protected static array $multitonMap = array();
/**
* @brief [Node] Node Title
*/
public static string $nodeTitle = 'theme_editor_categories';
/**
* @brief [Node] Subnode class
*/
public static ?string $subnodeClass = Setting::class;
/**
* [Node] Get Icon for tree
*
* @note Return the class for the icon (e.g. 'globe', the 'fa fa-' is added automatically so you do not need this here)
* @return mixed
*/
protected function get__icon(): mixed
{
if( $this->icon )
{
$icon = json_decode( $this->icon, true );
$suffix = substr( $icon['key'], -3 );
switch( $suffix )
{
case 'fab':
$icon['title'] = 'fa-brands fa-' . $icon['title'];
break;
case 'far':
$icon['title'] = 'fa-regular fa-' . $icon['title'];
break;
}
if( isset( $icon['extra'] ) )
{
$icon['title'] .= ' fa-' . $icon['extra'];
}
return $icon['title'];
}
return '';
}
/**
* @return string
*/
public function icon() : string
{
if( $this->icon )
{
$icon = json_decode( $this->icon, true );
$bits = explode( ":", $icon['key'] );
$return = 'fa-';
switch( $bits[1] )
{
case 'fas':
$return .= 'solid';
break;
case 'far':
$return .= 'regular';
break;
case 'fab':
$return .= 'brands';
break;
}
$return .= ' fa-' . $bits[0];
if( isset( $icon['extra'] ) )
{
$return .= ' ' . $icon['extra'];
}
return $return;
}
return '';
}
/**
* Generate icon data from the icon string
*
* @param string $key
* @return string[]
*/
public static function buildIconData( string $key ) : array
{
$suffix = null;
$icon = [
'type' => 'fa',
'raw' => "<i class='" . $key . "'></i>"
];
foreach( explode( "fa-", $key ) as $string )
{
$string = trim( $string );
switch( $string )
{
case 'solid':
$suffix = 'fas';
break;
case 'brands':
$suffix = 'fab';
break;
case 'regular':
$suffix = 'far';
break;
default:
if( !isset( $icon['title'] ) or empty( $icon['title'] ) )
{
$icon['title'] = $string;
}
else
{
$icon['extra'] = $string;
}
break;
}
}
$icon['key'] = $icon['title'] . ':' . ( $suffix ?? 'fas' );
$icon['html'] = Theme::i()->getTemplate( 'global', 'core', 'global' )->icon( $icon );
return $icon;
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
*
* @code
* array(
* array(
* 'icon' => 'plus-circle', // Name of FontAwesome icon to use
* 'title' => 'foo', // Language key to use for button's title parameter
* 'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
* 'class' => 'modalLink' // CSS Class to use on link (Optional)
* ),
* ... // Additional buttons
* );
* @endcode
* @param Url $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( Url $url, bool $subnode=FALSE ): array
{
$buttons = parent::getButtons( $url, $subnode );
if( $this->hasChildren( null, null, false ) or !$this->hasContents() )
{
$buttons['child'] = [
'icon' => 'plus',
'title' => 'themeeditor_cat_add',
'link' => $url->setQueryString( array( 'do' => 'categoryForm', 'parent' => $this->id ) )
];
}
/* Don't allow modification of 1st party settings if we're in theme designer mode */
if( !IN_DEV and Settings::i()->theme_designer_mode and !$this->set_id )
{
unset( $buttons['edit'] );
unset( $buttons['delete'] );
}
return $buttons;
}
/**
* [Node] Does the currently logged in user have permission to add a child node to this node?
*
* @return bool
*/
public function canAdd(): bool
{
if( !parent::canAdd() )
{
return false;
}
if( $this->hasChildren( null, null, false ) )
{
return false;
}
return true;
}
/**
* [Node] Add/Edit Form
*
* @param Form $form The form
* @return void
*/
public function form( Form &$form ) : void
{
$form->add( new Translatable( 'themeeditor_cat_name', null, true, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_cat_' . $this->id : null )
) ) );
$form->add( new Text( 'themeeditor_cat_key', $this->key, true, array(
'regex' => '/[A-Z]+/i',
'maxLength' => 150
), function( $val ){
try
{
$test = static::load( $val, 'cat_key' );
if( $test->id != $this->id )
{
throw new InvalidArgumentException( "err__duplicate_cat_key" );
}
}
catch( OutOfRangeException ){}
}) );
$iconData = $this->icon ? json_decode( $this->icon, true ) : [];
$form->add( new Icon( 'themeeditor_cat_icon', count( $iconData ) ? array( $iconData ) : null, true, array(
'emoji' => false,
'maxIcons' => 1
) ) );
$form->add( new Text( 'themeeditor_cat_icon_extra', $iconData['extra'] ?? null, false ) );
$form->add( new Node( 'themeeditor_cat_parent', $this->parent, false, array(
'class' => Category::class,
'subnodes' => false,
'autoPopulate' => false,
'multiple' => false,
'permissionCheck' => function( $val ){
if( $val instanceof Category and ( $val->hasSettings() or $val->hasColors() ) )
{
return false;
}
return true;
}
) ) );
if( Dispatcher::i()->module->key == 'customization' )
{
$form->add( new Node( 'themeeditor_cat_app', $this->app ? Application::load( $this->app ) : Application::load( 'core' ), true, array(
'class' => Application::class,
'multiple' => false,
'subnodes' => false
) ) );
}
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( array $values ): array
{
if( !$this->id )
{
$this->key = $values['themeeditor_cat_key'];
$this->save();
}
$icon = $values['themeeditor_cat_icon'][0];
if( $values['themeeditor_cat_icon_extra'] )
{
$icon['extra'] = $values['themeeditor_cat_icon_extra'];
}
$return = [
'cat_key' => $values['themeeditor_cat_key'],
'cat_icon' => json_encode( $icon ),
'cat_parent' => ( $values['themeeditor_cat_parent'] instanceof Model ? $values['themeeditor_cat_parent']->id : 0 ),
'cat_set_id' => Request::i()->set_id ?? 0
];
if( isset( $values['themeeditor_cat_app'] ) and $values['themeeditor_cat_app'] instanceof Application )
{
$return['cat_app'] = $values['themeeditor_cat_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->id, $values['themeeditor_cat_name'] );
return $return;
}
/**
* Does the category have anything in it?
*
* @return bool
*/
public function hasContents() : bool
{
return ( $this->hasChildren() or $this->hasSettings() or $this->hasColors() );
}
/**
* @var array|null
*/
protected ?array $_settings = null;
/**
* Load category settings
*
* @return void
*/
protected function loadSettings() : void
{
if( $this->_settings === null )
{
$this->_settings = [];
$this->_colors = [];
foreach( Setting::loadByCategory( $this ) as $setting )
{
if( $setting->type == Setting::SETTING_COLOR )
{
$this->_colors[] = $setting;
}
else
{
$this->_settings[] = $setting;
}
}
}
}
/**
* Return all settings in this category
*
* @return array
*/
public function settings() : array
{
$this->loadSettings();
return $this->_settings;
}
/**
* Determines if this category has any settings
*
* @return bool
*/
public function hasSettings() : bool
{
return (bool) ( count( $this->settings() ) );
}
/**
* @var array|null
*/
protected ?array $_colors = null;
/**
* Return all color settings in this category
*
* @return array
*/
public function colors() : array
{
$this->loadSettings();
return $this->_colors;
}
/**
* Determines if this category has any color settings
*
* @return bool
*/
public function hasColors() : bool
{
return (bool) ( count( $this->colors() ) );
}
/**
* @return string
*/
public function editorHtml() : string
{
return ( $this->hasContents() ) ? Theme::i()->getTemplate( 'themeeditor', 'core', 'front' )->editorPanel( $this ) : '';
}
/**
* Return only the editor categories that should be visible in the
* theme editor. Some may be related to disabled apps,
* some may be related to another theme.
*
* @return array
*/
public static function themeEditorCategories() : array
{
$appKeys = array_keys( Application::enabledApplications() );
$return = [];
foreach( static::roots( null ) as $cat )
{
if( $cat->app and !in_array( $cat->app, $appKeys ) )
{
continue;
}
if( $cat->set_id and !in_array( $cat->set_id, array( Theme::$defaultFrontendThemeSet, Theme::i()->id ) ) )
{
continue;
}
$return[] = $cat;
}
return $return;
}
}
+608
View File
@@ -0,0 +1,608 @@
<?php
/**
* @brief Setting
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 2/14/2024
*/
namespace IPS\Theme\Editor;
/* To prevent PHP errors (extending class does not exist) revealing path */
use InvalidArgumentException;
use IPS\Application;
use IPS\Data\Store;
use IPS\Db;
use IPS\Dispatcher;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Codemirror;
use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Number;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Translatable;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Lang;
use IPS\Member;
use IPS\Node\Model;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Request;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
use const IPS\IN_DEV;
use function str_replace;
use function trim;
use function ucwords;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class Setting extends Model
{
const SETTING_TEXT = 'text';
const SETTING_NUMBER = 'range';
const SETTING_COLOR = 'color';
const SETTING_SELECT = 'select';
const SETTING_CHECKBOX = 'checkbox';
const SETTING_IMAGE = 'image';
/**
* @brief [ActiveRecord] Multiton Store
* @note This needs to be declared in any child classes as well, only declaring here for editor code-complete/error-check functionality
*/
protected static array $multitons = array();
/**
* @brief [ActiveRecord] Database Prefix
*/
public static string $databasePrefix = 'setting_';
/**
* @brief [ActiveRecord] Database table
* @note This MUST be over-ridden
*/
public static ?string $databaseTable = 'core_theme_editor_settings';
/**
* @var array
*/
protected static array $multitonMap = array();
/**
* @brief [ActiveRecord] Database ID Fields
* @note If using this, declare a static $multitonMap = array(); in the child class to prevent duplicate loading queries
*/
protected static array $databaseIdFields = array( 'setting_key' );
/**
* @brief [Node] Order Database Column
*/
public static ?string $databaseColumnOrder = 'position';
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}" as the key
*/
public static ?string $titleLangPrefix = 'themeeditor_var_';
/**
* @brief [Node] Description suffix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
*/
public static ?string $descriptionLangSuffix = '_desc';
/**
* @brief [Node] Parent Node ID Database Column
*/
public static string $parentNodeColumnId = 'category_id';
/**
* @brief [Node] Parent Node Class
*/
public static string $parentNodeClass = Category::class;
/**
* @brief [Node] Node Title
*/
public static string $nodeTitle = 'theme_editor_settings';
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected array $caches = array( 'themeEditorSettings' );
/**
* @brief [ActiveRecord] Attempt to load from cache
* @note If this is set to TRUE you should define a getStore() method to return the objects from cache
*/
protected static bool $loadFromCache = true;
/**
* Attempt to load cached data
*
* @note This should be overridden in your class if you define $cacheToLoadFrom
* @return array
*/
public static function getStore(): array
{
try
{
$cache = Store::i()->themeEditorSettings;
}
catch( OutOfRangeException )
{
$cache = iterator_to_array( Db::i()->select( '*', static::$databaseTable, NULL, static::$databasePrefix . static::$databaseColumnId )->setKeyField( static::$databasePrefix . static::$databaseColumnId ) );
Store::i()->themeEditorSettings = $cache;
}
return $cache;
}
/**
* Load all the setting into the cache in one shot
*
* @return void
*/
public static function loadAllKeys() : void
{
if( !isset( static::$multitonMap[ 'setting_key'] ) )
{
foreach( new ActiveRecordIterator(
Db::i()->select( '*', static::$databaseTable ),
Setting::class
) as $setting )
{
if( !isset( static::$multitons[ $setting->id ] ) )
{
static::$multitons[ $setting->id ] = $setting;
}
/* Color settings will be called with the prefix light__ or dark__ */
if( $setting->type == static::SETTING_COLOR )
{
static::$multitonMap['setting_key'][ 'dark__' . $setting->key ] = $setting->id;
static::$multitonMap['setting_key'][ 'light__' . $setting->key ] = $setting->id;
}
static::$multitonMap['setting_key'][ $setting->key ] = $setting->id;
}
}
}
/**
* Fetch All Root Nodes
*
* @param string|null $permissionCheck The permission key to check for or NULl to not check permissions
* @param Member|null $member The member to check permissions for or NULL for the currently logged in member
* @param mixed $where Additional WHERE clause
* @param array|null $limit Limit/offset to use, or NULL for no limit (default)
* @return Model[]
*/
public static function roots( ?string $permissionCheck='view', Member $member=NULL, mixed $where=array(), array $limit=NULL ): array
{
$return = parent::roots( $permissionCheck, $member, $where, $limit );
static::loadAllKeys();
return $return;
}
/**
* @return string
*/
public function get_name() : string
{
return Member::loggedIn()->language()->addToStack( 'themeeditor_var_' . $this->id );
}
/**
* @return string
*/
public function get_desc() : string
{
if( Member::loggedIn()->language()->checkKeyExists( 'themeeditor_var_' . $this->id . '_desc' ) )
{
return Member::loggedIn()->language()->addToStack( 'themeeditor_var_' . $this->id . '_desc' );
}
return '';
}
/**
* @return array
*/
public function get_data() : array
{
return isset( $this->_data['data'] ) ? json_decode( $this->_data['data'], true ) : array();
}
/**
* @return array|string
*/
public function get_default() : array|string
{
if( $this->type == static::SETTING_COLOR )
{
return isset( $this->_data['default'] ) ? json_decode( $this->_data['default'], true ) : array( 'light' => '', 'dark' => '' );
}
elseif( $this->type == static::SETTING_NUMBER )
{
return isset( $this->_data['default'] ) ? (int) $this->_data['default'] : 0;
}
return $this->_data['default'] ?? '';
}
/**
* @param array|string|null $val
* @return void
*/
public function set_data( array|string|null $val ) : void
{
if( is_array( $val ) )
{
$val = json_encode( $val );
}
$this->_data['data'] = $val ?: null;
}
/**
* @var array|null
*/
protected static ?array $appKeys = null;
/**
* Load all settings for a given category
*
* @param Category $category
* @return array
*/
public static function loadByCategory( Category $category ) : array
{
if( static::$appKeys === null )
{
static::$appKeys = array_keys( Application::enabledApplications() );
}
$return = [];
foreach( new ActiveRecordIterator(
Db::i()->select( '*', static::$databaseTable, array( 'setting_category_id=?', $category->id ), 'setting_position' ),
Setting::class
) as $setting )
{
/* Make sure we only use enabled applications */
if( $setting->app and !in_array( $setting->app, static::$appKeys ) )
{
continue;
}
if( $setting->set_id and !in_array( $setting->set_id, array( Theme::$defaultFrontendThemeSet, Theme::i()->id ) ) )
{
continue;
}
$return[] = $setting;
}
return $return;
}
/**
* Figure out the value for this setting
*
* @param bool $forceDefault
* @return string|array
*/
public function value( bool $forceDefault=false ): string|array
{
/* Do we have an override in the theme? */
$overrides = $forceDefault ? [] : Theme::i()->getCssVariables( Theme::CUSTOM_ONLY );
switch( $this->type )
{
case static::SETTING_COLOR:
if( $forceDefault )
{
return [
'light' => $this->default['light'],
'dark' => $this->default['dark']
];
}
return [
'light' => ( $overrides['light__' . $this->key ] ?? $this->default['light'] ),
'dark' => ( $overrides['dark__' . $this->key ] ?? $this->default['dark'] )
];
default:
if( $forceDefault )
{
$value = $this->default;
}
else
{
$value = $overrides[ $this->key ] ?? $this->default;
}
if( empty( $value ) and $value != 0 )
{
return "";
}
/* If we have an override,then the admin manually entered text, so treat it
as plain text */
if( !$forceDefault and $this->type == static::SETTING_TEXT and isset( $overrides[ $this->key ] ) )
{
$value = htmlentities( $value, ENT_QUOTES );
}
$functionName = 'setting__' . ( $forceDefault ? 'default__' : '' ) . str_replace( '-', '_', $this->key );
Theme::runProcessFunction( Theme::_compileTemplate( $value, $functionName ), $functionName );
$settingFunction = 'IPS\\Theme\\' . $functionName;
$return = $settingFunction();
return trim( $return );
}
}
/**
* Wrapper to make things easier in the templates
*
* @return string|array
*/
public function defaultValue() : string|array
{
return $this->value( true );
}
/**
* Parse the value of this setting based on the setting type
*
* @param string|null $value
* @return string|int
*/
public function parsedValue( ?string $value ) : string|int
{
if( $value === null )
{
$value = Theme::i()->getCssVariables()[ $this->key ] ?? '';
}
switch( $this->type )
{
case static::SETTING_IMAGE:
$value = 'url("' . $value . '")';
break;
case static::SETTING_TEXT:
$value = strip_tags( $value );
$value = str_replace( array( '/*', ';' ), '', $value );
$value = htmlentities( $value );
break;
case static::SETTING_CHECKBOX:
$value = (int) $value;
break;
}
return $value;
}
/**
* @return string
*/
public function editorHtml() : string
{
$template = 'setting' . ucwords( $this->type );
return Theme::i()->getTemplate( 'themeeditor', 'core', 'front' )->$template( $this );
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
*
* @code
* array(
* array(
* 'icon' => 'plus-circle', // Name of FontAwesome icon to use
* 'title' => 'foo', // Language key to use for button's title parameter
* 'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
* 'class' => 'modalLink' // CSS Class to use on link (Optional)
* ),
* ... // Additional buttons
* );
* @endcode
* @param Url $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( Url $url, bool $subnode=FALSE ): array
{
$buttons = parent::getButtons( $url, $subnode );
/* Don't allow modification of 1st party settings if we're in theme designer mode */
if( !IN_DEV and Settings::i()->theme_designer_mode and !$this->set_id and in_array( $this->app, IPS::$ipsApps ) )
{
unset( $buttons['edit'] );
unset( $buttons['delete'] );
}
return $buttons;
}
/**
* [Node] Add/Edit Form
*
* @param Form $form The form
* @return void
*/
public function form( Form &$form ) : void
{
$form->add( new Translatable( 'themeeditor_setting_name', null, true, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_var_' . $this->id : null )
) ) );
$form->add( new Translatable( 'themeeditor_setting_desc', null, false, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_var_' . $this->id . '_desc' : null )
) ) );
$form->add( new Text( 'themeeditor_setting_key', $this->key, true, array(
'regex' => '/[A-Z]+/i',
'maxLength' => 150
), function( $val ){
try
{
$test = static::load( $val, 'setting_key' );
if( $test->id != $this->id )
{
throw new InvalidArgumentException( "err__duplicate_setting_key" );
}
}
catch( OutOfRangeException ){}
}) );
$form->add( new Node( 'themeeditor_setting_category', $this->parent(), true, array(
'class' => Category::class,
'multiple' => false,
'subnodes' => false,
'permissionCheck' => function( $val ){
if( $val instanceof Category and ( $val->hasSettings() or $val->hasColors() or !$val->hasContents() ) )
{
return true;
}
return false;
}
) ) );
$form->add( new Select( 'themeeditor_setting_type', $this->type, true, array(
'options' => array(
static::SETTING_COLOR => 'themeeditor_setting_type__' . static::SETTING_COLOR,
static::SETTING_TEXT => 'themeeditor_setting_type__' . static::SETTING_TEXT,
static::SETTING_NUMBER => 'themeeditor_setting_type__' . static::SETTING_NUMBER,
static::SETTING_SELECT => 'themeeditor_setting_type__' . static::SETTING_SELECT,
static::SETTING_CHECKBOX => 'themeeditor_setting_type__' . static::SETTING_CHECKBOX,
static::SETTING_IMAGE => 'themeeditor_setting_type__' . static::SETTING_IMAGE
),
'toggles' => array(
static::SETTING_COLOR => array( 'themeeditor_setting_light_default', 'themeeditor_setting_dark_default' ),
static::SETTING_TEXT => array( 'themeeditor_setting_default' ),
static::SETTING_NUMBER => array( 'themeeditor_setting_min', 'themeeditor_setting_max', 'themeeditor_setting_step', 'themeeditor_setting_default' ),
static::SETTING_SELECT => array( 'themeeditor_setting_options', 'themeeditor_setting_default' ),
static::SETTING_CHECKBOX => array( 'themeeditor_setting_default' ),
static::SETTING_IMAGE => array( 'themeeditor_setting_default' )
)
) ) );
$form->add( new Number( 'themeeditor_setting_min', $this->data['min'] ?? null, false, array(), null, null, null, 'themeeditor_setting_min' ) );
$form->add( new Number( 'themeeditor_setting_max', $this->data['max'] ?? null, null, array(), null, null, null, 'themeeditor_setting_max' ) );
$form->add( new Number( 'themeeditor_setting_step', $this->data['step'] ?? 1, false, array(
'decimals' => 2
), null, null, null, 'themeeditor_setting_step' ) );
$options = [];
if( isset( $this->data['options'] ) )
{
foreach( $this->data['options'] as $opt )
{
$options[] = [ 'key' => $opt[0], 'value' => $opt[1] ];
}
}
$form->add( new Form\Stack( 'themeeditor_setting_options', count( $options ) ? $options : null, null, array(
'stackFieldType' => 'KeyValue'
), null, null, null, 'themeeditor_setting_options' ) );
$form->add( new Codemirror( 'themeeditor_setting_default', is_array( $this->default ) ? null : $this->default, false, array(
'height' => 100
), null, null, null, 'themeeditor_setting_default' ) );
$form->add( new Codemirror( 'themeeditor_setting_light_default', is_array( $this->default ) ? $this->default['light'] : null, false, array(
'height' => 100
), null, null, null, 'themeeditor_setting_light_default' ) );
$form->add( new Codemirror( 'themeeditor_setting_dark_default', is_array( $this->default ) ? $this->default['dark'] : null, false, array(
'height' => 100
), null, null, null, 'themeeditor_setting_dark_default' ) );
$form->add( new YesNo( 'themeeditor_setting_refresh', $this->refresh, false ) );
if( Dispatcher::i()->module->key == 'customization' )
{
$form->add( new Node( 'themeeditor_setting_app', $this->app ? Application::load( $this->app ) : Application::load( 'core' ), true, array(
'class' => Application::class,
'multiple' => false,
'subnodes' => false
) ) );
}
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( array $values ): array
{
if( !$this->id )
{
$this->save();
}
$return = [
'setting_key' => $values['themeeditor_setting_key'],
'setting_category_id' => ( $values['themeeditor_setting_category'] instanceof Category ) ? $values['themeeditor_setting_category']->id : 0,
'setting_type' => $values['themeeditor_setting_type'],
'setting_set_id' => Request::i()->set_id ?? 0,
'setting_refresh' => (bool) $values['themeeditor_setting_refresh']
];
switch( $values['themeeditor_setting_type' ] )
{
case static::SETTING_COLOR:
$return['setting_default'] = json_encode( array(
'light' => $values['themeeditor_setting_light_default'],
'dark' => $values['themeeditor_setting_dark_default']
) );
break;
case static::SETTING_SELECT:
$options = [];
foreach( $values['themeeditor_setting_options'] as $opt )
{
$options[] = [ $opt['key'], $opt['value'] ];
}
$return['setting_data'] = array( 'options' => $options );
$return['setting_default'] = $values['themeeditor_setting_default'];
break;
case static::SETTING_NUMBER:
$return['setting_data'] = array(
'min' => $values['themeeditor_setting_min'],
'max' => $values['themeeditor_setting_max'],
'step' => $values['themeeditor_setting_step']
);
$return['setting_default'] = $values['themeeditor_setting_default'];
break;
default:
$return['setting_default'] = $values['themeeditor_setting_default'];
break;
}
if( isset( $values['themeeditor_setting_app'] ) and $values['themeeditor_setting_app'] instanceof Application )
{
$return['setting_app'] = $values['themeeditor_setting_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->id, $values['themeeditor_setting_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->id . '_desc', $values['themeeditor_setting_desc'] );
return $return;
}
}
+20 -13
View File
@@ -11,29 +11,36 @@
namespace IPS\Theme;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Error;
use ErrorException;
use IPS\Log;
use Throwable;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Sameboxed Template Class
*/
class _SandboxedTemplate
class SandboxedTemplate
{
/**
* @brief Template
*/
public $template;
public Template $template;
/**
* Contructor
*
* @param \IPS\Theme\Template $template Template to use
* @param Template $template Template to use
* @return void
*/
public function __construct( $template )
public function __construct( Template $template )
{
$this->template = $template;
}
@@ -41,11 +48,11 @@ class _SandboxedTemplate
/**
* Call
*
* @param string $name Method name
* @param array $args Method arguments
* @param string $name Method name
* @param array $args Method arguments
* @return string
*/
public function __call( $name, $args )
public function __call( string $name, array $args )
{
try
{
@@ -60,15 +67,15 @@ class _SandboxedTemplate
{
return $this->template->$name( ...$args );
}
catch ( \ErrorException $e )
catch ( ErrorException $e )
{
throw new \Error( $e );
throw new Error( $e );
}
}
}
catch( \Throwable $e )
catch( Throwable $e )
{
\IPS\Log::log( $e, 'template_error' );
Log::log( $e, 'template_error' );
return "<span style='background:black;color:white;padding:6px;'>[[Template {$this->template->app}/{$this->template->templateLocation}/{$this->template->templateName}/{$name} is throwing an error. This theme may be out of date. Run the support tool in the AdminCP to restore the default theme.]]</span>";
}
+17 -10
View File
@@ -11,16 +11,23 @@
namespace IPS\Theme\Setup;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Dispatcher;
use IPS\Theme as SystemTheme;
use IPS\Theme\Dev\Template as DevTemplate;
use IPS\Theme\Template;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* IN_DEV Skin set
*/
class _Theme extends \IPS\Theme
class Theme extends SystemTheme
{
/**
* Constructor
@@ -33,15 +40,15 @@ class _Theme extends \IPS\Theme
/**
* Get template
*
* @param string $template Template name
* @param string $app Application key (NULL for current application)
* @param string $templateLocation Template location (NULL for current template location)
* @return \IPS\Theme\Template
* @param string $group Template name
* @param string|null $app Application key (NULL for current application)
* @param string|null $location Template location (NULL for current template location)
* @return Template
*/
public function getTemplate( $template, $app=NULL, $templateLocation=NULL )
public function getTemplate( string $group, string $app=NULL, string $location=NULL ): Template
{
$obj = new \IPS\Theme\Dev\Template( $template, 'core', 'setup' );
$obj->sourceFolder = \IPS\ROOT_PATH . "/" . \IPS\CP_DIRECTORY . "/" . \IPS\Dispatcher::i()->setupLocation . "/html/{$template}/";
$obj = new DevTemplate( $group, 'core', 'setup' );
$obj->sourceFolder = \IPS\ROOT_PATH . "/admin/" . Dispatcher::i()->setupLocation . "/html/{$group}/";
return $obj;
}
}
+12 -8
View File
@@ -11,31 +11,35 @@
namespace IPS\Theme\System;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Theme\Dev\Template as DevTemplate;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Magic Template Class for BASIC mode
*/
class _Template extends \IPS\Theme\Dev\Template
class Template extends DevTemplate
{
/**
* @brief Source Folder
*/
public $sourceFolder = NULL;
public ?string $sourceFolder = NULL;
/**
* Contructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
public function __construct( string $app, string $templateLocation, string $templateName )
{
parent::__construct( $app, $templateLocation, $templateName );
$this->app = $app;
+25 -20
View File
@@ -11,38 +11,43 @@
namespace IPS\Theme\System;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Dispatcher;
use function defined;
use const IPS\DEFAULT_THEME_ID;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Basic themes
*/
class _Theme extends \IPS\Theme\Dev\Theme
class Theme extends \IPS\Theme\Dev\Theme
{
/**
* @brief Template Classes
*/
protected $templates;
protected array $templates = array();
/**
* Get currently logged in member's theme
*
* @return \IPS\Theme
* @return Theme
*/
public static function i()
public static function i(): Theme
{
if ( static::$memberTheme === null )
{
static::themes();
static::$memberTheme = new self;
if ( \IPS\Dispatcher::i()->controllerLocation === 'front' )
if ( Dispatcher::i()->controllerLocation === 'front' )
{
/* Add in the default theme properties (_data array, etc) */
foreach( static::$multitons[ \IPS\DEFAULT_THEME_ID ] as $k => $v )
foreach( static::$multitons[ DEFAULT_THEME_ID ] as $k => $v )
{
static::$memberTheme->$k = $v;
}
@@ -55,30 +60,30 @@ class _Theme extends \IPS\Theme\Dev\Theme
/**
* Get a template
*
* @param string $group Template Group
* @param string $app Application key (NULL for current application)
* @param string $location Template Location (NULL for current template location)
* @return \IPS\Theme\Template
* @param string $group Template Group
* @param string|null $app Application key (NULL for current application)
* @param string|null $location Template Location (NULL for current template location)
* @return Template
*/
public function getTemplate( $group, $app=NULL, $location=NULL )
public function getTemplate( string $group, string $app=NULL, string $location=NULL ): Template
{
/* Do we have an application? */
if ( $app === NULL )
{
$app = \IPS\Dispatcher::i()->application->directory;
$app = Dispatcher::i()->application->directory;
}
return new \IPS\Theme\System\Template( $app, $location, $group );
return new Template( $app, $location, $group );
}
/**
* Returns the path for the IN_DEV .phtml files
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @return string
*/
protected static function _getHtmlPath( $app, $location=null, $path=null )
protected static function _getHtmlPath( string $app, string $location=null, string $path=null ): string
{
return rtrim( \IPS\ROOT_PATH . "/applications/{$app}/data/html/{$location}/{$path}", '/' ) . '/';
}
@@ -87,7 +92,7 @@ class _Theme extends \IPS\Theme\Dev\Theme
* Returns the namespace for the template class
* @return string
*/
protected static function _getTemplateNamespace()
protected static function _getTemplateNamespace(): string
{
return 'IPS\\Theme\\Basic\\';
}
+25 -17
View File
@@ -11,64 +11,72 @@
namespace IPS\Theme;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Template Class
*/
abstract class _Template
abstract class Template
{
/**
* @brief Application key
*/
public $app = NULL;
public ?string $app = NULL;
/**
* @brief Template Location
*/
public $templateLocation = NULL;
public ?string $templateLocation = NULL;
/**
* @brief Template Name
*/
public $templateName = NULL;
public ?string $templateName = NULL;
/**
* Contructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
public function __construct( string $app, string $templateLocation, string $templateName )
{
$this->app = $app;
$this->templateLocation = $templateLocation;
$this->templateName = $templateName;
}
/**
* Return the app/location/group params
*
* @return array
*/
public function getParams()
public function getParams(): array
{
return array( 'app' => $this->app, 'location' => $this->templateLocation, 'group' => $this->templateName );
}
/**
* Hook Data
* htmlspecialchars cannot have string as null, so this wrapper makes it safe to avoid
* loads of errors
*
* @return array
* @param string|null $string $string
* @param int $flags
* @param string|null $encoding
* @param bool $double_encode
* @return string
*/
public static function hookData()
public static function htmlspecialchars( null|string $string, int $flags = ENT_QUOTES|ENT_SUBSTITUTE, ?string $encoding = null, bool $double_encode = true): string
{
return array();
return htmlspecialchars( $string === null ? '' : $string, $flags, $encoding, $double_encode );
}
}
+26 -19
View File
@@ -11,44 +11,50 @@
namespace IPS\Theme;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Db;
use IPS\Theme;
use RuntimeException;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Template Exception Class
*/
class _TemplateException extends \RuntimeException
class TemplateException extends RuntimeException
{
/**
* @brief Template data
*/
public $template = array( 'location' => NULL, 'group' => NULL, 'app' => NULL );
public array $template = array( 'location' => NULL, 'group' => NULL, 'app' => NULL );
/**
* @brief Theme
*/
public $theme = NULL;
public ?Theme $theme = NULL;
/**
* Constructor
*
* @param string $message MySQL Error message
* @param int $code MySQL Error Code
* @param \Exception|NULL $previous Previous Exception
* @param array $template Template data (app, group, location)
* @param \IPS\Theme $theme Theme object
* @return void
* @param string|null $message MySQL Error message
* @param int $code MySQL Error Code
* @param Exception|NULL $previous Previous Exception
* @param array|null $template Template data (app, group, location)
* @param Theme|null $theme Theme object
*/
public function __construct( $message = null, $code = 0, $previous = null, $template=NULL, $theme=NULL )
public function __construct( ?string $message = null, int $code = 0, ?Exception $previous = null, ?array $template=NULL, ?Theme $theme=NULL )
{
/* Store these for the extraLogData() method */
$this->template = $template;
$this->theme = $theme;
return parent::__construct( $message, $code, $previous );
parent::__construct( $message, $code, $previous );
}
/**
@@ -56,18 +62,19 @@ class _TemplateException extends \RuntimeException
*
* @return bool
*/
public function isThirdPartyError()
public function isThirdPartyError() : bool
{
/* Try to see if the template group has any modifications...if so, ignore the exception for reporting purposes */
/* Look for a custom template inserted via hookpoint */
try
{
return (bool) \IPS\Db::i()->select( 'COUNT(*)', 'core_theme_templates', array( 'template_set_id=? AND template_group=? AND template_location=? AND template_app=?', $this->theme->id, $this->template['group'], $this->template['location'], $this->template['app'] ) )->first();
return (bool) Db::i()->select( 'count(*)', 'core_theme_templates_custom', [
[ 'template_set_id=?', $this->theme->id ],
[ "template_hook_point LIKE CONCAT(?,'%')", ( $this->template['app'] . '/' . $this->template['location'] . '/' . $this->template['group'] . '/' ) ]
])->first();
}
catch( \Exception $e )
catch( Exception $e )
{
return FALSE;
}
return FALSE;
}
}
+2104 -3051
View File
File diff suppressed because it is too large. Load diff