Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
@@ -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}/";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Whitespace-only changes.
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user