513 lines
11 KiB
PHP
513 lines
11 KiB
PHP
<?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\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] 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 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->name;
|
|
}
|
|
|
|
/**
|
|
* [Node] Get Title language key, not added to a language stack
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function get__titleLanguageKey(): ?string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
/**
|
|
* [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 Text( 'themeeditor_cat_name', $this->name, true ) );
|
|
|
|
$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
|
|
{
|
|
$icon = $values['themeeditor_cat_icon'][0];
|
|
if( $values['themeeditor_cat_icon_extra'] )
|
|
{
|
|
$icon['extra'] = $values['themeeditor_cat_icon_extra'];
|
|
}
|
|
|
|
$return = [
|
|
'cat_name' => $values['themeeditor_cat_name'],
|
|
'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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |