Files
2025-12-19 18:37:58 -08:00

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;
}
}