Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+171 -100
View File
@@ -80,7 +80,7 @@ class _Theme extends \IPS\Node\Model
/**
* @brief IN_DEV "theme"
*/
protected static $inDevTheme = NULL;
public static $inDevTheme = NULL;
/**
* @brief Setup "theme"
@@ -88,7 +88,7 @@ class _Theme extends \IPS\Node\Model
protected static $setupSkin = NULL;
/**
* @brief Setup "theme"
* @brief Admin "theme"
*/
public static $adminSkin = NULL;
@@ -179,6 +179,11 @@ class _Theme extends \IPS\Node\Model
* @brief Bit option for theme settings
*/
const THEME_ID_KEY = 2;
/**
* @brief Bit option for theme settings
*/
const TEMPLATE_SETTINGS_DEFAULT = 4;
/**
* @brief Disable the copy button - useful when the forms are very distinctly different
@@ -477,6 +482,7 @@ class _Theme extends \IPS\Node\Model
static::themes();
$default = static::$multitons[ static::$defaultFrontendThemeSet ];
$master = static::constructFromData( array_merge( static::$themeSetRelationships[ $default->parent_id ][ $default->id ], array( 'set_id' => 0 ) ) );
$master->_data['settings'] = $default->getThemeSettings( static::TEMPLATE_SETTINGS_DEFAULT | static::THEME_KEY_VALUE_PAIRS );
unset( static::$multitons[ 0 ] );
@@ -525,7 +531,7 @@ class _Theme extends \IPS\Node\Model
/* Make sure the member theme is set */
\IPS\Theme::i();
$class = \get_class( static::$memberTheme );
$class = static::i();
static::$memberTheme = $class::load( $themeId );
/* Store the ID in sessions so ajax loads correct theme */
@@ -872,6 +878,23 @@ class _Theme extends \IPS\Node\Model
return static::$calledTemplates[ $hashedKey ];
}
/**
* Determine the path to load the file from
*
* @param string $file File path
* @return string
*/
public static function getHookPath( $file )
{
$path = \IPS\ROOT_PATH;
if ( \IPS\CIC2 AND \IPS\IPS::isThirdParty( $file ) )
{
$path = \IPS\SITE_FILES_PATH;
}
return $path;
}
/**
* Eval hooks into life
@@ -886,7 +909,9 @@ class _Theme extends \IPS\Node\Model
{
foreach ( \IPS\IPS::$hooks[ "\\IPS\\Theme\\{$class}" ] as $id => $data )
{
if ( file_exists( \IPS\ROOT_PATH . '/' . $data['file'] ) )
$path = static::getHookPath( $data['file'] );
if ( file_exists( $path . '/' . $data['file'] ) )
{
if ( class_exists( "{$namespace}\\" . $data['class'], FALSE ) )
{
@@ -894,7 +919,7 @@ class _Theme extends \IPS\Node\Model
continue;
}
$contents = "namespace {$namespace};\n\n" . str_replace( '_HOOK_CLASS_', $class, file_get_contents( \IPS\ROOT_PATH . '/' . $data['file'] ) );
$contents = "namespace {$namespace};\n\n" . str_replace( '_HOOK_CLASS_', $class, file_get_contents( $path . '/' . $data['file'] ) );
try
{
@@ -949,13 +974,27 @@ class _Theme extends \IPS\Node\Model
if ( ! \is_array( $obj->_data['resource_map'] ) )
{
if ( $imgMap = json_decode( $obj->_data['resource_map'], true ) )
if( $obj->id > 0 )
{
$obj->_data['resource_map'] = $imgMap;
if ( $imgMap = json_decode( $obj->_data['resource_map'], true ) )
{
$obj->_data['resource_map'] = $imgMap;
}
else
{
$obj->_data['resource_map'] = array();
}
}
else
{
$obj->_data['resource_map'] = array();
if( !isset( \IPS\Data\Store::i()->acp_resource_map ) )
{
$obj->_data['resource_map'] = [];
}
else
{
$obj->_data['resource_map'] = \IPS\Data\Store::i()->acp_resource_map;
}
}
}
@@ -991,6 +1030,11 @@ class _Theme extends \IPS\Node\Model
if ( \is_array( $value ) )
{
$this->_data['resource_map'] = json_encode( $value );
if ( $this->id === 0 )
{
\IPS\Data\Store::i()->acp_resource_map = $value;
}
}
}
@@ -1058,33 +1102,6 @@ class _Theme extends \IPS\Node\Model
/*! Node */
/**
* Get sharer logo
*
* @deprecated
* @note This method will be removed in a future version
* @return \IPS\Http\Url|string
*/
public function get_logo_sharer()
{
/* While we now allow multiple share logos now, this deprecated method can only return one */
$shareLogos = \IPS\Settings::i()->icons_sharer_logo ? json_decode( \IPS\Settings::i()->icons_sharer_logo, true ) : array();
if( \count( $shareLogos ) )
{
try
{
return \IPS\File::get( 'core_Icons', $shareLogos[0] )->url->setScheme( ( \IPS\Request::i()->isSecure() ) ? 'https' : 'http' );
}
catch( \Exception $e )
{
return '';
}
}
return '';
}
/**
* Get header logo
*
@@ -1095,30 +1112,6 @@ class _Theme extends \IPS\Node\Model
return $this->logoImage( 'front' );
}
/**
* Get favicon logo
*
* @return \IPS\Http\Url|string
* @deprecated
* @note This method will be removed in a future version
*/
public function get_logo_favicon()
{
if( \IPS\Settings::i()->icons_favicon )
{
try
{
return \IPS\File::get( 'core_Icons', \IPS\Settings::i()->icons_favicon )->url;
}
catch( \Exception $e )
{
return '';
}
}
return '';
}
/**
* Return a string of CSS var definitions
*
@@ -1556,11 +1549,11 @@ class _Theme extends \IPS\Node\Model
}
} ) );
$form->add( new \IPS\Helpers\Form\Select(
$form->add( new \IPS\Helpers\Form\CheckboxSet(
'core_theme_set_permissions',
( $this->id ) ? ( $this->permissions === '*' ? '*' : explode( ",", $this->permissions ) ) : '*',
FALSE,
array( 'options' => \IPS\Member\Group::groups(), 'multiple' => TRUE, 'parse' => 'normal', 'unlimited' => '*', 'unlimitedLang' => 'all' ),
array( 'options' => \IPS\Member\Group::groups(), 'multiple' => TRUE, 'parse' => 'normal', 'unlimited' => '*', 'unlimitedLang' => 'all', 'impliedUnlimited' => TRUE ),
NULL,
NULL,
NULL,
@@ -2059,7 +2052,7 @@ class _Theme extends \IPS\Node\Model
try
{
$image = \IPS\Image::create( \IPS\File::get( 'core_Theme', $url )->contents() );
$this->_data['logo']['front'] = array( 'url' => $url, 'width' => $image->width, 'height' => $image->height );
$this->_data['logo']['front'] = array( 'url' => $url, 'height' => 100 );
}
catch ( \RuntimeException $e )
{
@@ -2204,7 +2197,8 @@ class _Theme extends \IPS\Node\Model
$this->is_default = $values['core_theme_set_is_default'];
$this->permissions = ( $values['core_theme_set_permissions'] === '*' ) ? '*' : implode( ',', $values['core_theme_set_permissions'] );
$this->parent_id = ( $values['core_theme_parent_id'] instanceof \IPS\Theme ) ? $values['core_theme_parent_id']->id : 0;
$this->css_updated = time();
$this->save();
if ( $creating === TRUE AND $this->id )
@@ -2349,7 +2343,7 @@ class _Theme extends \IPS\Node\Model
$image = \IPS\Image::create( $original->contents() );
$newImage = \IPS\File::create( 'core_Theme', $original->originalFilename, $original->contents() );
$logos[$file] = array( 'url' => (string) $newImage, 'width' => $image->width, 'height' => $image->height );
$logos[$file] = array( 'url' => (string) $newImage, 'height' => $data['height'] );
}
catch ( \Exception $e ) { }
}
@@ -2373,13 +2367,15 @@ class _Theme extends \IPS\Node\Model
{
foreach( \IPS\Application::applications() as $appKey => $data )
{
$path = static::getHookPath( "applications/{$appKey}/data/themesettings.json" );
/* Root skin? Add default theme settings */
if ( ! file_exists( \IPS\ROOT_PATH . "/applications/{$appKey}/data/themesettings.json" ) )
if ( ! file_exists( $path . "/applications/{$appKey}/data/themesettings.json" ) )
{
continue;
}
$json = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$appKey}/data/themesettings.json" ), true );
$json = json_decode( file_get_contents( $path . "/applications/{$appKey}/data/themesettings.json" ), true );
/* Add */
foreach( $json as $key => $data)
@@ -2606,12 +2602,14 @@ class _Theme extends \IPS\Node\Model
{
if ( !class_exists( "IPS\\Theme\\" . $data['class'] . "_tmp", FALSE ) )
{
$path = static::getHookPath( $data['file'] );
/* Like code hooks, we should only attempt to load the files contents if it actually exists */
if ( file_exists( \IPS\ROOT_PATH . '/' . $data['file'] ) )
if ( file_exists( $path . '/' . $data['file'] ) )
{
try
{
if ( eval( "namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp', 'array()' ), file_get_contents( \IPS\ROOT_PATH . '/' . $data['file'] ) ) ) !== FALSE )
if ( eval( "namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp', 'array()' ), file_get_contents( $path . '/' . $data['file'] ) ) ) !== FALSE )
{
$class = "IPS\\Theme\\" . $data['class'] . "_tmp";
$templateHooks = array_merge_recursive( $templateHooks, $class::hookData() );
@@ -3101,14 +3099,21 @@ EOF;
public function getThemeSettings( $flags=0 )
{
$settings = array();
$rows = \IPS\Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( 'sc.sc_set_id=?', $this->id ) )
->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' );
foreach ( $rows as $row )
{
$row['_value'] = ( array_key_exists( 'sv_value', $row ) AND $row['sv_value'] !== NULL ) ? $row['sv_value'] : $row['sc_default'];
if ( $flags & static::TEMPLATE_SETTINGS_DEFAULT )
{
$row['_value'] = $row['sc_default'];
}
else
{
$row['_value'] = ( array_key_exists( 'sv_value', $row ) AND $row['sv_value'] !== NULL ) ? $row['sv_value'] : $row['sc_default'];
}
if ( $row['_value'] and $row['sc_type'] === 'Upload' )
{
try
@@ -3134,9 +3139,70 @@ EOF;
$settings[ $row['sc_key'] ] = $row;
}
}
return $settings;
}
/**
* Ensure children get new theme settings
*
* @return void
*/
public function updateChildrenThemeSettings()
{
$mainRows = iterator_to_array( \IPS\Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( 'sc.sc_set_id=?', $this->id ) )
->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' ) );
$mainRowsByKey = [];
foreach( $mainRows as $row )
{
$mainRowsByKey[ $row['sc_key'] ] = $row;
}
foreach( $this->children() as $child )
{
$childRows = iterator_to_array( \IPS\Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( 'sc.sc_set_id=?', $child->id ) )
->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' ) );
$rowsByKey = [];
foreach( $childRows as $row )
{
$rowsByKey[ $row['sc_key'] ] = $row;
}
$diff = array_diff( array_keys( $mainRowsByKey ), array_keys( $rowsByKey ) );
if ( \count( $diff ) )
{
foreach( $diff as $keyName )
{
$save = [];
foreach( $mainRowsByKey[ $keyName ] as $k => $v )
{
if ( \mb_substr( $k, 0, 3 ) == 'sc_' and $k != 'sc_id' )
{
$save[ $k ] = $v;
}
}
$save['sc_set_id'] = $child->id;
$save['sc_order'] = \IPS\Db::i()->select( 'MAX(sc_order)', 'core_theme_settings_fields', array( 'sc_set_id=? AND sc_tab_key=?', $child->id, $save['sc_tab_key'] ) )->first() + 1;
$id = \IPS\Db::i()->insert( 'core_theme_settings_fields', $save );
$themeSettings = $child->getThemeSettings();
$json = array();
foreach( $themeSettings as $settingId => $row )
{
$json[ $row['sc_key'] ] = $row['_value'];
}
$child->template_settings = json_encode( $json );
$child->save();
}
}
}
}
/**
* Get raw CSS. Raw means {resource..} tags and uncompiled
@@ -3658,33 +3724,17 @@ EOF;
}
$results['templates'] = iterator_to_array( \IPS\Db::i()->select(
'*, MD5( CONCAT( template_app, ".", template_location, ".", template_group, ".", template_name ) ) as bit_key',
'*, MD5( CONCAT( template_app, \'.\', template_location, \'.\', template_group, \'.\', template_name ) ) as bit_key',
'core_theme_templates',
array( 'template_set_id=?', $this->id )
)->setKeyField('bit_key') );
$results['css'] = iterator_to_array( \IPS\Db::i()->select(
'*, MD5( CONCAT( css_app, ".", css_location, ".", css_path, ".", css_name ) ) as bit_key',
'*, MD5( CONCAT( css_app, \'.\', css_location, \'.\', css_path, \'.\', css_name ) ) as bit_key',
'core_theme_css',
array( 'css_set_id=?', $this->id )
)->setKeyField('bit_key') );
/*header( "Content-type: text/plain");
foreach( $results['templates'] as $key => $row )
{
print $row['bit_key'] . ' ' . $row['template_name'];
if ( isset( $history['template'][ $key ] ) )
{
print ' = ' . $history['template'][ $key ]['content_name'];
}
print "\n";
}
exit();*/
/* Find changed and new template bits */
foreach( $results['templates'] as $key => $data )
{
@@ -3935,6 +3985,7 @@ EOF;
\IPS\File::getClass( 'core_Theme')->deleteContainer('css_built_' . $set->_id );
$set->css_map = array();
$set->css_updated = time();
$set->save();
}
}
@@ -4032,6 +4083,7 @@ EOF;
/* Update mappings */
$set->css_map = $map;
$set->css_updated = time();
$set->save();
}
}
@@ -4150,8 +4202,9 @@ EOF;
if ( empty( $themeId ) )
{
/* ACP CSS */
/* ACP CSS and Resources */
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => '[]' ) );
unset( \IPS\Data\Store::i()->acp_resource_map );
}
}
@@ -4172,7 +4225,7 @@ EOF;
$pseudoSelectors = array(
'active', 'checked', 'disabled', 'empty', 'enabled', 'first-child', 'first-of-type', 'focus', 'hover', 'in-range', 'invalid', 'lang', 'last-child',
'last-of-type', 'link', 'not', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'only-of-type', 'only-child', 'optional', 'out-of-range', 'read-only', 'read-write', 'requuired',
'last-of-type', 'link', 'eq', 'not', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'only-of-type', 'only-child', 'optional', 'out-of-range', 'read-only', 'read-write', 'required',
'root', 'target', 'valid', 'visited',
'after', 'before', 'first-letter', 'first-line', 'selection'
);
@@ -4607,7 +4660,9 @@ PHP;
{
foreach ( \IPS\Application::applications() as $app )
{
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/extensions/core/OutputPlugins/" . mb_ucfirst( $plugin ) . ".php" ) )
$path = static::getHookPath( "applications/{$app->directory}/extensions/core/OutputPlugins/" . mb_ucfirst( $plugin ) . ".php" );
if ( file_exists( $path . "/applications/{$app->directory}/extensions/core/OutputPlugins/" . mb_ucfirst( $plugin ) . ".php" ) )
{
$pluginClass = 'IPS\\' . $app->directory . '\\extensions\\core\\OutputPlugins\\' . mb_ucfirst( $plugin );
}
@@ -4777,8 +4832,9 @@ PHP;
$theme->save();
}
/* ACP CSS */
/* ACP CSS and Resources */
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => '[]' ) );
unset( \IPS\Data\Store::i()->acp_resource_map );
}
/**
@@ -4799,7 +4855,7 @@ PHP;
throw new \InvalidArgumentException;
}
$name = static::makeBuiltTemplateLookupHash( $data['app'], $data['location'], $data['path'] ) . '_' . $data['name'];
$name = static::makeBuiltTemplateLookupHash( $data['app'], $data['location'], $data['path'] ) . '_' . $data['name'];
if ( $addToMaster )
{
@@ -4840,7 +4896,15 @@ PHP;
if ( $data['content'] )
{
$fileName = (string) \IPS\File::create( 'core_Theme', $name, $data['content'], 'set_resources_' . $theme->id, FALSE, NULL, FALSE );
$saveName = $name;
if ( $resource !== NULL and $resource['resource_data'] != $data['content'] )
{
/* This resource exists, so let us create a unique name to prevent cache issues */
$ext = mb_substr( $name, ( mb_strrpos( $name, '.' ) + 1 ) );
$saveName = mb_substr( $name, 0, ( mb_strrpos( $name, '.' ) ) ) . '_' . mt_rand() . '.' . $ext;
}
$fileName = (string) \IPS\File::create( 'core_Theme', $saveName, $data['content'], 'set_resources_' . $theme->id, FALSE, NULL, FALSE );
\IPS\Db::i()->insert( 'core_theme_resources', array(
'resource_set_id' => $theme->id,
@@ -4934,11 +4998,17 @@ PHP;
if ( ! empty( $data['set_id'] ) )
{
/* This is a specific theme set */
static::setThemeCustomized( $data['set_id'] );
}
else
{
/* Update all themes to let them know that a new master CSS has been added */
\IPS\Db::i()->update( 'core_themes', array( 'set_css_updated' => time() ) );
}
// create the plugin template
if( \IPS\IN_DEV AND $data['app'] === 'core' AND $data['location'] === 'custom' AND $data['path'] === 'front' AND isset( $data['plugin'] ) )
if( \IPS\IN_DEV AND $data['app'] === 'core' AND $data['location'] === 'front' AND $data['path'] === 'custom' AND isset( $data['plugin'] ) )
{
$plugin = \IPS\Plugin::load( $data['plugin'] );
@@ -5010,9 +5080,9 @@ PHP;
/* Insert */
$insertId = \IPS\Db::i()->insert( 'core_theme_templates', array(
'template_set_id' => ( isset( $data['set_id'] ) ) ? $data['set_id'] : 0,
'template_app' => mb_strtolower( $data['app'] ),
'template_location' => mb_strtolower( $data['location'] ),
'template_group' => mb_strtolower( $data['group'] ),
'template_app' => $data['app'],
'template_location' => $data['location'],
'template_group' => $data['group'],
'template_name' => $data['name'],
'template_data' => $data['variables'],
'template_content' => $data['content'],
@@ -5327,6 +5397,7 @@ PHP;
{
\IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $child->id );
$child->css_map = array();
$child->css_updated = time();
$child->save();
}
}