Files
IPB/system/Widget/Widget.php
T
2025-12-19 19:04:57 -08:00

1760 lines
51 KiB
PHP

<?php
/**
* @brief Sidebar Widget Class
* @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
* @since 15 Nov 2013
*/
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DomainException;
use ErrorException;
use Exception;
use IPS\cms\Widget as CmsWidget;
use IPS\Data\Store;
use IPS\Helpers\Form;
use IPS\Helpers\Form\CheckboxSet;
use IPS\Helpers\Form\Codemirror;
use IPS\Helpers\Form\Color;
use IPS\Helpers\Form\Number;
use IPS\Helpers\Form\Radio;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Trbl;
use IPS\Helpers\Form\Upload;
use IPS\Helpers\Form\YesNo;
use IPS\Helpers\Table\Custom;
use IPS\Http\Url;
use IPS\Member\Club;
use IPS\Platform\Bridge;
use IPS\Widget\Area;
use IPS\Widget\Builder;
use IPS\Widget\Customizable;
use IPS\Widget\Polymorphic;
use OutOfRangeException;
use Throwable;
use UnderflowException;
use function count;
use function defined;
use function file_put_contents;
use function func_get_args;
use function get_class;
use function in_array;
use function is_array;
use function is_countable;
use function is_numeric;
use function is_string;
use function json_decode;
use function mb_substr;
use function strlen;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Sidebar Widget Class
*/
abstract class Widget
{
/**
* @brief The number of widgets that can be expired per request (to prevent loads of rebuilds on a single request which would slow the page down). Deliberately hardcoded.
*/
protected static int $expirePerRequest = 1;
/**
* @brief Configuration
*/
public array $configuration = array();
/**
* @brief Access. Array of allowed apps that execute the widgets. Null for no restriction
*/
protected mixed $access = null;
/**
* @brief Custom template callback
*/
public mixed $template = null;
/**
* @brief Orientation
*/
protected ?string $orientation = null;
/**
* @brief Layout currently in use, calculated by the widget area
*/
public string $currentLayout = '';
/**
* @var string
*/
public string $defaultLayout = '';
/**
* @brief Menu style
*/
public string $menuStyle = 'menu';
/**
* @var array
*/
public array $layouts = array();
/**
* @brief Allow block to be reused
*/
public bool $allowReuse = false;
/**
* @brief Show the padding controls in the page builder
*/
public bool $allowCustomPadding = false;
/**
* @brief Show the no wrap option in the page builder
*/
public bool $allowNoBox = false;
/**
* @brief Unique key for this widget
*/
public string|int|null $uniqueKey = NULL;
/**
* @brief Prevent caching for this block
*/
public bool $neverCache = FALSE;
/**
* @brief Error language string key shown after the configuration
*/
public string $errorMessage = 'widget_blank_or_no_context';
/**
* @brief Set to true if this widget must be the only one in its area
*/
public bool $soloWidget = false;
/**
* @brief Set to false if this widget should be hidden from the block list
* in the Page Editor
*/
public static bool $showInBlockList = true;
/**
* @brief If a widget should not be dropped into a particular area (e.g. a database widget in the header), list those areas here
*/
public static array $disallowedAreas = [];
/**
* These layouts are the default allowed "feed" layouts
*
* @var string[] $defaultFeedLayouts
*/
public static array $defaultFeedLayouts = array(
"featured",
"featured-carousel",
"grid",
"grid-carousel",
"minimal",
'minimal-carousel',
"mini-grid",
"mini-grid-carousel",
"table",
"table-carousel",
"wallpaper",
"wallpaper-carousel"
);
/**
* Constructor
*
* @param String $uniqueKey Unique key for this specific instance
* @param array $configuration Widget custom configuration
* @param array|string|null $access Array/JSON string of executable apps (core=sidebar only, content=IP.Content only, etc)
* @param string|null $orientation Horizontal or vertical orientation
* @param string $layout Current layout in use
* @return void
*/
public function __construct( string $uniqueKey, array $configuration, array|string $access=null, string $orientation=null, string $layout='' )
{
$this->configuration = $configuration;
$this->orientation = $orientation;
if( empty( $layout ) )
{
$layout = $this->defaultLayout ?? 'table';
}
$this->currentLayout = $layout;
if ( $access !== null and is_string( $access ) )
{
$test = json_decode( $access, true );
if ( is_array( $test ) AND count( $test ) )
{
$this->access = $test;
}
}
else if ( is_array( $access ) AND count( $access ) )
{
$this->access = $access;
}
$this->init();
$this->uniqueKey = ( empty( $uniqueKey ) ) ? ( $this->key ?: mt_rand() ) : $uniqueKey;
if ( !$this->hasConfiguration() )
{
$this->errorMessage = 'widget_blank_or_no_context_no_config';
}
}
/**
* Initialise this widget
*
* @return void
* @throws ErrorException
*/
public function init(): void
{
if ( $this->app and $this->template === null )
{
$this->template( array( Theme::i()->getTemplate( 'widgets', $this->app, 'front' ), $this->key ) );
}
}
/**
* Is this a block builder widget?
*
* @return bool
*/
public function isBuilderWidget() : bool
{
return in_array( Builder::class, class_implements( $this ) );
}
/**
* Can this widget be customized?
* @return bool
*/
public function isCustomizableWidget() : bool
{
return in_array( Customizable::class, class_implements( $this ) );
}
/**
* Constructor
*
* @param array|string $app Application key (core,cms,gallery, etc)
* @return bool
*/
public function isExecutableByApp( array|string $app ): bool
{
if ( ! Bridge::i()->pagesAllowDatabaseAccess() )
{
$databaseWidgets = [ 'Categories', 'Database', 'DatabaseFilters', 'RecordFeed' ];
if ( $this->app === 'cms' and in_array( $this->key, $databaseWidgets ) )
{
return false;
}
}
if ( $this->access === null or ( is_array( $this->access ) and ! count($this->access ) ) )
{
return true;
}
else
{
if ( is_string( $app ) )
{
$checkApps = array( $app );
}
else
{
$checkApps = $app;
}
foreach( $checkApps as $check )
{
if ( in_array( $check, $this->access ) )
{
return true;
}
}
}
return false;
}
/**
* Can this widget be used on this page?
*
* @param string $app
* @param string $module
* @param string $controller
* @return bool
*/
public function isExecutableByPage( string $app, string $module, string $controller ) : bool
{
return true;
}
/**
* Fetch the application for this widget
*
* @return Application
*/
public function application(): Application
{
return Application::load( $this->app );
}
/**
* Fetch the title for this widget
*
* @return string
*/
public function title(): string
{
return Member::loggedIn()->language()->addToStack( 'block_' . $this->key );
}
/**
* Fetch the description for this widget
*
* @return string
*/
public function description(): string
{
return Member::loggedIn()->language()->addToStack( 'block_' . $this->key . '_desc' );
}
/**
* Set the template for this widget
*
* @param mixed $callback Function to use for template callback
* @return void
*/
public function template( mixed $callback ): void
{
$this->template = $callback;
}
/**
* Get Template Location
* Returns the template app/location/group/name params
* @return array
*/
public function getTemplateLocation(): array
{
$class = get_class( $this->template[0] );
if ( $class === 'IPS\Theme\Dev\Template' )
{
$params = $this->template[0]->getParams();
}
else
{
$params = array( 'app' => $this->template[0]->template->app, 'location' => $this->template[0]->template->templateLocation, 'group' => $this->template[0]->template->templateName );
}
return array_merge( $params, array( 'name' => $this->template[1] ) );
}
/**
* Return any extra classes that should be added to the widget wrapper
* Placeholder method in case an override is necessary for individual widgets.
*
* @return array
*/
public function getWrapperClasses() : array
{
return [];
}
/**
* Return all data attributes that will be placed on the widget container
* for block management
*
* @return array
*/
public function dataAttributes() : array
{
$return = [
'blocktitle' => $this->title(),
'blockID' => 'app_' . $this->app . '_' . $this->key . '_' . $this->uniqueKey,
'blockErrorMessage' => Member::loggedIn()->language()->addToStack( $this->errorMessage ),
'menuStyle' => $this->menuStyle ?? 'menu'
];
if( !empty( $this->allowReuse ) )
{
$return['allowReuse'] = true;
}
if( $this->hasConfiguration() )
{
$return['blockConfig'] = true;
}
if( $this->isBuilderWidget() )
{
$return['blockBuilder'] = true;
}
if( $this->soloWidget )
{
$return['widget-is-solo'] = true;
}
if( $this->isCustomizableWidget() )
{
$return['widget-customizable'] = true;
$return['widget-layouts'] = implode( ",", $this->layouts );
}
if ( !empty( $this->getSearchTerms() ) )
{
$return['searchterms'] = implode( ',', $this->getSearchTerms() );
}
if( $this->allowCustomPadding )
{
$return['widget-paddingallowed'] = true;
}
if( $this->allowNoBox )
{
$return['widget-noboxallowed'] = true;
}
if( count( static::$disallowedAreas ) )
{
$return['widget-disallowed-areas'] = implode( ",", static::$disallowedAreas );
}
return $return;
}
/**
* Get the current layout, or the first supported layout is not supported
*
* @return string
*/
public function getCurrentLayout() : string
{
if ( empty( $this->currentLayout ) or ( $this->isCustomizableWidget() and !in_array( $this->currentLayout, $this->getSupportedLayouts() ) ) )
{
$this->currentLayout = $this->defaultLayout ?: $this->getSupportedLayouts()[0];
}
return $this->currentLayout;
}
/** @var array */
protected static array $baseWidgetConfig = [];
protected static array $widgetConfigs = [];
/**
* Get config data from an app, caching all to avoid loads of queries for widget heavy pages
*
* @param string $app
* @param string $key
* @return array
*/
protected static function getWidgetDataByKeyAndApp( string $app, string $key ): array
{
if ( ! count( static::$widgetConfigs ) )
{
foreach( Db::i()->select( '*', 'core_widgets' ) as $widget )
{
static::$widgetConfigs[ $widget['app'] ][ $widget['key'] ] = $widget;
}
}
if ( isset( static::$widgetConfigs[ $app ][ $key ] ) )
{
return static::$widgetConfigs[ $app ][ $key ];
}
/* Still here? Oops */
throw new UnderflowException;
}
/**
* Get the search terms allowed for this widget
*
* @return string[]
*/
public function getSearchTerms() : array
{
if ( !isset( static::$baseWidgetConfig[ $this::class ]['searchterms'] ) )
{
if ( !isset( static::$baseWidgetConfig[ $this::class ]['searchterms'] ) )
{
$class = trim( $this::class, '\\' );
$components = explode( '\\', $class );
if ( @$components[0] === 'IPS' and @$components[1] and $components[1] === strtolower( $components[1] ) and @$components[2] === 'widgets' )
{
$app = $components[1];
$widget = $components[3];
try
{
static::$baseWidgetConfig[ $this::class ] = static::getWidgetDataByKeyAndApp( $app, $widget );
}
catch ( UnderflowException )
{
return [];
}
}
}
}
return is_string( @static::$baseWidgetConfig[ $this::class ][ 'searchterms' ] ) ? explode( ',', static::$baseWidgetConfig[ $this::class ][ 'searchterms' ] ) : [];
}
/**
* @return string[]
*/
public function getSupportedLayouts() : array
{
if ( !isset( static::$baseWidgetConfig[ $this::class ]['layouts'] ) OR !is_array( static::$baseWidgetConfig[ $this::class ]['layouts'] ) )
{
$layouts = ['table'];
if ( $this->isCustomizableWidget() )
{
$class = trim( $this::class, '\\' );
$components = explode( '\\', $class );
if ( @$components[0] === 'IPS' and @$components[1] and $components[1] === strtolower( $components[1] ) and @$components[2] === 'widgets' )
{
$app = $components[1];
$widget = $components[3];
try
{
$baseConfig = static::getWidgetDataByKeyAndApp( $app, $widget );
}
catch ( UnderflowException )
{
$baseConfig = [];
}
if ( !isset( $baseConfig['layouts'] ) or empty( $baseConfig['layouts'] ) )
{
$layouts = Widget::$defaultFeedLayouts;
}
elseif ( $baseConfig['layouts'] === '*' )
{
$layouts = Area::$allowedWrapBehaviors;
}
else
{
$layouts = array_intersect( Area::$allowedWrapBehaviors, explode( ',', $baseConfig['layouts'] ) );
}
if( isset( $baseConfig['default_layout'] ) and $baseConfig['default_layout'] )
{
array_unshift( $layouts, $baseConfig['default_layout'] );
}
/* Make sure base layouts all have a carousel equivalent */
foreach ( Area::$carouselToRegularLayouts as $carouselLayout => $regularLayouts )
{
if ( in_array( $carouselLayout, $layouts ) )
{
continue;
}
foreach ( $regularLayouts as $regularLayout )
{
if ( in_array( $regularLayout, $layouts ) )
{
$layouts[] = $carouselLayout;
continue 2;
}
}
}
static::$baseWidgetConfig[ $this::class ] = $baseConfig;
}
}
static::$baseWidgetConfig[ $this::class ]['layouts'] = array_values( $layouts );
}
return static::$baseWidgetConfig[ $this::class ]['layouts'];
}
/**
* Get HTML using the template (language strings not parsed)
*
* @return string
*/
public function output(): string
{
$currentLayout = $this->getCurrentLayout();
$args = func_get_args();
$args[] = str_replace( '-carousel', '', $currentLayout );
$args[] = str_ends_with( $currentLayout, "-carousel" );
$template = $this->template;
$output = $template( ...$args );
if( $this->isBuilderWidget() )
{
$config = array();
foreach( $this->configuration as $key => $value )
{
if ( mb_substr( $key, 0, 12 ) === 'widget_adv__' )
{
$config[ mb_substr( $key, 12 ) ] = $value;
}
}
$config['class'] = 'app_' . $this->app . '_' . $this->key . '_' . $this->uniqueKey;
$config['style'] = static::buildInlineStyles( $config );
return Theme::i()->getTemplate( 'widgets', 'core' )->builderWrapper( $output, $config );
}
else
{
return $output;
}
}
/**
* Build inline styles
*
* @param array $config
* @return array
*/
public static function buildInlineStyles( array $config ): array
{
$css = array();
if ( ! empty( $config['padding'] ) and $config['padding'] === 'custom' )
{
$css['padding'] = $config['padding_custom'][0] . 'px ' . $config['padding_custom'][1] . 'px ' . $config['padding_custom'][2] . 'px ' . $config['padding_custom'][3] . 'px';
}
if ( isset( $config['background_custom'] ) and $config['background_custom'] == 'image' and ! empty( $config['background_custom_image'] ) )
{
$css['background-image'] = 'url("' . File::get( 'core_Attachment', $config['background_custom_image'] )->url . '")';
$css['background-size'] = 'cover';
$css['background-repeat'] = 'no-repeat';
$css['background-position'] = 'center';
$css['background-color'] = 'transparent';
}
elseif ( isset( $config['background_custom'] ) and $config['background_custom'] == 'custom' and ! empty( $config['background'] ) )
{
$css['background-color'] = $config['background'];
}
if ( ! empty( $config['fontcolor'] ) )
{
if ( isset( $config['fontcolor_custom'] ) and $config['fontcolor_custom'] == 'custom' )
{
$css['color'] = $config['fontcolor'];
}
}
if ( ! empty( $config['fontsize'] ) )
{
if ( $config['fontsize'] === 'custom' )
{
$css['font-size'] = $config['fontsize_custom'] . 'px';
}
}
if ( ! empty( $config['font'] ) and $config['font'] !== 'inherit' )
{
$fontWeight = 400;
if ( mb_substr( $config['font'], -6 ) === ' black' )
{
$fontWeight = 900;
$config['font'] = mb_substr( $config['font'], 0, -6 );
}
$css['font-family'] = '"' . $config['font'] . '"';
$css['font-weight'] = $fontWeight;
}
if ( ! empty( $config['fontalign'] ) )
{
$css['text-align'] = $config['fontalign'];
}
$return = array();
foreach( $css as $name => $rule )
{
$return[ $name ] = $name . ":" . $rule . ";";
}
return $return;
}
/**
* Efficient way to see if a widget has configuration
*
* @return bool
*/
public function hasConfiguration(): bool
{
return method_exists( $this, 'configuration' );
}
/**
* Before the widget is removed, we can do some clean up
*
* @return void
*/
public function delete()
{
/* Does nothing by default but can be overridden */
}
/**
* Factory Method
*
* @param Application $parent Widget application
* @param String $widgetKey Widget key used to load class
* @param String $uniqueKey Unique key for this specific instance
* @param array $configuration Current configuration
* @param array|string|null $access Array/JSON string of executable apps (core=sidebar only, content=IP.Content only, etc)
* @param string|null $orientation Horizontal or vertical orientation
* @param string $layout Current layout in use
* @return Widget
* @throws OutOfRangeException
*/
public static function load( Application $parent, string $widgetKey, string $uniqueKey, array $configuration=array(), array|string $access=null, string $orientation=null, string $layout='' ): Widget
{
/* If our parent is not enabled, do not attempt to use this widget */
if ( !$parent->enabled )
{
throw new OutOfRangeException;
}
/* Is this a valid widget? */
if( !static::isValidWidget( $parent, $widgetKey ) )
{
throw new OutOfRangeException;
}
$class = '\IPS\\' . $parent->directory . '\widgets\\' . $widgetKey;
$baseWidgetKey = $widgetKey;
if ( class_exists( $class ) and in_array( "IPS\\Widget\\Polymorphic", class_implements( $class ) ) )
{
$baseWidgetKey = $class::getBaseKey();
}
/* Return */
if ( class_exists( $class ) )
{
if ( ! empty( $configuration['widget_adv__custom'] ) )
{
Output::i()->headCss = Output::i()->headCss . "\n" . $configuration['widget_adv__custom'];
}
return new $class( $uniqueKey, $configuration, $access, $orientation, $layout );
}
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
{
$app = ( $app instanceof Application ) ? $app : Application::load( $app );
$class = '\IPS\\' . $app->directory . '\widgets\\' . $widgetKey;
return ( in_array( $widgetKey, $app->getValidWidgetKeys() ) and class_exists( $class ) );
}
/**
* Widget Types
*
* @return array
*/
public static function widgetTypes(): array
{
return array(
'default' => '\IPS\Widget',
'StaticCache' => '\IPS\Widget\StaticCache',
'PermissionCache' => '\IPS\Widget\PermissionCache',
);
}
/**
* Dev Table
*
* @param string $json Path to JSON file
* @param Url $url URL to page
* @param string $widgetDirectory Directory where PHP files are stored
* @param string $subpackage The value to use for the subpackage in the widget file's header
* @param string $namespace The namespace for the widget file
* @param int|string $appKeyOrPluginId If widget belongs to an application, it's key, or if a plugin, it's ID
* @return string
*/
public static function devTable( string $json, Url $url, string $widgetDirectory, string $subpackage, string $namespace, int|string $appKeyOrPluginId ): string
{
if ( !file_exists( $json ) )
{
file_put_contents( $json, json_encode( array() ) );
}
switch ( Request::i()->widgetTable )
{
case 'form':
/* Build the list of layouts that are allowed on a widget */
$layouts = [];
foreach( Area::$widgetOnlyLayouts as $wrapBehavior )
{
if( !str_ends_with( $wrapBehavior, '-carousel' ) )
{
$layouts[ $wrapBehavior ] = 'core_pagebuilder_wrap__' . $wrapBehavior;
}
}
$current = NULL;
if ( isset( Request::i()->key ) )
{
$widgets = json_decode( file_get_contents( $json ), TRUE );
if ( array_key_exists( Request::i()->key, $widgets ) )
{
$current = array(
'dev_widget_key' => Request::i()->key,
'dev_widget_class' => $widgets[ Request::i()->key ]['class'],
'dev_widget_restrict' => $widgets[ Request::i()->key ]['restrict'],
'dev_widget_allow_reuse' => isset($widgets[ Request::i()->key ]['allow_reuse']) ? $widgets[ Request::i()->key ]['allow_reuse'] : 0,
'dev_widget_menu_style' => isset($widgets[ Request::i()->key ]['menu_style']) ? $widgets[ Request::i()->key ]['menu_style'] : 'menu',
'dev_widget_layouts' => $widgets[ Request::i()->key ]['layouts'] ?? implode( ",", $layouts ),
'dev_widget_searchterms' => is_string( @$widgets[ Request::i()->key ]['searchterms'] ) ? explode( ',', $widgets[ Request::i()->key ]['searchterms'] ) : [],
'dev_widget_embeddable' => isset($widgets[ Request::i()->key ]['embeddable']) ? $widgets[ Request::i()->key ]['embeddable'] : 1,
'dev_widget_allow_padding' => isset( $widgets[ Request::i()->key ]['padding'] ) ? $widgets[ Request::i()->key]['padding'] : 0,
'dev_widget_default_layout' => $widgets[ Request::i()->key ]['default_layout'] ?? null
);
}
unset( $widgets );
}
$form = new Form;
$form->add( new Text( 'dev_widget_key', $current ? $current['dev_widget_key'] : NULL, TRUE, array( 'maxLength' => 255, 'regex' => '/^[a-z][a-z0-9]*$/i' ), function( $val ) use ( $current )
{
if( mb_strpos( $val, "_" ) !== FALSE )
{
throw new DomainException( 'dev_widget_key_err_alpha' );
}
$where = array( array( '`key`=?', $val ) );
if ( isset( $current['dev_widget_key'] ) )
{
$where[] = array( '`key`<>?', $current['dev_widget_key'] );
}
if ( Db::i()->select( 'count(*)', 'core_widgets', $where )->first() )
{
throw new DomainException( 'dev_widget_key_err' );
}
} ) );
$classes = array();
foreach ( static::widgetTypes() as $key => $class )
{
$classes[ $class ] = $class;
Member::loggedIn()->language()->words[ $class . '_desc' ] = Member::loggedIn()->language()->get( 'widget_class_' . $key );
}
$form->add( new Radio( 'dev_widget_class', ( is_array( $current ) ? $current['dev_widget_class'] : NULL ), TRUE, array( 'options' => $classes ) ) );
$form->add( new CheckboxSet( 'dev_widget_restrict', ( is_array( $current ) and !empty( $current['dev_widget_restrict'] ) ) ? $current['dev_widget_restrict'] : array( 'sidebar', 'cms' ), FALSE, array(
'options' => array(
'sidebar' => Member::loggedIn()->language()->addToStack('dev_widget_restrict_sidebar'),
'cms' => Member::loggedIn()->language()->addToStack('dev_widget_restrict_cms'),
),
'multiple' => true ) ) );
$form->add( new Radio( 'dev_widget_menu_style', ( is_array( $current ) ? $current['dev_widget_menu_style'] : 'menu' ), FALSE, array(
'options' => array(
'menu' => Member::loggedIn()->language()->addToStack('dev_widget_menu_style_menu'),
'modal' => Member::loggedIn()->language()->addToStack('dev_widget_menu_style_modal'),
) ) ) );
$form->add( new Form\Stack( 'dev_widget_searchterms', $current['dev_widget_searchterms'] ?? null, false ) );
$form->add( new CheckboxSet( 'dev_widget_layouts', ( is_array( $current ) AND $current['dev_widget_layouts'] ) ? explode( ",", $current['dev_widget_layouts'] ) : $layouts, false, array(
'options' => $layouts,
'noDefault' => true,
'class' => 'widget_layouts__container'
) ) );
$form->add( new Select( 'dev_widget_default_layout', ( is_array( $current ) and $current['dev_widget_default_layout'] ) ? $current['dev_widget_default_layout'] : null, false, array(
'options' => array( '' => '' ) + $layouts,
'parse' => 'lang'
) ) );
$form->add( new YesNo( 'dev_widget_allow_reuse', ( is_array( $current ) ? $current['dev_widget_allow_reuse'] : 0 ) ) );
$form->add( new YesNo( 'dev_widget_allow_padding', ( is_array( $current ) ? $current['dev_widget_allow_padding'] : 0 ) ) );
/** @deprecated - This option will not be supported in a future version which is why it's hidden */
$form->add( new YesNo( 'dev_widget_embeddable', ( is_array( $current ) ? $current['dev_widget_embeddable'] : 0 ), options: ["rowClasses" => ["ipsHide"]] ) );
Member::loggedIn()->language()->words['dev_widget_embeddable'] = "(Deprecated) " . Member::loggedIn()->language()->get( "dev_widget_embeddable" );
if ( $values = $form->values() )
{
/* Write PHP file */
$widgetFile = $widgetDirectory . "/{$values['dev_widget_key']}.php";
if ( !file_exists( $widgetFile ) )
{
if ( !is_dir( $widgetDirectory ) )
{
mkdir( $widgetDirectory );
chmod( $widgetDirectory, IPS_FOLDER_PERMISSION);
}
file_put_contents( $widgetFile, str_replace(
array(
'{key}',
"{subpackage}\n",
'{date}',
'{namespace}',
'{class}',
'{appkey}',
),
array(
$values['dev_widget_key'],
( $subpackage != 'core' ) ? ( " * @subpackage\t" . $subpackage . "\n" ) : '',
date( 'd M Y' ),
$namespace,
$values['dev_widget_class'],
$appKeyOrPluginId
),
file_get_contents( ROOT_PATH . "/applications/core/data/defaults/Widget.txt" )
) );
chmod( $widgetFile, IPS_FILE_PERMISSION);
}
/* Figure out the layouts */
$values['dev_widget_layouts'] = implode( ",", $values['dev_widget_layouts'] );
$values['dev_widget_searchterms'] = implode( ",", $values['dev_widget_searchterms'] );
/* Add to DB */
$query = Db::i()->replace( 'core_widgets', array(
'app' => $appKeyOrPluginId,
'key' => $values['dev_widget_key'],
'class' => $values['dev_widget_class'],
'restrict' => ( ! count( $values['dev_widget_restrict'] ) ? FALSE : json_encode( array_values( $values['dev_widget_restrict'] ) ) ),
'allow_reuse' => $values['dev_widget_allow_reuse'],
'menu_style' => $values['dev_widget_menu_style'],
'layouts' => $values['dev_widget_layouts'],
'searchterms' => $values['dev_widget_searchterms'],
'embeddable' => $values['dev_widget_embeddable'],
'padding' => $values['dev_widget_allow_padding'],
'default_layout' => $values['dev_widget_default_layout']
) );
unset( Store::i()->widgets );
/* Add to JSON file */
$widgets = json_decode( file_get_contents( $json ), TRUE );
$widgets[ $values['dev_widget_key'] ] = array(
'class' => $values['dev_widget_class'],
'restrict' => ( ! count( $values['dev_widget_restrict'] ) ? FALSE : array_values( $values['dev_widget_restrict'] ) ),
'allow_reuse' => $values['dev_widget_allow_reuse'],
'menu_style' => $values['dev_widget_menu_style'],
'layouts' => $values['dev_widget_layouts'],
'embeddable' => $values['dev_widget_embeddable'],
'padding' => $values['dev_widget_allow_padding'],
'default_layout' => $values['dev_widget_default_layout'],
'searchterms' => $values['dev_widget_searchterms']
);
Application::writeJson( $json, $widgets );
/* Redirect */
Output::i()->redirect( $url, 'saved' );
}
return $form;
case 'delete':
Session::i()->csrfCheck();
$widgets = json_decode( file_get_contents( $json ), TRUE );
if ( array_key_exists( Request::i()->key, $widgets ) )
{
unset( $widgets[ Request::i()->key ] );
file_put_contents( $json, json_encode( $widgets, JSON_PRETTY_PRINT ) );
if ( file_exists( $widgetDirectory . "/" . Request::i()->key . ".php" ) )
{
unlink( $widgetDirectory . "/" . Request::i()->key . ".php" );
}
Db::i()->delete( 'core_widgets', array( 'app=? AND `key`=?', $appKeyOrPluginId, Request::i()->key ) );
unset( Store::i()->widgets );
}
Output::i()->redirect( $url, 'saved' );
default:
$data = array();
foreach ( json_decode( file_get_contents( $json ), TRUE ) as $k => $json )
{
$data[ $k ] = array(
'dev_widget_key' => $k,
'dev_widget_class' => $json['class'],
'dev_widget_restrict' => $json['restrict'] === FALSE ? Member::loggedIn()->language()->addToStack('dev_widget_nowhere') : ( ( ( count( $json['restrict'] ) > 0 and count( $json['restrict'] ) !== 2 ) ? implode( ',', array_map( function($val ) { return Member::loggedIn()->language()->addToStack('dev_widget_restrict_'.$val); }, $json['restrict'] ) ) : Member::loggedIn()->language()->addToStack('everywhere') ) )
);
}
$table = new Custom( $data, $url );
$table->quickSearch = 'dev_widget_key';
$table->rootButtons = array(
'add' => array(
'icon' => 'plus',
'title' => 'add',
'link' => $url->setQueryString( 'widgetTable', 'form' ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') )
)
);
$table->rowButtons = function( $row ) use ( $url, $widgetDirectory )
{
$buttons = [];
$filePath = $widgetDirectory . "/" . $row['dev_widget_key'] . ".php";
if( $ideLink = Developer::getIdeHref( $filePath ) )
{
$buttons['ide'] = [
'icon' => 'fa-file-code',
'title' => 'open_in_ide',
'link' => $ideLink
];
};
$buttons['edit'] = array(
'icon' => 'pencil',
'title' => 'edit',
'link' => $url->setQueryString( 'widgetTable', 'form' )->setQueryString( 'key', $row['dev_widget_key'] ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('edit') )
);
$buttons['delete'] = array(
'icon' => 'times-circle',
'title' => 'delete',
'link' => $url->setQueryString( 'widgetTable', 'delete' )->setQueryString( 'key', $row['dev_widget_key'] )->csrf(),
'data' => array( 'delete' => '' )
);
return $buttons;
};
return $table;
}
}
/**
* Get all cache keys (for all possible permissions, etc.)
*
* @param String|null $key Widget key
* @param String|null $app Parent application
* @return array
* @note This method does not take responsibility for checking if caches are expired
*/
public static function getCaches( string $key=NULL, string $app=NULL ): array
{
$caches = array();
try
{
foreach(Db::i()->select( '*', 'core_widgets', static::_buildWhere( $key, $app ) ) as $widget )
{
if( $widget['caches'] )
{
$json = json_decode( $widget['caches'], TRUE );
if ( ! is_array( $json ) )
{
return array();
}
foreach ( $json as $cKey => $time )
{
$caches[ $cKey ] = $time;
}
}
}
}
catch ( UnderflowException $e )
{
}
return $caches;
}
/**
* Delete caches
*
* @param String|null $key Widget key
* @param String|null $app Parent application
* @return void
*/
public static function deleteCaches( string $key=NULL, string $app=NULL ) : void
{
if( Application::appIsEnabled( 'cms' ) )
{
CmsWidget::deleteCachesForBlocks( $key, $app );
}
$caches = static::getCaches( $key, $app );
foreach ( $caches as $cacheKey => $time )
{
unset( Store::i()->$cacheKey );
}
Db::i()->update( 'core_widgets', array( 'caches' => NULL ), static::_buildWhere( $key, $app ) );
unset( Store::i()->widgets );
}
/**
* Store this widget instance as trash incase we need to fetch the configuration
* when another column is updated due to a widget being moved from one column to another.
*
* @param string $uniqueId Widget's Unique ID
* @param array $data Widget Data
* @return void
*/
public static function trash( string $uniqueId, array $data ) : void
{
Db::i()->delete( 'core_widget_trash', array( 'id=?', $uniqueId ) );
Db::i()->insert( 'core_widget_trash', array(
'id' => $uniqueId,
'data' => json_encode( $data ),
'date' => time()
) );
}
/**
* @var array
*/
protected static array $cachedConfiguration = [];
/**
* Get and cache all configs to prevent one query per widget
*
* @return array
*/
protected static function getAllConfigurations(): array
{
/* Try the json first */
foreach( Db::i()->select( '*', 'core_widget_areas' ) as $item )
{
$widgets = json_decode( $item['widgets'], TRUE );
foreach( $widgets as $widget )
{
static::$cachedConfiguration[ $widget['unique'] ] = $widget['configuration'];
}
}
/* Now try the inline data as this may be more accurate */
foreach( Db::i()->select( '*', 'core_widgets_config' ) as $widget )
{
static::$cachedConfiguration[ $widget['id'] ] = json_decode( $widget['data'], true );
}
return static::$cachedConfiguration;
}
/**
* Fetch the configuration for this unqiue ID. Looks in active tables and trash. When a widget is moved, saveOrder is called twice,
* once to remove the widget from column A and again to add it to column B. We store the widget removed from column A into the trash
* table.
*
* @param string $uniqueId Widget's unique ID
* @return array
*/
public static function getConfiguration( string $uniqueId ): array
{
if ( ! count( static::$cachedConfiguration ) )
{
static::getAllConfigurations();
}
/* Still here? rummage in the trash */
if( !isset( static::$cachedConfiguration[ $uniqueId ] ) )
{
try
{
$widget = Db::i()->select( '*', 'core_widget_trash', array( 'id=?', $uniqueId ) )->first();
$data = json_decode( $widget['data'], TRUE );
if ( isset( $data['configuration'] ) )
{
static::$cachedConfiguration[ $uniqueId ] = $data['configuration'];
}
}
catch( UnderflowException $ex )
{
static::$cachedConfiguration[ $uniqueId ] = [];
}
}
return static::$cachedConfiguration[ $uniqueId ] ?? array();
}
/**
* @brief Cached output to prevent rendering widget twice
*/
protected ?string $cachedOutput = NULL;
/**
* Return the widget output or an empty string if the widget shouldn't be returned on this page
*
* @return string
*/
protected function _render() : string
{
if( Settings::i()->clubs AND isset( $this->configuration['clubs_visibility'] ) )
{
switch ( $this->configuration['clubs_visibility'] )
{
case 'all':
return $this->render();
case 'without_clubs':
return ( Club::userIsInClub() ) ? '' : $this->render();
case 'only_clubs':
return ( Club::userIsInClub() ) ? $this->render() : '';
}
}
return $this->render();
}
/**
* Convert the widget to HTML
*
* @return string
*/
public function __toString()
{
/* Wrap the whole thing in a try/catch because exceptions in __toString confuses PHP */
try
{
/* Put the app check here as it needs to check the member's secondary groups but the PermissionCache only stores the primary group IDs */
if ( $this->app )
{
if ( ! $this->application()->canAccess( Member::loggedIn() ) )
{
return '';
}
}
/* If we're not caching (e.g. dynamic blocks in Pages), just return it */
if ( $this->neverCache === TRUE )
{
return static::parseOutput( $this->_render() );
}
/* Otherwise, figure out what to display. Saved in $this->cachedOutput so if this is being used twice on the same page, we only do this once */
if ( $this->cachedOutput === NULL )
{
/* Does this go in the store? Things like active users don't get stored, and if in developer or designer mode, nothing does */
if ( isset( $this->cacheKey ) AND ( !isset( Request::i()->cookie['vle_editor'] ) or !Request::i()->cookie['vle_editor'] ) AND !IN_DEV)
{
/* How long does the store last (in seconds)? */
$expiration = Settings::i()->widget_cache_ttl;
if ( isset( $this->cacheExpiration ) )
{
$expiration = $this->cacheExpiration;
}
/* If we have the TTL set to 0, don't bother with the store */
if ( $expiration )
{
/* Add/update in the store if it isn't there or it's expired */
$cacheKey = $this->cacheKey;
if ( !isset( Store::i()->$cacheKey ) or ( $widget = Store::i()->$cacheKey and $widget['built'] < ( time() - $expiration ) and static::$expirePerRequest-- ) )
{
/* The render() call below may take a long time to run for some widgets - we don't want lots of users to call
it simultaneously, so save a blank widget for now. For a second or two (until we've built and stored
the correct output which is done right after calling render) users will see nothing, which isn't ideal
but is better than killing the server */
Store::i()->$cacheKey = array( 'built' => time(), 'html' => '' );
/* Render and store */
$content = $this->_render();
Member::loggedIn()->language()->parseOutputForDisplay( $content );
Store::i()->$cacheKey = array( 'built' => time(), 'html' => $content ); // Corrects the blank output written above
/* Log that cache key so if we need to delete all the caches for this widget later we have it */
$caches = static::getCaches( $this->key, $this->app );
foreach( $caches as $key => $timeBuilt )
{
if ( $key === $cacheKey )
{
continue;
}
if ( $timeBuilt < ( time() - $expiration ) )
{
if ( isset( Store::i()->$key ) )
{
unset( Store::i()->$key );
}
unset( $caches[ $key ] );
}
}
$caches[ $cacheKey ] = time();
$keyForCacheStore = $this->key;
if( in_array( Polymorphic::class, class_implements( $this ) ) )
{
/* @var Polymorphic $widgetClass */
$widgetClass = get_class( $this );
$keyForCacheStore = $widgetClass::getBaseKey();
}
Db::i()->update( 'core_widgets', array( 'caches' => json_encode( $caches ) ), static::_buildWhere( $keyForCacheStore, $this->app ) );
}
/* Then use what the store has */
$widget = Store::i()->$cacheKey;
$this->cachedOutput = $widget['html'];
}
}
/* If we still don't have anything, go ahead and render */
if( $this->cachedOutput === NULL )
{
$this->cachedOutput = $this->_render();
}
}
/* And render */
return static::parseOutput( $this->cachedOutput );
}
catch ( Exception | Throwable $e )
{
IPS::exceptionHandler( $e );
}
return '';
}
/**
* Parse <time> tags to avoid caching with another's timezone
*
* @param string $output HTML code which may contain the tag
* @return string
*/
public static function parseOutput( string $output ): string
{
if ( mb_stristr( $output, '<time' ) )
{
$output = preg_replace_callback( '#<time([^>]+?)?>([^<]+?)</time>#i', function( $matches )
{
$time = NULL;
if ( is_numeric( $matches[2] ) and strlen( $matches[2] ) === 10 )
{
$time = $matches[2];
}
else if ( preg_match( '#\s?datetime=[\'"]([^\'"]+?)[\'"]#', $matches[1], $dateTimeString ) )
{
if ( $dateTimeString[1] )
{
$time = strtotime( $dateTimeString[1] );
}
}
if ( $time )
{
$options = array();
preg_match_all( '#(\S+?)=["\'](.+?)["\']\s?#', $matches[1], $submatches, PREG_SET_ORDER );
foreach( $submatches as $idx => $data )
{
$options[ str_replace( 'data-', '', $data[1] ) ] = $data[2];
}
$obj = DateTime::ts( $time );
$val = $obj->html();
if ( isset( $options['dateonly'] ) )
{
$val = $obj->localeDate();
}
else
{
if ( isset( $options['norelative'] ) )
{
$val = (string)$obj;
}
}
return $val;
}
}, $output );
}
return $output;
}
/**
* Empty the widget trash
*
* @param int $seconds Seconds old to remove
* @return void
*/
public static function emptyTrash( int $seconds=86400 ) : void
{
$uniqueIds = static::getUniqueIds();
foreach( Db::i()->select( '*', 'core_widget_trash', array( array( 'date < ?', time() - $seconds ) ) ) as $row )
{
$data = json_decode( $row['data'], TRUE );
if ( ! empty( $data['app'] ) and ! empty( $data['key'] ) and ! empty( $data['unique'] ) )
{
/* Is this unique ID actually used elsewhere? Sometimes moving blocks around can add a row in the trash table with the same unique ID */
if ( in_array( $data['unique'], $uniqueIds ) )
{
continue;
}
try
{
$widget = static::load( Application::load( $data['app'] ), $data['key'], $data['unique'], $data['configuration'] ?? NULL );
$widget->delete();
}
catch( Exception $ex ) { }
}
}
Db::i()->delete( 'core_widget_trash', array( 'date < ?', time() - $seconds ) );
}
/**
* Return an array of all areas that use this widget
*
* @param Application $app
* @param string $key
* @return array
*/
public static function usedWhere( Application $app, string $key ): array
{
$areas = [];
foreach ( Db::i()->select( '*', 'core_widget_areas' ) as $row )
{
$data = json_decode( $row['widgets'], TRUE );
if ( is_countable( $data ) AND count( $data ) )
{
foreach( $data as $widget )
{
if ( isset( $widget['unique'] ) and $widget['app'] === $app->directory and $widget['key'] === $key )
{
$areas[] = [
'id' => $row['id'],
'app' => $row['app'],
'module' => $row['module'],
'controller' => $row['controller'],
'area' => $row['area']
];
}
}
}
}
return $areas;
}
/**
* Return unique IDs in use
*
* @return array
*/
public static function getUniqueIds(): array
{
$uniqueIds = array();
foreach ( Db::i()->select( '*', 'core_widget_areas' ) as $row )
{
if( $row['widgets'] )
{
$data = json_decode( $row['widgets'], TRUE );
if ( is_countable( $data ) )
{
foreach( $data as $widget )
{
if ( isset( $widget['unique'] ) )
{
$uniqueIds[] = $widget['unique'];
}
}
}
}
if( $row['tree'] )
{
$area = new Area( json_decode( $row['tree'], true ), $row['area'] );
foreach( $area->getAllWidgets() as $widget )
{
if( isset( $widget['unique'] ) )
{
$uniqueIds[] = $widget['unique'];
}
}
}
}
return array_unique( $uniqueIds );
}
/**
* Build the where clause based on key, app
*
* @param string|null $key Key
* @param string|null $app Application
* @return array
*/
protected static function _buildWhere( ?string $key, ?string $app ): array
{
$where = array();
if( $key )
{
$where[] = array( '`key`=?', $key );
}
if( $app )
{
$where[] = array( 'app=?', $app );
}
return $where;
}
/**
* Specify widget configuration
*
* @param Form|null $form Form object
* @return Form
*/
public function configuration( Form &$form=null ): Form
{
if ( $form === null )
{
$form = new Form;
}
/* Only show visibility options if we are editing from block manager as configuration is specific to each instance */
if( Dispatcher::i()->controllerLocation == 'front' )
{
$form->add( new CheckboxSet( 'devices_to_show', $this->configuration['devices_to_show'] ?? array( 'Phone', 'Tablet', 'Desktop' ), FALSE, array( 'options' => array( 'Phone' => Member::loggedIn()->language()->addToStack( 'device_phone' ), 'Tablet' => Member::loggedIn()->language()->addToStack( 'device_tablet' ), 'Desktop' => Member::loggedIn()->language()->addToStack( 'device_desktop' ) ) ), NULL, NULL, NULL, 'devices_to_show' ) );
}
if( Settings::i()->clubs AND ( ( Request::i()->pageApp === 'core' AND Request::i()->pageModule === 'clubs' ) OR ( Request::i()->pageApp !== 'core' AND Request::i()->pageApp !== 'nexus' ) ) )
{
$form->add( new Radio( 'clubs_visibility', $this->configuration['clubs_visibility'] ?? 'all', FALSE, array( 'options' => array( 'all' => 'everywhere', 'without_clubs' => 'without_clubs', 'only_clubs' => 'only_clubs')) ) );
}
if( $this->isBuilderWidget() )
{
Output::i()->linkTags['googlefonts'] = array( 'rel' => 'stylesheet', 'href' => "https://fonts.googleapis.com/css?family=Lato|Merriweather|Open+Sans|Raleway:400,900|Roboto:400,900&display=swap" );
if ( ! isset( $this->configuration['widget_adv__custom'] ) and isset( Request::i()->block ) )
{
$customCss = '.' . Request::i()->block . " {\n\n}";
}
else
{
$customCss = $this->configuration['widget_adv__custom'] ?? '';
}
/* Box model */
$form->add( new Select( 'widget_adv__padding', $this->configuration['widget_adv__padding'] ?? 'full', FALSE,
array(
'options' => array(
'none' => 'widget_adv__padding_none',
'half' => 'widget_adv__padding_half',
'full' => 'widget_adv__padding_full',
'custom' => 'custom',
),
'toggles' => array(
'custom' => array( 'padding_custom' )
)
) ) );
$form->add( new Trbl( 'widget_adv__padding_custom', ( $this->configuration['widget_adv__padding_custom'] ?? 0 ), FALSE, array(), NULL, NULL, NULL, 'padding_custom' ) );
/* Font size */
$form->add( new Select( 'widget_adv__fontsize', $this->configuration['widget_adv__fontsize'] ?? 'i-font-size_1', FALSE,
array(
'options' => array(
'inherit' => 'widget_adv__inherit',
'ipsTitle ipsTitle--h3' => 'widget_adv__fontsize_pagetitle',
'i-font-size_2' => 'widget_adv__fontsize_large',
'i-font-size_1' => 'widget_adv__fontsize_medium',
'i-font-size_-1' => 'widget_adv__fontsize_small',
'custom' => 'custom',
),
'toggles' => array(
'custom' => array( 'font_custom' )
)
) ) );
$form->add( new Number( 'widget_adv__fontsize_custom', ( $this->configuration['widget_adv__fontsize_custom'] ?? 12 ), FALSE, array(), NULL, NULL, 'px', 'font_custom' ) );
/* Font face */
$fontOptions = array( 'inherit' => 'widget_adv__inherit' );
foreach( array( 'arial', 'helvetica', 'times new roman', 'courier', 'lato', 'merriweather', 'open sans', 'raleway', 'raleway black', 'roboto', 'roboto black' ) as $font )
{
$fontOptions[ ucfirst( $font ) ] = 'widget_adv__font_' . str_replace( ' ', '_', $font );
}
$form->add( new Select( 'widget_adv__font', $this->configuration['widget_adv__font'] ?? 'inherit', FALSE,
array(
'options' => $fontOptions
) ) );
$form->add( new Select( 'widget_adv__fontalign', $this->configuration['widget_adv__fontalign'] ?? 'left', FALSE,
array(
'options' => array(
'left' => 'widget_adv__fontalign_left',
'center' => 'widget_adv__fontalign_center',
'right' => 'widget_adv__fontalign_right'
)
) ) );
/* Font color */
$form->add( new Select( 'widget_adv__fontcolor_custom', $this->configuration['widget_adv__fontcolor_custom'] ?? FALSE, FALSE,
array(
'options' => array(
'inherit' => 'widget_adv__inherit',
'custom' => 'custom',
),
'toggles' => array(
'custom' => array( 'fontcolor_custom' )
)
), NULL, NULL, NULL, 'fontcolor_selector' ) );
$form->add( new Color( 'widget_adv__fontcolor', $this->configuration['widget_adv__fontcolor'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE ), NULL, NULL, NULL, 'fontcolor_custom' ) );
/* Background color */
$form->add( new Select( 'widget_adv__background_custom', $this->configuration['widget_adv__background_custom'] ?? FALSE, FALSE,
array(
'options' => array(
'inherit' => 'widget_adv__inherit',
//'transparent' => 'widget_adv__fontcolor_custom_transparent',
'custom' => 'widget_adv__background_custom_color',
'image' => 'widget_adv__background_custom_image',
),
'toggles' => array(
'custom' => array( 'background_custom' ),
'image' => array( 'background_image', 'background_overlay' )
)
), NULL, NULL, NULL, 'background_selector' ) );
$form->add( new Upload( 'widget_adv__background_custom_image', isset( $this->configuration['widget_adv__background_custom_image'] ) ? File::get( 'core_Attachment', $this->configuration['widget_adv__background_custom_image'] ) : '', FALSE, array( 'storageExtension' => 'core_Attachment', 'allowStockPhotos' => TRUE, 'image' => true ), NULL, NULL, NULL, 'background_image' ) );
$form->add( new Color( 'widget_adv__background_custom_image_overlay', $this->configuration['widget_adv__background_custom_image_overlay'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE, 'allowNone' => true ), NULL, NULL, NULL, 'background_overlay' ) );
$form->add( new Color( 'widget_adv__background', $this->configuration['widget_adv__background'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE ), NULL, NULL, NULL, 'background_custom' ) );
$form->add( new Codemirror( 'widget_adv__custom', $customCss, FALSE, array( 'codeModeAllowedLanguages' => [ 'css' ] ) ) );
}
return $form;
}
/**
* Get the widget from the stored data
*
* @param array $data The data stored for the widget, either in the core_widget_areas table or the cms_page_widget_areas table
*
* @return Widget|null
*/
public static function createWidgetFromStoredData( array $data ) : ?Widget
{
$config = ( isset( $data['configuration'] ) AND !empty( $data['configuration'] ) ) ? $data['configuration'] : static::getConfiguration( $data['unique'] );
try
{
$widget = static::load( Application::load( $data['app'] ), $data['key'], $data['unique'], $config, $data['restrict'] ?? null, $data['orientation'] ?? 'horizontal', $data['layout'] ?? '' ); // todo wtf does orientation do?
if ( $data['key'] === 'Database' and !\IPS\cms\Databases\Dispatcher::i()->databaseId )
{
$widget->render();
}
return $widget;
}
catch( OutOfRangeException )
{
/* The app might be disabled, so just skip it */
return null;
}
}
/**
* This method can be used to remove a widgets appearances in the CMS and Core App
*
* @param string $widgetkey The widget key
* @param string $app The app the widget belongs to
* @return bool
*/
final public static function deprecateWidget( string $widgetkey, string $app ): bool
{
$areas = array( 'core_widget_areas' );
if ( Application::appIsEnabled('cms') )
{
$areas[] = 'cms_page_widget_areas';
}
foreach ( $areas as $table )
{
$widgetsColumn = $table == 'core_widget_areas' ? 'widgets' : 'area_widgets';
foreach (Db::i()->select( '*', $table ) as $area )
{
$whereClause = $table == 'core_widget_areas' ? array( 'id=? AND area=?', $area['id'], $area['area'] ) : array( 'area_page_id=? AND area_area=?', $area['area_page_id'], $area['area_area'] );
$widgets = json_decode( $area[ $widgetsColumn ], TRUE );
$update = FALSE;
foreach ( $widgets as $k => $widget )
{
if ( $widget['key'] == $widgetkey AND $app == $widget['app'] )
{
unset( $widgets[ $k ] );
$update = TRUE;
}
}
if ( $update )
{
Db::i()->update( $table, array( $widgetsColumn => json_encode( $widgets ) ), $whereClause );
}
}
}
return TRUE;
}
}