Version 5.0.0 beta 2

This commit is contained in:
Neo committed 2025-12-19 16:39:01 -08:00
1 parent 15c7beabc5
commit f61b0da85d
181 files changed
+3182 -1533

No files matched your search

+5
View File
@@ -15,6 +15,7 @@ interface Browsers {
const BROWSER = 'Browser';
const BUNJALLOO = 'Bunjalloo';
const CAMINO = 'Camino';
const CHATGPT_USER = 'ChatGPT-User';
const CHROME = 'Chrome';
const CURL = 'curl';
const EDGE = 'Edge';
@@ -24,17 +25,20 @@ interface Browsers {
const GOOGLEBOT = 'Googlebot';
const GOOGLEBOT_IMAGE = 'Googlebot-Image';
const GOOGLEBOT_VIDEO = 'Googlebot-Video';
const GPTBOT = 'GPTBot';
const HEADLESSCHROME = 'HeadlessChrome';
const IEMOBILE = 'IEMobile';
const IMESSAGEBOT = 'iMessageBot';
const KINDLE = 'Kindle';
const LYNX = 'Lynx';
const MASTODON = 'Mastodon';
const MIDORI = 'Midori';
const MIUIBROWSER = 'MiuiBrowser';
const MSIE = 'MSIE';
const MSNBOT_MEDIA = 'msnbot-media';
const NETFRONT = 'NetFront';
const NINTENDOBROWSER = 'NintendoBrowser';
const OAI_SEARCHBOT = 'OAI-SearchBot';
const OCULUSBROWSER = 'OculusBrowser';
const OPERA = 'Opera';
const PUFFIN = 'Puffin';
@@ -42,6 +46,7 @@ interface Browsers {
const SAILFISHBROWSER = 'SailfishBrowser';
const SAMSUNGBROWSER = 'SamsungBrowser';
const SILK = 'Silk';
const SLACKBOT = 'Slackbot';
const TELEGRAMBOT = 'TelegramBot';
const TIZENBROWSER = 'TizenBrowser';
const TWITTERBOT = 'Twitterbot';
+21 -15
View File
@@ -90,7 +90,7 @@ REGEX
} elseif( $platform == 'Adr' ) {
$platform = 'Android';
} elseif( $platform === null ) {
if(preg_match_all('%(?P<platform>Android)[:/ ]%ix', $u_agent, $result)) {
if( preg_match_all('%(?P<platform>Android)[:/ ]%ix', $u_agent, $result) ) {
$platform = $result[PLATFORM][0];
}
}
@@ -98,18 +98,19 @@ REGEX
preg_match_all(<<<'REGEX'
%(?P<browser>Camino|Kindle(\ Fire)?|Firefox|Iceweasel|IceCat|Safari|MSIE|Trident|AppleWebKit|
TizenBrowser|(?:Headless)?Chrome|YaBrowser|Vivaldi|IEMobile|Opera|OPR|Silk|Midori|(?-i:Edge)|EdgA?|CriOS|UCBrowser|Puffin|
OculusBrowser|SamsungBrowser|SailfishBrowser|XiaoMi/MiuiBrowser|
Baiduspider|Applebot|Facebot|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|
Valve\ Steam\ Tenfoot|
OculusBrowser|SamsungBrowser|SailfishBrowser|XiaoMi/MiuiBrowser|YaApp_Android|
Baiduspider|Applebot|Facebot|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|ChatGPT-User|GPTBot|OAI-SearchBot|
Valve\ Steam\ Tenfoot|Mastodon|
NintendoBrowser|PLAYSTATION\ (?:\d|Vita)+)
\)?;?
(?:[:/ ](?P<version>[0-9A-Z.]+)|/[A-Z]*)%ix
(?:[:/ ](?P<version>[0-9A-Z.]+)|/[A-Z]*)
%ix
REGEX
, $u_agent, $result);
// If nothing matched, return null (to avoid undefined index errors)
if( !isset($result[BROWSER][0], $result[BROWSER_VERSION][0]) ) {
if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)(/(?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)([/ :](?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
return [ PLATFORM => $platform ?: null, BROWSER => $result[BROWSER], BROWSER_VERSION => empty($result[BROWSER_VERSION]) ? null : $result[BROWSER_VERSION] ];
}
@@ -125,7 +126,7 @@ REGEX
$lowerBrowser = array_map('strtolower', $result[BROWSER]);
$find = function ( $search, &$key = null, &$value = null ) use ( $lowerBrowser ) {
$find = function( $search, &$key = null, &$value = null ) use ( $lowerBrowser ) {
$search = (array)$search;
foreach( $search as $val ) {
@@ -141,7 +142,7 @@ REGEX
return false;
};
$findT = function ( array $search, &$key = null, &$value = null ) use ( $find ) {
$findT = function( array $search, &$key = null, &$value = null ) use ( $find ) {
$value2 = null;
if( $find(array_keys($search), $key, $value2) ) {
$value = $search[$value2];
@@ -154,7 +155,7 @@ REGEX
$key = 0;
$val = '';
if( $findT([ 'OPR' => 'Opera', 'Facebot' => 'iMessageBot', 'UCBrowser' => 'UC Browser', 'YaBrowser' => 'Yandex', 'Iceweasel' => 'Firefox', 'Icecat' => 'Firefox', 'CriOS' => 'Chrome', 'Edg' => 'Edge', 'EdgA' => 'Edge', 'XiaoMi/MiuiBrowser' => 'MiuiBrowser' ], $key, $browser) ) {
if( $findT([ 'OPR' => 'Opera', 'Facebot' => 'iMessageBot', 'UCBrowser' => 'UC Browser', 'YaBrowser' => 'Yandex', 'YaApp_Android' => 'Yandex', 'Iceweasel' => 'Firefox', 'Icecat' => 'Firefox', 'CriOS' => 'Chrome', 'Edg' => 'Edge', 'EdgA' => 'Edge', 'XiaoMi/MiuiBrowser' => 'MiuiBrowser' ], $key, $browser) ) {
$version = is_numeric(substr($result[BROWSER_VERSION][$key], 0, 1)) ? $result[BROWSER_VERSION][$key] : null;
} elseif( $find('Playstation Vita', $key, $platform) ) {
$platform = 'PlayStation Vita';
@@ -168,7 +169,7 @@ REGEX
} elseif( $find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS' ) {
$browser = 'NintendoBrowser';
$version = $result[BROWSER_VERSION][$key];
} elseif( $find('Kindle', $key, $platform) ) {
} elseif( $find([ 'Kindle' ], $key, $platform) ) {
$browser = $result[BROWSER][$key];
$version = $result[BROWSER_VERSION][$key];
} elseif( $find('Opera', $key, $browser) ) {
@@ -187,7 +188,7 @@ REGEX
}
}
}
} elseif( $find([ 'Applebot', 'IEMobile', 'Edge', 'Midori', 'Vivaldi', 'OculusBrowser', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome', 'HeadlessChrome', 'SailfishBrowser' ], $key, $browser) ) {
} elseif( $find([ 'Googlebot', 'Applebot', 'IEMobile', 'Edge', 'Midori', 'Vivaldi', 'OculusBrowser', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome', 'HeadlessChrome', 'SailfishBrowser' ], $key, $browser) ) {
$version = $result[BROWSER_VERSION][$key];
} elseif( $rv_result && $find('Trident') ) {
$browser = 'MSIE';
@@ -200,12 +201,17 @@ REGEX
$platform = 'BlackBerry';
} elseif( $platform == 'BlackBerry' || $platform == 'PlayBook' ) {
$browser = 'BlackBerry Browser';
} else {
$find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser);
} elseif( $find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser) ) {
$version = $result[BROWSER_VERSION][$key];
} elseif( count($result[BROWSER]) ) {
$key = count($result[BROWSER]) - 1;
$browser = $result[BROWSER][$key];
$version = $result[BROWSER_VERSION][$key];
}
$find('Version', $key);
$version = $result[BROWSER_VERSION][$key];
if( $find('Version', $key) ) {
$version = $result[BROWSER_VERSION][$key];
}
} elseif( $pKey = preg_grep('/playstation \d/i', $result[BROWSER]) ) {
$pKey = reset($pKey);
+5 -4
View File
@@ -25,6 +25,7 @@ use IPS\core\FrontNavigation;
use IPS\Data\Cache;
use IPS\Data\Store;
use IPS\Db\Exception;
use Exception as PHPException;
use IPS\Helpers\Form;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\YesNo;
@@ -2607,7 +2608,7 @@ EOF;
Db::i()->update( 'core_theme_editor_categories', $category, [ 'cat_key=?', $key ] );
}
Lang::saveCustom( 'core', 'themeeditor_cat_' . $categoryId, $name );
Lang::saveCustom( 'core', 'themeeditor_cat_' . $key, $name, false, $name );
$category['cat_id'] = $categoryId;
$currentCategories[ $key ] = $category;
@@ -2678,10 +2679,10 @@ EOF;
], [ 'setting_key=?', $key ] );
}
Lang::saveCustom( 'core', 'themeeditor_var_' . $settingId, $setting['name'] );
Lang::saveCustom( 'core', 'themeeditor_var_' . $key, $setting['name'], false, $setting['name'] );
if( isset( $setting['desc'] ) )
{
Lang::saveCustom( 'core', 'themeeditor_var_' . $settingId . '_desc', $setting['desc'] );
Lang::saveCustom( 'core', 'themeeditor_var_' . $key . '_desc', $setting['desc'], false, $setting['desc'] );
}
}
}
@@ -5277,7 +5278,7 @@ EOF;
$block->layouts = $block->getSupportedLayouts();
$blocks[ $widget['key'] ] = $block;
}
catch( Exception $e )
catch( PHPException $e )
{
continue;
}
-1
View File
@@ -566,7 +566,6 @@ class Controller extends CoverPhotoController
$showing = intval( Request::i()->showing ?: 0 );
foreach ( $newComments as $newComment )
{
$output[] = Output::i()->replaceEmojiWithImages( $newComment->html() );
$lastId = ( $newComment->$commentIdColumn > $lastId ) ? $newComment->$commentIdColumn : $lastId;
if ( ++$showing >= $perPage )
{
+3 -3
View File
@@ -4525,7 +4525,7 @@ abstract class Item extends Content
}
else
{
Output::i()->json( array( 'type' => 'merge', 'id' => $lastComment->$commentIdColumn, 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => Output::i()->replaceEmojiWithImages( $newContent ) ) );
Output::i()->json( array( 'type' => 'merge', 'id' => $lastComment->$commentIdColumn, 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => $newContent ) );
}
}
else
@@ -4557,7 +4557,7 @@ abstract class Item extends Content
if ( $currentPageCount != $newPageCount )
{
Output::i()->json( array( 'type' => 'redirect', 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => Output::i()->replaceEmojiWithImages( $comment->html() ), 'url' => (string) $comment->url('find') ) );
Output::i()->json( array( 'type' => 'redirect', 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => $comment->html(), 'url' => (string) $comment->url('find') ) );
}
else
{
@@ -4596,7 +4596,7 @@ abstract class Item extends Content
'id' => $comment->$commentIdColumn,
'page' => $newPageCount,
'total' => $this->mapped('num_comments'),
'content' => Output::i()->replaceEmojiWithImages( $output ),
'content' => $output,
'message' => $message,
'postedByLoggedInMember' => true,
/* This is used on the front end */
+3 -2
View File
@@ -390,13 +390,14 @@ trait Reactable
else
{
/* Set the data in $this->reputation to save queries later */
$this->reputation = array();
$_reputation = array();
foreach( Db::i()->select( '*', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
{
$this->reputation[ $reaction['member_id'] ] = $reaction['reaction'];
$_reputation[ $reaction['member_id'] ] = $reaction['reaction'];
$this->_reactions[ $reaction['member_id'] ][] = $reaction['reaction'];
$this->_reactionCount += $reaction['rep_rating'];
}
$this->reputation = $_reputation;
}
}
-11
View File
@@ -39,17 +39,6 @@ abstract class Result
*/
public static function preDisplay( string $content ): string
{
/* We cannot use images (or *anything* other than text) in search results as it messes with the truncation
logic. All HTML (including emoticons) are already stripped before they go into the search index
for this reason, but Emoji (because they're text) won't be. We can't swap the Emoji back out
with one of the alternative Emoji style images though because that re-introduces images, so we
just strip them out (making them behave like emoticons) */
if ( Settings::i()->emoji_style == 'twemoji' )
{
$content = preg_replace( '/(?:' . Parser::getEmojiRegex() . '|\x{200d})+/u', '', $content );
}
/* We need to strip the image labels as returned by the AI image scanner */
return preg_replace('#<i\s+?l="([^<]+?)</i>#', '', $content);
}
+2 -2
View File
@@ -269,7 +269,7 @@ class Select implements Iterator, Countable
/**
* @brief Key Field
*/
protected ?string $keyField = NULL;
protected mixed $keyField = NULL;
/**
* @brief Key Table
@@ -279,7 +279,7 @@ class Select implements Iterator, Countable
/**
* @brief Value Field
*/
protected ?string $valueField = NULL;
protected mixed $valueField = NULL;
/**
* @brief Value Table
+6 -2
View File
@@ -108,8 +108,6 @@ abstract class Standard extends Dispatcher
Output::i()->jsVars['ipb_url_filter_option'] = Settings::i()->ipb_url_filter_option;
Output::i()->jsVars['url_filter_any_action'] = Settings::i()->url_filter_any_action;
Output::i()->jsVars['bypass_profanity'] = intval( Member::loggedIn()->group['g_bypass_badwords'] );
Output::i()->jsVars['emoji_style'] = ( Settings::i()->getFromConfGlobal('sql_utf8mb4') === TRUE ) ? Settings::i()->emoji_style : 'disabled';
Output::i()->jsVars['emoji_shortcodes'] = (bool) Settings::i()->emoji_shortcodes;
Output::i()->jsVars['emoji_cache'] = (int) Settings::i()->emoji_cache;
Output::i()->jsVars['image_jpg_quality'] = (int) Settings::i()->image_jpg_quality ?: 85;
Output::i()->jsVars['cloud2'] = (bool) CIC2;
@@ -327,6 +325,11 @@ abstract class Standard extends Dispatcher
*/
public bool $destruct = true;
/**
* @brief Flag to check if we are in the destructor
*/
public bool $inDestructor = false;
/**
* Destructor
* Runs tasks
@@ -343,6 +346,7 @@ abstract class Standard extends Dispatcher
if ( $this->runTasks and Settings::i()->task_use_cron == 'normal' and !Request::i()->isAjax() )
{
$this->inDestructor = true;
try
{
$task = Task::queued();
+1 -1
View File
@@ -444,7 +444,7 @@ class Amazon extends File
) );
/* Turn on task */
Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 's3_delete' ) );
Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 's3Delete' ) );
/* Log deletion request */
$realContainer = $this->container;
+6 -1
View File
@@ -51,6 +51,11 @@ abstract class File
* @var string|null
*/
public ?string $contextInfo;
/**
* @var mixed|null
*/
public mixed $screenshot = null;
/**
* Get available storage handlers
@@ -402,7 +407,7 @@ abstract class File
* @throws DomainException
* @throws RuntimeException
*/
public static function createFromUploads( string $storageLocation, ?string $fieldName=NULL, ?array $allowedFileTypes=NULL, int|float $maxFileSize=NULL, ?int $totalMaxSize=NULL, int $flags=0, array|callable|null $callback=NULL, ?string $container=NULL, bool $obscure=TRUE ) : array
public static function createFromUploads( string $storageLocation, ?string $fieldName=NULL, ?array $allowedFileTypes=NULL, int|float $maxFileSize=NULL, int|float $totalMaxSize=NULL, int $flags=0, array|callable|null $callback=NULL, ?string $container=NULL, bool $obscure=TRUE ) : array
{
/* Do we have any uploads? */
if( empty( $_FILES ) )
-3
View File
@@ -289,9 +289,6 @@ class Editor extends FormAbstract
/* Clean resources in ACP */
$value = $this->value;
Output::i()->parseFileObjectUrls( $value );
/* Fix Emoji */
$value = Output::i()->replaceEmojiWithImages( $value );
// Todo we can probably can be deleted. The <br> is now saved, and we also have CSS that makes the empty <p></p> tags hidden. Needs Ehren's signoff :)
/* The editor will replace <p></p> (which doesn't display anything in a normal post) with <p><br></p> (which does)
+7 -3
View File
@@ -5516,7 +5516,8 @@ class Member extends ActiveRecord
{
if ( $this->member_id )
{
if ( $by === NULL and Dispatcher::hasInstance() )
/* Set this only if this was not called by the task system, otherwise we'll set the member who triggered the task as moderator */
if ( Dispatcher::hasInstance() and !Dispatcher::i()->inDestructor and $by === NULL )
{
$by = Session::i()->member; // Not \IPS\Member::loggedIn() because if this is an admin logged in as a member, we want to log that the action was done by the admin
}
@@ -6652,9 +6653,12 @@ class Member extends ActiveRecord
/* PMs are treated extra */
Db::i()->update( 'core_message_posts', array( 'msg_ip_address' => '' ), array( "msg_ip_address != '' AND msg_date <= " . $time ) );
foreach ( Application::allExtensions( 'core', 'pruneIpAddresses', FALSE, 'core' ) as $key => $extension )
foreach ( Application::allExtensions( 'core', 'IpAddresses', FALSE, 'core' ) as $key => $extension )
{
$extension->pruneIpAddresses( $time );
if( method_exists( $extension, 'pruneIpAddresses' ) )
{
$extension->pruneIpAddresses( $time );
}
}
}
+10 -5
View File
@@ -2313,7 +2313,12 @@ abstract class Model extends ActiveRecord
{
if( static::$_allPermissions === null )
{
static::$_allPermissions = iterator_to_array(
static::$_allPermissions = [];
}
if( !isset( static::$_allPermissions[ get_called_class() ] ) )
{
static::$_allPermissions[ get_called_class() ] = iterator_to_array(
Db::i()->select( array( 'perm_type_id', 'perm_id', 'perm_view', 'perm_2', 'perm_3', 'perm_4', 'perm_5', 'perm_6', 'perm_7' ), 'core_permission_index', array( "app=? AND perm_type=?", static::$permApp, static::$permType ) )
->setKeyField( 'perm_type_id' )
);
@@ -2331,11 +2336,11 @@ abstract class Model extends ActiveRecord
{
if ( $this->_permissions === NULL )
{
if( !isset( static::permissionStore()[ $this->_id ] ) )
if( !isset( static::permissionStore()[ get_called_class() ][ $this->_id ] ) )
{
try
{
static::$_allPermissions[ $this->_id ] = Db::i()->select( array( 'perm_type_id', 'perm_id', 'perm_view', 'perm_2', 'perm_3', 'perm_4', 'perm_5', 'perm_6', 'perm_7' ), 'core_permission_index', array( "app=? AND perm_type=? and perm_type_id=?", static::$permApp, static::$permType, $this->_id ) )->first();
static::$_allPermissions[ get_called_class() ][ $this->_id ] = Db::i()->select( array( 'perm_type_id', 'perm_id', 'perm_view', 'perm_2', 'perm_3', 'perm_4', 'perm_5', 'perm_6', 'perm_7' ), 'core_permission_index', array( "app=? AND perm_type=? and perm_type_id=?", static::$permApp, static::$permType, $this->_id ) )->first();
}
catch( UnderflowException )
{
@@ -2346,11 +2351,11 @@ abstract class Model extends ActiveRecord
'perm_view' => ''
) );
static::$_allPermissions[ $this->_id ] = array( 'perm_id' => $permId, 'perm_view' => '', 'perm_2' => NULL, 'perm_3' => NULL, 'perm_4' => NULL, 'perm_5' => NULL, 'perm_6' => NULL, 'perm_7' => NULL );
static::$_allPermissions[ get_called_class() ][ $this->_id ] = array( 'perm_id' => $permId, 'perm_view' => '', 'perm_2' => NULL, 'perm_3' => NULL, 'perm_4' => NULL, 'perm_5' => NULL, 'perm_6' => NULL, 'perm_7' => NULL );
}
}
$this->_permissions = static::permissionStore()[ $this->_id ];
$this->_permissions = static::permissionStore()[ get_called_class() ][ $this->_id ];
}
return $this->_permissions;
-67
View File
@@ -945,12 +945,6 @@ class Output
$this->parseFileObjectUrls( $output );
}
/* Replace emoji */
if ( $output and $parseEmoji and Dispatcher::hasInstance() and Dispatcher::i()->controllerLocation !== 'setup' )
{
$output = $this->replaceEmojiWithImages( $output );
}
/* Combine some common checks for re-use */
$canCache = function( $contentType, $obj ) {
if( $obj->pageCaching and
@@ -1355,68 +1349,7 @@ class Output
/* ___base_url___ is a bit dramatic but it prevents accidental replacements with tags called base_url if a third party app or hook uses it */
$output = str_replace( '<___base_url___>', rtrim( Settings::i()->base_url, '/' ), $output );
}
/**
* Replace emoji unicode with images
*
* @param string $output The output containing emojis as unicode
* @return string
*/
public function replaceEmojiWithImages( string $output ): string
{
if ( Settings::i()->emoji_style == 'twemoji' )
{
return preg_replace_callback( '/<span class="ipsEmoji">(.+?)<\/span>/', function( $matches ) {
$hex = bin2hex( mb_convert_encoding( $matches[1], 'UTF-32', 'UTF-8' ) );
$hexLength = strlen( $hex ) / 8;
$chunks = array();
for ( $i = 0; $i < $hexLength; ++$i )
{
$tmp = substr( $hex, $i * 8, 8 );
$copy = false;
$len = strlen( $tmp );
$res = '';
for ( $j = 0; $j < $len; ++$j )
{
$ch = $tmp[ $j ];
if ( !$copy )
{
if ( $ch != '0' )
{
$copy = true;
}
else if ( ( $i + 1 ) == $len )
{
$res = '0';
}
}
if ( $copy )
{
$res .= $ch;
}
}
$chunks[ $i ] = $res;
}
$image = implode( '-', $chunks );
if ( strstr( $image, '200d' ) === FALSE or $image === '1f441-fe0f-200d-1f5e8-fe0f' )
{
$image = str_replace( '-fe0f', '', $image );
}
if ( in_array( $image, array( '0031-20e3', '0030-20e3', '0032-20e3', '0034-20e3', '0035-20e3', '0036-20e3', '0037-20e3', '0038-20e3', '0033-20e3', '0039-20e3', '0023-20e3', '002a-20e3', '00a9', '00ae' ) ) )
{
$image = str_replace( '00', '', $image );
}
return '<img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/' . $image . '.png" class="ipsEmoji" alt="' . $matches[1] . '" loading="lazy">';
}, $output );
}
return $output;
}
/**
* Show Offline
*
+11 -3
View File
@@ -138,7 +138,7 @@ class CustomTemplate extends Model
*/
public static function all(): ActiveRecordIterator
{
return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, NULL, static::$databasePrefix . static::$databaseColumnOrder ), 'IPS\Theme\CustomTemplate' );
return new ActiveRecordIterator( Db::i()->select( '*', static::$databaseTable, Db::i()->in( 'template_app', array_keys( Application::enabledApplications() ) ), static::$databasePrefix . static::$databaseColumnOrder ), 'IPS\Theme\CustomTemplate' );
}
/**
@@ -272,7 +272,12 @@ class CustomTemplate extends Model
*/
public static function pathToArray( string $path ): array
{
[ $app, $location, $group, $templateName ] = explode( '/', $path );
$bits = explode( "/", $path );
$app = array_shift( $bits );
$location = array_shift( $bits );
$group = array_shift( $bits );
$templateName = implode( "_", $bits );
//[ $app, $location, $group, $templateName ] = explode( '/', $path );
return [
'app' => $app,
@@ -576,9 +581,12 @@ EOF;
if ( $this->hookpoint and $this->hookpoint_type )
{
[ $path, $hook ] = explode( ':', $this->hookpoint );
$pathData = CustomTemplate::pathToArray( $path );
$badge = array(
0 => 'style2',
1 => $this->hookpoint . ' ' . $this->hookpoint_type
1 => implode( "/", $pathData ) . ':' . $hook . ' ' . $this->hookpoint_type
);
}
else if ( $this->key )
+35 -1
View File
@@ -24,6 +24,7 @@ use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Translatable;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Member;
use IPS\Node\Model;
use IPS\Request;
use IPS\Settings;
@@ -98,6 +99,39 @@ class Category extends Model
*/
public static ?string $subnodeClass = Setting::class;
/**
* [Node] Get Title
*
* @return string
*/
protected function get__title(): string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key, NULL, array( 'escape' => TRUE ) );
}
/**
* 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 $language->addToStack( static::$titleLangPrefix . $this->key, NULL, $options );
}
/**
* [Node] Get Title language key, not added to a language stack
*
* @return string|null
*/
protected function get__titleLanguageKey(): ?string
{
return static::$titleLangPrefix . $this->key;
}
/**
* [Node] Get Icon for tree
*
@@ -365,7 +399,7 @@ class Category extends Model
$return['cat_app'] = $values['themeeditor_cat_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->id, $values['themeeditor_cat_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key, $values['themeeditor_cat_name'] );
return $return;
}
+45 -2
View File
@@ -127,6 +127,49 @@ class Setting extends Model
*/
protected static bool $loadFromCache = true;
/**
* [Node] Get Title
*
* @return string
*/
protected function get__title(): string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key, NULL, array( 'escape' => TRUE ) );
}
/**
* 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 $language->addToStack( static::$titleLangPrefix . $this->key, NULL, $options );
}
/**
* [Node] Get Title language key, not added to a language stack
*
* @return string|null
*/
protected function get__titleLanguageKey(): ?string
{
return static::$titleLangPrefix . $this->key;
}
/**
* [Node] Get content table description
*
* @return string|null
*/
protected function get_description(): ?string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key . static::$descriptionLangSuffix );
}
/**
* Attempt to load cached data
*
@@ -600,8 +643,8 @@ class Setting extends Model
$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'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key, $values['themeeditor_setting_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $this->key . '_desc', $values['themeeditor_setting_desc'] );
return $return;
}
+15
View File
@@ -656,6 +656,21 @@ class Area
}
}
/* Make sure that the widget actually exists, otherwise ignore it.
This can be an issue if a widget was added and now no longer exists in the system. */
try
{
if( !Widget::isValidWidget( Application::load( $block['app'] ), $block['key'] ) )
{
return $this;
}
}
catch( Exception )
{
return $this;
}
/* Load configuration */
if ( empty( $block['configuration'] ) or !is_array( $block['configuration'] ) )
{
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* @brief Polymorphic
* @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 Oct 2024
*/
namespace IPS\Widget;
/* To prevent PHP errors (extending class does not exist) revealing path */
use function defined;
use function header;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
interface Polymorphic
{
/**
* Get the base key. When the class name is dynamically generated, we need to use the base key to look in the database and find the actual base configuration
*
* @return string
*/
public static function getBaseKey() : string;
}
+46 -3
View File
@@ -494,7 +494,7 @@ abstract class Widget
*/
public function getSupportedLayouts() : array
{
if ( !isset( static::$baseWidgetConfig[ $this::class ]['layouts'] ) OR empty( static::$baseWidgetConfig[ $this::class ]['layouts'] ) )
if ( !isset( static::$baseWidgetConfig[ $this::class ]['layouts'] ) OR !is_array( static::$baseWidgetConfig[ $this::class ]['layouts'] ) )
{
$layouts = ['table'];
if ( $this->isCustomizableWidget() )
@@ -718,6 +718,17 @@ abstract class Widget
}
$class = '\IPS\\' . $parent->directory . '\widgets\\' . $widgetKey;
$baseWidgetKey = $widgetKey;
if ( class_exists( $class ) and in_array( "IPS\\Widget\\Polymorphic", class_implements( $class ) ) )
{
$baseWidgetKey = $class::getBaseKey();
}
/* Is this a valid widget? */
if( !static::isValidWidget( $parent, $baseWidgetKey ) )
{
throw new OutOfRangeException;
}
/* Return */
if ( class_exists( $class ) )
@@ -732,6 +743,37 @@ abstract class Widget
throw new OutOfRangeException;
}
/**
* Make sure that the widget actually is supported by the app
*
* @param Application|string $app
* @param string $widgetKey
* @return bool
*/
public static function isValidWidget( Application|string $app, string $widgetKey ) : bool
{
/* Is this a valid widget? */
try
{
$cache = Store::i()->widgets;
}
catch( OutOfRangeException )
{
static::appDefaults( $app );
$cache = Store::i()->widgets;
}
$app = ( $app instanceof Application ) ? $app->directory : $app;
$class = '\IPS\\' . $app . '\widgets\\' . $widgetKey;
if ( class_exists( $class ) and in_array( "IPS\\Widget\\Polymorphic", class_implements( $class ) ) )
{
$widgetKey = $class::getBaseKey();
}
return array_key_exists( $widgetKey, $cache[ $app ] );
}
/**
* Widget Types
@@ -1183,11 +1225,12 @@ abstract class Widget
/**
* Get default widgets for an application
*
* @param Application $app The application
* @param Application|string $app The application
* @return array
*/
public static function appDefaults( Application $app ): array
public static function appDefaults( Application|string $app ): array
{
$app = is_string( $app ) ? Application::load( $app ) : $app;
if ( !isset( Store::i()->widgets ) )
{
$widgets = array();