Seditio Source
Root |
./othercms/ips_4.3.4/system/Widget/Widget.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 */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_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 $expirePerRequest = 1;
   
   
/**
     * @brief    Configuration
     */
   
public $configuration = array();
   
   
/**
     * @brief    Access. Array of allowed apps that execute the widgets. Null for no restriction
     */
   
protected $access = null;
   
   
/**
     * @brief    Custom template callback
     */
   
public $template = null;
   
   
/**
     * @brief    Orientation
     */
   
protected $orientation = null;
   
   
/**
     * @brief    Menu style
     */
   
public $menuStyle = 'menu';
   
   
/**
     * @brief    Allow block to be reused
     */
   
public $allowReuse = false;

   
/**
     * @brief    Unique key for this widget
     */
   
public $uniqueKey = NULL;
   
   
/**
     * @brief    Prevent caching for this block
     */
   
public $neverCache = FALSE;

   
/**
     * @brief    Error language string key shown after the configuration
     */
   
public $errorMessage = 'widget_blank_or_no_context';
   
   
/**
     * Constructor
     *
     * @param    String                $uniqueKey            Unique key for this specific instance
     * @param    array                $configuration        Widget custom configuration
     * @param    null|string|array    $access                Array/JSON string of executable apps (core=sidebar only, content=IP.Content only, etc)
     * @param    string                $orientation        Horizontal or vertical orientation
     * @return    void
     */
   
public function __construct( $uniqueKey, array $configuration, $access=null, $orientation=null )
    {
       
$this->configuration = $configuration;
       
$this->orientation = $orientation;
       
        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
     */
   
public function init()
    {
        if (
$this->app )
        {
           
$this->template( array( \IPS\Theme::i()->getTemplate( 'widgets', $this->app, 'front' ), $this->key ) );
        }
    }
   
   
/**
     * Constructor
     *
     * @param    string|array    $app    Application key (core,cms,gallery, etc)
     * @return    boolean
     */
   
public function isExecutableByApp( $app )
    {
        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;
    }

   
/**
     * Fetch the application for this widget
     *
     * @return    \IPS\Application
     */
   
public function application()
    {
        return \
IPS\Application::load( $this->app );
    }

   
/**
     * Fetch the title for this widget
     *
     * @return    string
     */
   
public function title()
    {
        return \
IPS\Member::loggedIn()->language()->addToStack( 'block_' . $this->key );
    }
   
   
/**
     * Fetch the description for this widget
     *
     * @return    string
     */
   
public function description()
    {
        return \
IPS\Member::loggedIn()->language()->addToStack( 'block_' . $this->key . '_desc' );
    }
   
   
/**
     * Set the template for this widget
     *
     * @param    Array|Function        $callback        Function to use for template callback
     * @return    string
     */
   
public function template( $callback )
    {
       
$this->template = $callback;
    }
   
   
/**
     * Get Template Location
     * Returns the template app/location/group/name params
     * @return array
     */
   
public function getTemplateLocation()
    {
       
$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] ) );
    }

   
/**
     * Get HTML using the template (language strings not parsed)
     *
     * @return    string
     */
   
public function output()
    {
       
$args = func_get_args();
       
$args[] = $this->orientation;

       
$template = $this->template;

       
$output = $template( ...$args );

        return
$output;
    }
   
   
/**
     * Efficient way to see if a widget has configuration
     *
     * @return boolean
     */
   
public function hasConfiguration()
    {
        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    \IPS\Application|\IPS\Plugin    $parent                Widget application or plugin
     * @param    String                            $widgetKey            Widget key used to load class
     * @param    String                            $uniqueKey            Unique key for this specific instance
     * @param    Array                            $configuration        Current configuration
     * @param    null|string|array                $access                Array/JSON string of executable apps (core=sidebar only, content=IP.Content only, etc)
     * @param    string                            $orientation        Horizontal or vertical orientation
     * @return    \IPS\Widget
     * @throws    \OutOfRangeException
     */
   
public static function load( $parent, $widgetKey, $uniqueKey, $configuration=array(), $access=null, $orientation=null )
    {
       
/* If our parent is not enabled, do not attempt to use this widget - both \IPS\Application and \IPS\Plugin have get__enabled() methods, so this covers both */
       
if ( $parent->_enabled == FALSE )
        {
            throw new \
OutOfRangeException;
        }
       
       
$class = NULL;
        if (
$parent instanceof \IPS\Application )
        {
           
$class = '\IPS\\' . $parent->directory . '\widgets\\' . $widgetKey;
        }
        else
        {
            if (
file_exists( \IPS\ROOT_PATH . '/plugins/' . $parent->location . '/widgets/' . $widgetKey . '.php' ) )
            {
                require_once \
IPS\ROOT_PATH . '/plugins/' . $parent->location . '/widgets/' . $widgetKey . '.php';
               
$class = '\IPS\\plugins\\' . $parent->location . '\\widgets\\' . $widgetKey;
            }
        }
       
       
/* Return */
       
if ( class_exists( $class ) )
        {
            return new
$class( $uniqueKey, $configuration, $access, $orientation );
        }
       
        throw new \
OutOfRangeException;
    }
   
   
/**
     * Dev Table
     *
     * @param    string            $json                Path to JSON file
     * @param    \IPS\Http\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( $json, $url, $widgetDirectory, $subpackage, $namespace, $appKeyOrPluginId )
    {
        if ( !
file_exists( $json ) )
        {
            \
file_put_contents( $json, json_encode( array() ) );
        }
   
        switch ( \
IPS\Request::i()->widgetTable )
        {
            case
'form':
               
               
$current = NULL;
                if ( isset( \
IPS\Request::i()->key ) )
                {
                   
$widgets = json_decode( file_get_contents( $json ), TRUE );
                    if (
array_key_exists( \IPS\Request::i()->key, $widgets ) )
                    {
                       
$current = array(
                               
'dev_widget_key'            => \IPS\Request::i()->key,
                               
'dev_widget_class'            => $widgets[ \IPS\Request::i()->key ]['class'],
                               
'dev_widget_restrict'        => $widgets[ \IPS\Request::i()->key ]['restrict'],
                               
'dev_widget_default_area'    => isset($widgets[ \IPS\Request::i()->key ]['default_area']) ? $widgets[ \IPS\Request::i()->key ]['default_area'] : NULL,
                               
'dev_widget_allow_reuse'    => isset($widgets[ \IPS\Request::i()->key ]['allow_reuse'])  ? $widgets[ \IPS\Request::i()->key ]['allow_reuse']  : 0,
                               
'dev_widget_menu_style'        => isset($widgets[ \IPS\Request::i()->key ]['menu_style'])   ? $widgets[ \IPS\Request::i()->key ]['menu_style']   : 'menu',
                               
'dev_widget_embeddable'     => isset($widgets[ \IPS\Request::i()->key ]['embeddable'])   ? $widgets[ \IPS\Request::i()->key ]['embeddable']   : 1,

                        );
                    }
                    unset(
$widgets );
                }
   
               
$form = new \IPS\Helpers\Form;
               
$form->add( new \IPS\Helpers\Form\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 ( \
IPS\Db::i()->select( 'count(*)', 'core_widgets', $where )->first() )
                    {
                        throw new \
DomainException( 'dev_widget_key_err' );
                    }
                } ) );
               
               
$classes = array( '\IPS\Widget' => '\IPS\Widget' );
                \
IPS\Member::loggedIn()->language()->words[ "\\IPS\\Widget" . '_desc' ] = \IPS\Member::loggedIn()->language()->get( 'widget_class_default' );
                foreach ( new \
DirectoryIterator( \IPS\ROOT_PATH . "/system/Widget" ) as $file )
                {
                    if ( !
$file->isDot() and $file->getExtension() === 'php' and $file != 'Widget.php' )
                    {
                       
$classes[ "\\IPS\\Widget\\" .  mb_substr( $file, 0, -4 ) ] = "\\IPS\\Widget\\" .  mb_substr( $file, 0, -4 );
                        try
                        {
                            \
IPS\Member::loggedIn()->language()->words[ "\\IPS\\Widget\\" .  mb_substr( $file, 0, -4 ) . '_desc' ] = \IPS\Member::loggedIn()->language()->get( 'widget_class_' . mb_substr( $file, 0, -4 ) );
                        }
                        catch ( \
UnderflowException $e ) { }
                    }
                }
               
$form->add( new \IPS\Helpers\Form\Radio( 'dev_widget_class', ( $current ? $current['dev_widget_class'] : NULL ), TRUE, array( 'options' => $classes ) ) );
               
               
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'dev_widget_restrict', ( ( $current and !empty( $current['dev_widget_restrict'] ) ) ? $current['dev_widget_restrict'] : ( $current['dev_widget_restrict'] === FALSE ? array() : array( 'sidebar', 'cms' ) ) ), FALSE, array(
                   
'options' => array(
                       
'sidebar'    => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_restrict_sidebar'),
                       
'cms'       => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_restrict_cms'),
                    ),
                   
'multiple' => true ) ) );
               
               
$form->add( new \IPS\Helpers\Form\Radio( 'dev_widget_default_area', ( $current ? ( $current['dev_widget_default_area'] ?: 'none' ) : 'none' ), FALSE, array(
                       
'options' => array(
                               
'none'        => \IPS\Member::loggedIn()->language()->addToStack('none'),
                               
'sidebar'    => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_default_area_sidebar'),
                               
'header'    => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_default_area_header'),
                               
'footer'    => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_default_area_footer'),
                               
                        ),
                       
'multiple' => false ) ) );
               
               
$form->add( new \IPS\Helpers\Form\Radio( 'dev_widget_menu_style', ( $current ? $current['dev_widget_menu_style'] : 'menu' ), FALSE, array(
                   
'options' => array(
                       
'menu'    => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_menu_style_menu'),
                       
'modal'       => \IPS\Member::loggedIn()->language()->addToStack('dev_widget_menu_style_modal'),
                ) ) ) );
               
               
$form->add( new \IPS\Helpers\Form\YesNo( 'dev_widget_allow_reuse', ( $current ? $current['dev_widget_allow_reuse'] : 0 ), FALSE ) );

               
$form->add( new \IPS\Helpers\Form\YesNo( 'dev_widget_embeddable', ( $current ? $current['dev_widget_embeddable'] : 0 ), FALSE ) );

                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\IPS_FOLDER_PERMISSION );
                        }
   
                        \
file_put_contents( $widgetFile, str_replace(
                                array(
                                       
'{key}',
                                       
"{subpackage}\n",
                                       
'{date}',
                                       
'{namespace}',
                                       
'{class}',
                                       
'{appkey}',
                                       
'{pluginid}'
                               
),
                                array(
                                       
$values['dev_widget_key'],
                                        (
$subpackage != 'core' ) ? ( " * @subpackage\t" . $subpackage . "\n" ) : '',
                                       
date( 'd M Y' ),
                                       
$namespace,
                                       
$values['dev_widget_class'],
                                       
is_string( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                                       
is_numeric( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                                ),
                               
file_get_contents( \IPS\ROOT_PATH . "/applications/core/data/defaults/Widget.txt" )
                        ) );
                       
chmod( $widgetFile, \IPS\IPS_FILE_PERMISSION );
                    }
                       
                   
/* Add to DB */
                   
\IPS\Db::i()->replace( 'core_widgets', array(
                           
'app'            => is_string( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                           
'plugin'        => is_numeric( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                           
'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'] ) ) ),
                           
'default_area'    => ( $values['dev_widget_default_area'] === 'none' ) ? '' : $values['dev_widget_default_area'],
                           
'allow_reuse'    => $values['dev_widget_allow_reuse'],
                           
'menu_style'    => $values['dev_widget_menu_style'],
                           
'embeddable'    => $values['dev_widget_embeddable']
                    ) );
                    unset( \
IPS\Data\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'] ) ),
                       
'default_area' => ( $values['dev_widget_default_area'] === 'none' ) ? '' : $values['dev_widget_default_area'],
                       
'allow_reuse'  => $values['dev_widget_allow_reuse'],
                       
'menu_style'   => $values['dev_widget_menu_style'],
                       
'embeddable'   => $values['dev_widget_embeddable']
                    );
                   
                    \
IPS\Application::writeJson( $json, $widgets );
                       
                   
/* Redirect */
                   
\IPS\Output::i()->redirect( $url, 'saved' );
                }
   
                return
$form;
   
            case
'delete':
   
               
$widgets = json_decode( file_get_contents( $json ), TRUE );
                if (
array_key_exists( \IPS\Request::i()->key, $widgets ) )
                {
                    unset(
$widgets[ \IPS\Request::i()->key ] );
                    \
file_put_contents( $json, json_encode( $widgets ) );
                       
                    if (
file_exists( $widgetDirectory . "/" . \IPS\Request::i()->key . ".php" ) )
                    {
                       
unlink( $widgetDirectory . "/" . \IPS\Request::i()->key . ".php" );
                    }
                       
                    \
IPS\Db::i()->delete( 'core_widgets', array( ( is_string( $appKeyOrPluginId ) ? 'app' : 'plugin' ) . '=? AND `key`=?', $appKeyOrPluginId, \IPS\Request::i()->key ) );
                    unset( \
IPS\Data\Store::i()->widgets );
                }
                \
IPS\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 ? \IPS\Member::loggedIn()->language()->addToStack('dev_widget_nowhere') : ( ( ( count( $json['restrict'] ) > 0 and count( $json['restrict'] ) !== 2 ) ? implode( ',', array_map( function( $val ) { return \IPS\Member::loggedIn()->language()->addToStack('dev_widget_restrict_'.$val); }, $json['restrict'] ) ) : \IPS\Member::loggedIn()->language()->addToStack('everywhere') ) ),
                           
'dev_widget_area'        => isset($json['default_area']) ? ( $json['default_area'] ? \IPS\Member::loggedIn()->language()->addToStack( 'dev_widget_default_area_' . $json['default_area'] ) : \IPS\Member::loggedIn()->language()->addToStack('none') ) : 'sidebar',
                           
'dev_widget_embeddable'    => isset($json['embeddable'])   ? ( $json['embeddable'] ? '&#10003;' : '&#10007;' ) : '&#10007;'
                   
);
                }
   
               
$table = new \IPS\Helpers\Table\Custom( $data, $url );
               
$table->rootButtons = array(
                       
'add' => array(
                               
'icon'    => 'plus',
                               
'title'    => 'add',
                               
'link'    => $url->setQueryString( 'widgetTable', 'form' ),
                               
'data'    => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('add') )
                        )
                );
               
$table->rowButtons = function( $row ) use ( $url )
                {
                    return array(
                           
'edit' => array(
                                   
'icon'    => 'pencil',
                                   
'title'    => 'edit',
                                   
'link'    => $url->setQueryString( 'widgetTable', 'form' )->setQueryString( 'key', $row['dev_widget_key'] ),
                                   
'data'    => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') )
                            ),
                           
'delete' => array(
                                   
'icon'    => 'times-circle',
                                   
'title'    => 'delete',
                                   
'link'    => $url->setQueryString( 'widgetTable', 'delete' )->setQueryString( 'key', $row['dev_widget_key'] ),
                                   
'data'    => array( 'delete' => '' )
                            )
                    );
                };
   
                return
$table;
                   
        }
    }
   
   
/**
     * Get all cache keys (for all possible permissions, etc.)
     *
     * @param    String    $key                Widget key
     * @param    String    $app                Parent application
     * @param    String    $plugin                Parent plugin
     * @return    array
     * @note    This method does not take responsibility for checking if caches are expired
     */
   
public static function getCaches( $key=NULL, $app=NULL, $plugin=NULL )
    {
       
$caches = array();

        try
        {
            foreach( \
IPS\Db::i()->select( '*', 'core_widgets', static::_buildWhere( $key, $app, $plugin ) ) 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    $key                Widget key
     * @param    String    $app                Parent application
     * @param    String    $plugin                Parent plugin
     * @return    void
     */
   
public static function deleteCaches( $key=NULL, $app=NULL, $plugin=NULL )
    {
       
$caches = static::getCaches( $key, $app, $plugin );

        foreach (
$caches as $cacheKey => $time )
        {
            unset( \
IPS\Data\Store::i()->$cacheKey );
        }

        \
IPS\Db::i()->update( 'core_widgets', array( 'caches' => NULL ), static::_buildWhere( $key, $app, $plugin ) );
        unset( \
IPS\Data\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( $uniqueId, $data )
    {
        \
IPS\Db::i()->delete( 'core_widget_trash', array( 'id=?', $uniqueId ) );
        \
IPS\Db::i()->insert( 'core_widget_trash', array(
           
'id'    => $uniqueId,
           
'data'  => json_encode( $data ),
           
'date'  => time()
        ) );
    }

   
/**
     * 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( $uniqueId )
    {
        foreach( \
IPS\Db::i()->select( '*', 'core_widget_areas' ) as $item )
        {
           
$widgets = json_decode( $item['widgets'], TRUE );

            foreach(
$widgets as $widget )
            {
                if (
$widget['unique'] == $uniqueId )
                {
                    if ( isset(
$widget['configuration'] ) )
                    {
                        return
$widget['configuration'];
                    }
                }
            }
        }

       
/* Still here? rummage in the trash */
       
try
        {
           
$widget = \IPS\Db::i()->select( '*', 'core_widget_trash', array( 'id=?', $uniqueId ) )->first();

           
$data = json_decode( $widget['data'], TRUE );

            if ( isset(
$data['configuration'] ) )
            {
                return
$data['configuration'];
            }
        }
        catch( \
UnderflowException $ex ) { }

        return array();
    }    
   
   
/**
     * Get default widgets for an application
     *
     * @param    \IPS\Application    $app    The application
     * @return    array
     */
   
public static function appDefaults( \IPS\Application $app )
    {
        if ( !isset( \
IPS\Data\Store::i()->widgets ) )
        {
           
$widgets = array();
            foreach ( \
IPS\Db::i()->select( '*', 'core_widgets' ) as $row )
            {
                if (
$row['app'] )
                {
                   
$widgets[ $row['app'] ][ $row['key'] ] = $row;
                }
            }
           
            \
IPS\Data\Store::i()->widgets = $widgets;
        }
       
       
$return = array();
        if ( isset( \
IPS\Data\Store::i()->widgets[ $app->directory ] ) )
        {
            foreach ( \
IPS\Data\Store::i()->widgets[ $app->directory ] as $widget )
            {
                if (
$widget['default_area'] )
                {
                   
$return[] = $widget;
                }
            }
        }
       
        return
$return;
    }

   
/**
     * @brief    Cached output to prevent rendering widget twice
     */
   
protected $cachedOutput    = NULL;
   
   
/**
     * 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( \IPS\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( \IPS\Request::i()->cookie['vle_editor'] ) or !\IPS\Request::i()->cookie['vle_editor'] ) AND !\IPS\IN_DEV AND !\IPS\Theme::designersModeEnabled() )
                {        
                   
/* How long does the store last (in seconds)? Individual widgets can set a higher expiration where sensible (e.g. upcoming calendar events
                        can be cached for a day regardless of this setting) but not lower */
                   
$expiration = \IPS\Settings::i()->widget_cache_ttl;
                    if ( isset(
$this->cacheExpiration ) and $this->cacheExpiration > $expiration )
                    {
                       
$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( \
IPS\Data\Store::i()->$cacheKey ) or ( $widget = \IPS\Data\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 */
                           
\IPS\Data\Store::i()->$cacheKey = array( 'built' => time(), 'html' => '' );
                           
                           
/* Render and store */
                           
$content = $this->render();
                            \
IPS\Member::loggedIn()->language()->parseOutputForDisplay( $content );
                            \
IPS\Data\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, $this->plugin );
                           
                            foreach(
$caches as $key => $timeBuilt )
                            {
                                if (
$key === $cacheKey )
                                {
                                    continue;
                                }
                               
                                if (
$timeBuilt < ( time() - $expiration ) )
                                {
                                    if ( isset( \
IPS\Data\Store::i()->$key ) )
                                    {
                                        unset( \
IPS\Data\Store::i()->$key );
                                    }
           
                                    unset(
$caches[ $key ] );
                                }
                            }
                           
                           
$caches[ $cacheKey ] = time();
                            \
IPS\Db::i()->update( 'core_widgets', array( 'caches' => json_encode( $caches ) ), static::_buildWhere( $this->key, $this->app, $this->plugin ) );
                        }
                       
                       
/* Then use what the store has */
                       
$widget = \IPS\Data\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 $e )
        {
            \
IPS\IPS::exceptionHandler( $e );
        }
        catch ( \
Throwable $e )
        {
            \
IPS\IPS::exceptionHandler( $e );
        }
    }

   
/**
     * 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( $output )
    {
        if (
mb_stristr( $output, '<time' ) )
        {
           
$output = preg_replace_callback( '#<time([^>]+?)?>(\d{10})</time>#i', function( $matches )
            {
               
$options = array();
               
                if (
$matches[1] )
                {
                   
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 = \IPS\DateTime::ts( $matches[2] );
               
$val = $obj->html();
               
                if ( isset(
$options['dateonly'] ) )
                {
                   
$val = (string) $obj->localDate();    
                }
                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( $seconds=86400 )
    {
       
$uniqueIds = static::getUniqueIds();

        foreach( \
IPS\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( \IPS\Application::load( $data['app'] ), $data['key'], $data['unique'], isset( $data['configuration'] ) ? $data['configuration'] : NULL );
                   
$widget->delete();
                }
                catch( \
Exception $ex ) { }
            }
        }
       
        \
IPS\Db::i()->delete( 'core_widget_trash', array( 'date < ?', time() - $seconds ) );
    }
   
   
/**
     * Return unique IDs in use
     *
     * @return array
     */
   
public static function getUniqueIds()
    {
       
$uniqueIds = array();
        foreach ( \
IPS\Db::i()->select( '*', 'core_widget_areas' ) as $row )
        {
           
$data = json_decode( $row['widgets'], TRUE );
           
            if (
count( $data ) )
            {
                foreach(
$data as $widget )
                {
                    if ( isset(
$widget['unique'] ) )
                    {
                       
$uniqueIds[] = $widget['unique'];
                    }
                }
            }
        }

        return
$uniqueIds;
    }
   
   
/**
     * Build the where clause based on key, app, plugin
     *
     * @param    string    $key    Key
     * @param    string    $app    Application
     * @param    string    $plugin    Plugin
     * @return    array
     */
   
protected static function _buildWhere( $key, $app, $plugin )
    {
       
$where = array();
       
        if(
$key )
        {
           
$where[] = array( '`key`=?', (string) $key );
        }

        if(
$app )
        {
           
$where[] = array( 'app=?', (string) $app );
        }

        if(
$plugin )
        {
           
$where[] = array( 'plugin=?', (string) $plugin );
        }

        return
$where;
    }
}