<?php
/**
 * @brief		Sidebar Widget Class
 *
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 *
 * @package		IPS Social Suite
 * @since		15 Nov 2013
 * @version		SVN_VERSION_NUMBER
 */

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	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;
	
	/**
	 * 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 ?: uniqid() ) : $uniqueKey;
	}
	
	/**
	 * 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
		{
			$bits = explode( '\\', $class );
			list( $class, $app, $location, $group ) = explode( '_', $bits[2] );

			$params = array( 'app' => $app, 'location' => $location, 'group' => $group );
		}

		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['orientation'] = $this->orientation;

		$output = call_user_func_array( $this->template, $args );

		return $output;
	}
	
	/**
	 * Efficient way to see if a widget has configuration
	 *
	 * @return boolean
	 */
	public function hasConfiguration()
	{
		return method_exists( $this, 'configuration' );
	}
	
	/**
	 * 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 )
	{
		$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				$version			The application/plugin's current version
	 * @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, $version, $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 ), function( $val ) use ( $current )
				{
					$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}',
										'{version_long}',
										'{class}',
										'{appkey}',
										'{pluginid}'
								),
								array(
										$values['dev_widget_key'],
										( $subpackage != 'core' ) ? ( " * @subpackage\t" . $subpackage . "\n" ) : '',
										date( 'd M Y' ),
										$namespace,
										$version,
										$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" )
						) );
					}
						
					/* 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 Caches
	 *
	 * @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
		{
			$widgets = \IPS\Db::i()->select( '*', 'core_widgets', static::_buildWhere( $key, $app, $plugin ) );

			if ( count( $widgets ) )
			{
				foreach( $widgets as $widget )
				{
					if( $widget['caches'] )
					{
						foreach ( json_decode( $widget['caches'], TRUE ) 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['id'] ] = $row;
				}
			}
			
			\IPS\Data\Store::i()->widgets = $widgets;
		}
		
		return isset( $app->directory ) ? \IPS\Data\Store::i()->widgets[ $app->directory ] : array();
	}

	/**
	 * @brief	Cached output to prevent rendering widget twice
	 */
	protected $cachedOutput	= NULL;
	
	/**
	 * Convert the widget to HTML
	 *
	 * @return	string
	 */
	public function __toString()
	{
		try
		{
			if ( $this->cachedOutput !== NULL )
			{
				return static::parseOutput( $this->cachedOutput );
			}
			
			if ( $this->neverCache === TRUE )
			{
				return static::parseOutput( $this->render() );
			}
			
			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() )
			{
				$cacheKey   = $this->cacheKey;
				$expiration = \IPS\Settings::i()->widget_cache_ttl;

				if ( isset( $this->cacheExpiration ) and $this->cacheExpiration < $expiration )
				{
					$expiration = $this->cacheExpiration;
				}

				if ( isset( \IPS\Data\Store::i()->$cacheKey ) )
				{
					$widget = \IPS\Data\Store::i()->$cacheKey;

					if( !$expiration OR ( $expiration AND $widget['built'] < ( time() - $expiration ) ) )
					{
						unset( \IPS\Data\Store::i()->$cacheKey );
					}
					else
					{
						$this->cachedOutput = $widget['html'];

						/* Cache is good, so prevent recaching below */
						return is_string( $this->cachedOutput ) ? static::parseOutput( $this->cachedOutput ) : '';
					}
				}

				/* Rebuild */
				if( $this->cachedOutput === NULL )
				{
					/* Avoid race conditions */
					\IPS\Data\Store::i()->$cacheKey = array( 'built' => time(), 'html' => NULL );

					$this->cachedOutput = $this->render();
					\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $this->cachedOutput );
				}
				
				$output = $this->cachedOutput;

				/* Cache */
				\IPS\Data\Store::i()->$cacheKey = array( 'built' => time(), 'html' => $output );
				
				/* Store the cache key */
				$caches = static::getCaches( $this->key, $this->app, $this->plugin );

				foreach( $caches as $key => $timeBuilt )
				{
					if( !$expiration OR ( $expiration AND $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 ) );
				unset( \IPS\Data\Store::i()->widgets );
				
				/* Return */
				return static::parseOutput( $output );
			}
			else
			{
				/* Rebuild */
				if( $this->cachedOutput === NULL )
				{
					$this->cachedOutput = $this->render();
				}
				
				return static::parseOutput( $this->cachedOutput );
			}
		}
		catch ( \Exception $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;
	}

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