639 lines
18 KiB
PHP
639 lines
18 KiB
PHP
<?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\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] 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;
|
|
|
|
/**
|
|
* [Node] Get Title
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get__title(): string
|
|
{
|
|
return $this->name ?? '';
|
|
}
|
|
|
|
/**
|
|
* Get the title for a node using the specified language object
|
|
* This is commonly used where we cannot use the logged in member's language, such as sending emails
|
|
*
|
|
* @param Lang $language Language object to fetch the title with
|
|
* @param array $options What options to use for language parsing
|
|
* @return string
|
|
*/
|
|
public function getTitleForLanguage( Lang $language, array $options=array() ): string
|
|
{
|
|
return $this->_title;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Title language key, not added to a language stack
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function get__titleLanguageKey(): ?string
|
|
{
|
|
return $this->_title;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get content table description
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function get_description(): ?string
|
|
{
|
|
return $this->desc;
|
|
}
|
|
|
|
/**
|
|
* 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 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'] ) and is_numeric( $this->_data['default'] ) ) ? $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() );
|
|
}
|
|
|
|
$themesToCheck = [ Theme::i()->id, Theme::$defaultFrontendThemeSet ];
|
|
|
|
/* If we are working with a specific theme, make sure we allow it */
|
|
if( isset( Request::i()->set_id ) )
|
|
{
|
|
$themesToCheck[] = Request::i()->set_id;
|
|
}
|
|
|
|
$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, $themesToCheck ) )
|
|
{
|
|
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_TEXT:
|
|
$value = strip_tags( $value );
|
|
$value = str_replace( array( '/*', ';' ), '', $value );
|
|
$value = htmlentities( $value );
|
|
break;
|
|
|
|
case static::SETTING_CHECKBOX:
|
|
$value = (int) $value;
|
|
break;
|
|
|
|
case static::SETTING_IMAGE:
|
|
/* Handle Resource URLs */
|
|
preg_match( '/\{([a-z]+?=([\'"]).+?\\2 ?+)}/', $value, $matches );
|
|
if( count( $matches ) )
|
|
{
|
|
preg_match_all( '/(.+?)="([^"]*)"\s?/', $matches[1], $submatches );
|
|
if( count( $submatches[0] ) and $submatches[1][0] == 'resource' )
|
|
{
|
|
$resourceName = array_shift( $submatches[2] );
|
|
array_shift( $submatches[1] );
|
|
$options = array();
|
|
foreach ( $submatches[1] as $k => $v )
|
|
{
|
|
$options[ $v ] = $submatches[2][ $k ];
|
|
}
|
|
|
|
$value = Theme::i()->resource( $resourceName, $options['app'] ?? 'core', $options['location'] ?? 'front' )->url;
|
|
}
|
|
}
|
|
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 Text( 'themeeditor_setting_name', $this->name, true ) );
|
|
$form->add( new Text( 'themeeditor_setting_desc', $this->desc, false ) );
|
|
$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(
|
|
'decimals' => 2
|
|
), null, null, null, 'themeeditor_setting_min' ) );
|
|
$form->add( new Number( 'themeeditor_setting_max', $this->data['max'] ?? null, null, array(
|
|
'decimals' => 2
|
|
), 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_name' => $values['themeeditor_setting_name'],
|
|
'setting_desc' => $values['themeeditor_setting_desc'],
|
|
'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;
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
} |