<?php
/**
 * @brief		Application Class
 * @author		<a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/legal/standards/
 * @package		IPS Social Suite
 * @since		18 Feb 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;
}

/**
 * @brief	Abstract class that applications extend and use to handle application data
 */
class _Application extends \IPS\Node\Model
{
	/**
	 * @brief	IPS Applications
	 */
	public static $ipsApps = array(
		'blog',
		'calendar',
		'chat',
		'cms',
		'core',
		'downloads',
		'forums',
		'gallery',
		'nexus'
		);

	/**
	 * @brief	[ActiveRecord] Multiton Store
	 */
	protected static $multitons;
	
	/**
	 * @brief	Have fetched all?
	 */
	protected static $gotAll	= FALSE;

	/**
	 * @brief	Defined versions
	 */
	protected $definedVersions	= NULL;

	/**
	 * @brief	[Node] Title prefix.  If specified, will look for a language key with "{$key}_title" as the key
	 */
	public static $titleLangPrefix = '__app_';

	/**
	 * @brief	Defined theme locations for the theme system
	 */
	public $themeLocations = array('admin', 'front', 'global');
	
	/**
	 * Set default
	 *
	 * @return void
	 */
	public function setAsDefault()
	{
		/* Get the FURL definition */
		$furlDefinition = \IPS\Http\Url::furlDefinition();
		
		/* Add the top-level directory to all the FURLs for the old default app */
		$previousDefaultApp = \IPS\Db::i()->select( 'app_directory', 'core_applications', 'app_default=1' )->first();
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$previousDefaultApp}/data/furl.json" ) )
		{
			$oldDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( \IPS\ROOT_PATH . "/applications/{$previousDefaultApp}/data/furl.json" ) ), TRUE );
			if ( $oldDefaultAppDefinition['topLevel'] )
			{
				foreach ( $oldDefaultAppDefinition['pages'] as $k => $data )
				{
					if ( isset( $furlDefinition[ $k ] ) )
					{
						$furlDefinition[ $k ]['without_top_level'] = $furlDefinition[ $k ]['friendly'];
						$furlDefinition[ $k ]['friendly'] = rtrim( $oldDefaultAppDefinition['topLevel'] . '/' . $furlDefinition[ $k ]['friendly'], '/' );
					}
				}
			}
		}
		
		/* And remove it from the new */
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/furl.json" ) )
		{
			$newDefaultAppDefinition = json_decode( preg_replace( '/\/\*.+?\*\//s', '', \file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/furl.json" ) ), TRUE );
			if ( $newDefaultAppDefinition['topLevel'] )
			{
				foreach ( $newDefaultAppDefinition['pages'] as $k => $data )
				{
					if ( isset( $furlDefinition[ $k ] ) )
					{
						unset( $furlDefinition[ $k ]['without_top_level'] );
						$furlDefinition[ $k ]['with_top_level'] = $furlDefinition[ $k ]['friendly'];
						$furlDefinition[ $k ]['friendly'] = rtrim( preg_replace( '/^' . preg_quote( $newDefaultAppDefinition['topLevel'] ) . '\/?/', '', $furlDefinition[ $k ]['friendly'] ), '/' );
					}
				}
			}
		}
				
		/* Save the new FURL definition */		
		\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => json_encode( $furlDefinition ) ), array( 'conf_key=?', 'furl_configuration' ) );
		
		/* Actually update the database */
		\IPS\DB::i()->update( 'core_applications', array( 'app_default' => 0 ) );
		\IPS\DB::i()->update( 'core_applications', array( 'app_default' => 1 ), array( 'app_id=?', $this->id ) );
		
		/* Clear cached data */
		unset( \IPS\Data\Store::i()->applications );
		unset( \IPS\Data\Store::i()->settings );
		\IPS\Member::clearCreateMenu();
	}
	
	/**
	 * Get Applications
	 *
	 * @return	array
	 */
	public static function applications()
	{
		if( self::$gotAll === FALSE )
		{
			if ( isset( \IPS\Data\Store::i()->applications ) )
			{
				$rows = \IPS\Data\Store::i()->applications;
			}
			else
			{
				$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_applications', NULL, 'app_position' ) );
				\IPS\Data\Store::i()->applications = $rows;
			}
			
			self::$multitons = array();
			
			foreach ( $rows as $row )
			{
				try
				{
					self::$multitons[ $row['app_directory'] ] = static::constructFromData( $row );
				}
				catch( \UnexpectedValueException $e )
				{
					if ( mb_stristr( $e->getMessage(), 'Missing:' ) )
					{
						/* Ignore this, the app is in the table, but not 4.0 compatible */
						continue;
					}
				}
			}
			
			self::$gotAll = TRUE;
		}
		
		return self::$multitons;
	}
	
	/**
	 * Does an application exist and is it enabled? Note: does not check if offline for a particular member
	 *
	 * @see		\IPS\Application::canAccess()
	 * @param	string	$key	Application key
	 * @return	bool
	 */
	public static function appIsEnabled( $key )
	{
		$applications = self::applications();
		
		if ( !array_key_exists( $key, $applications ) )
		{
			return FALSE;
		}

		if ( ! file_exists( \IPS\ROOT_PATH . '/applications/' . $key . '/Application.php' ) )
		{
			return FALSE;
		}
				
		return $applications[ $key ]->enabled;
	}
	 
	/**
	 * Load Record
	 *
	 * @see		\IPS\Db::build
	 * @param	int|string	$id					ID
	 * @param	string		$idField			The database column that the $id parameter pertains to (NULL will use static::$databaseColumnId)
	 * @param	mixed		$extraWhereClause	Additional where clause(s) (see \IPS\Db::build for details)
	 * @return	static
	 * @throws	\InvalidArgumentException
	 * @throws	\OutOfRangeException
	 */
	public static function load( $id, $idField=NULL, $extraWhereClause=NULL )
	{
		static::applications(); // Load all applications so we can grab the data from the cache
		return parent::load( $id, $idField, $extraWhereClause );		
	}

	/**
	 * Fetch All Root Nodes
	 *
	 * @param	string|NULL			$permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Member|NULL	$member				The member to check permissions for or NULL for the currently logged in member
	 * @param	mixed				$where				Additional WHERE clause
	 * @note	This is overridden to prevent UnexpectedValue exceptions when there is an old application record in core_applications without an Application.php file
	 * @return	array
	 */
	public static function roots( $permissionCheck='view', $member=NULL, $where=array() )
	{
		return static::applications();
	}

	/**
	 * Get all extensions
	 *
	 * @param	\IPS\Application|string		$app				The app key of the application which owns the extension
	 * @param	string						$extension			Extension Type
	 * @param	\IPS\Member|bool			$checkAccess		Check access permission for application against supplied member (or logged in member, if TRUE) before including extension
	 * @param	string|NULL					$firstApp			If specified, the application with this key will be returned first
	 * @param	string|NULL					$firstExtensionKey	If specified, the extension with this key will be returned first
	 * @param	bool						$construct			Should an object be returned? (If false, just the classname will be returned)
	 * @return	array
	 */
	public static function allExtensions( $app, $extension, $checkAccess=TRUE, $firstApp=NULL, $firstExtensionKey=NULL, $construct=TRUE )
	{
		$extensions = array();
	
		/* Get applications */
		$apps = self::applications();

		if ( $firstApp !== NULL )
		{
			$apps = self::$multitons;

			usort( $apps, function( $a, $b ) use ( $firstApp )
			{
				if ( $a->directory === $firstApp )
				{
					return -1;
				}
				if ( $b->directory === $firstApp )
				{
					return 1;
				}
				return 0;
			} );
		}
		
		/* Get extensions */
		foreach ( $apps as $application )
		{
			if ( !static::appIsEnabled( $application->directory ) )
			{
				continue;
			}
						
			if( $checkAccess !== FALSE )
			{
				if( !$application->canAccess( $checkAccess === TRUE ? NULL : $checkAccess ) )
				{
					continue;
				}
			}

			$_extensions = array();

			foreach ( $application->extensions( $app, $extension, $construct, $checkAccess ) as $key => $class )
			{
				$_extensions[ $application->directory . '_' . $key ] = $class;
			}

			if ( $firstExtensionKey !== NULL AND array_key_exists( $application->directory . '_' . $firstExtensionKey, $_extensions ) )
			{
				uksort( $_extensions, function( $a, $b ) use ( $application, $firstExtensionKey )
				{
					if ( $a === $application->directory . '_' . $firstExtensionKey )
					{
						return -1;
					}
					if ( $b === $application->directory . '_' . $firstExtensionKey )
					{
						return 1;
					}
					return 0;
				} );
			}

			$extensions = array_merge( $extensions, $_extensions );
		}
		
		/* Return */
		return $extensions;
	}

	/**
	 * Retrieve a list of applications that contain a specific type of extension
	 *
	 * @param	\IPS\Application|string		$app				The app key of the application which owns the extension
	 * @param	string						$extension			Extension Type
	 * @param	\IPS\Member|bool			$checkAccess		Check access permission for application against supplied member (or logged in member, if TRUE) before including extension
	 * @return	array
	 */
	public static function appsWithExtension( $app, $extension, $checkAccess=TRUE )
	{
		$_apps	= array();

		foreach( self::applications() as $application )
		{
			if ( static::appIsEnabled( $application->directory ) )
			{
				/* If $checkAccess is false we don't verify access to the app */
				if( $checkAccess !== FALSE )
				{
					/* If we passed true, we want to check current member, otherwise pass the member in directly */
					if( $application->canAccess( ( $checkAccess === TRUE ) ? NULL : $checkAccess ) !== TRUE )
					{
						continue;
					}
				}

				if( count( $application->extensions( $app, $extension ) ) )
				{
					$_apps[ $application->directory ] = $application;
				}
			}
		}

		return $_apps;
	}
	
	/**
	 * Get available version for an application
	 * Used by the installer/upgrader
	 *
	 * @param	string		$appKey	The application key
	 * @param	bool		$human	Return the human-readable version instead
	 * @return	int|null
	 */
	public static function getAvailableVersion( $appKey, $human=FALSE )
	{
		$versionsJson = \IPS\ROOT_PATH . "/applications/{$appKey}/data/versions.json";

		$_versions	= $human ? array_values( json_decode( file_get_contents( $versionsJson ), TRUE ) ) : array_keys( json_decode( file_get_contents( $versionsJson ), TRUE ) );
		if ( file_exists( $versionsJson ) and $versionsJson = $_versions )
		{
			return array_pop( $versionsJson );
		}
		
		return NULL;
	}

	/**
	 * Get all defined versions for an application
	 *
	 * @return	array
	 */
	public function getAllVersions()
	{
		if( $this->definedVersions !== NULL )
		{
			return $this->definedVersions;
		}

		$this->definedVersions	= array();

		$versionsJson = \IPS\ROOT_PATH . "/applications/{$this->directory}/data/versions.json";

		if ( file_exists( $versionsJson ) )
		{
			$this->definedVersions	= json_decode( file_get_contents( $versionsJson ), TRUE );
		}
		
		return $this->definedVersions;
	}
	
	/**
	 * Return the human version of an INT long version
	 *
	 * @param 	int 	$longVersion	Long version (10001)
	 * @return	string|false			Long Version (1.1.1 Beta 1)
	 */
	public function getHumanVersion( $longVersion )
	{
		$this->getAllVersions();
		
		if ( isset( $this->definedVersions[ $longVersion ] ) )
		{
			return $this->definedVersions[ (int) $longVersion ];
		}
		
		return false;
	}
	
	/**
	 * @brief	[ActiveRecord] Database Table
	 */
	public static $databaseTable = 'core_applications';
	
	/**
	 * @brief	[ActiveRecord] Database Prefix
	 */
	public static $databasePrefix = 'app_';
	
	/**
	 * @brief	[ActiveRecord] ID Database Column
	 */
	public static $databaseColumnId = 'directory';
	
	/**
	 * @brief	[ActiveRecord] Database ID Fields
	 */
	protected static $databaseIdFields = array( 'app_id' );
		
	/**
	 * @brief	[Node] Subnode class
	 */
	public static $subnodeClass = 'IPS\Application\Module';
	
	/**
	 * @brief	[Node] Node Title
	 */
	public static $nodeTitle = 'applications_and_modules';
	
	/**
	 * @brief	[Node] Order Database Column
	 */
	public static $databaseColumnOrder = 'position';
	
	/**
	 * @brief	[Node] ACP Restrictions
	 */
	protected static $restrictions = array( 'app' => 'core', 'module' => 'applications', 'prefix' => 'app_' );
	
	/**
	 * Construct ActiveRecord from database row
	 *
	 * @param	array	$data							Row from database table
	 * @param	bool	$updateMultitonStoreIfExists	Replace current object in multiton store if it already exists there?
	 * @return	static
	 */
	public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
	{
		/* Load class */
		if( !file_exists( \IPS\ROOT_PATH . '/applications/' . $data['app_directory'] . '/Application.php' ) )
		{
			if( !\IPS\Dispatcher::hasInstance() OR \IPS\Dispatcher::i()->controllerLocation !== 'setup' )
			{
				throw new \UnexpectedValueException( "Missing: " . '/applications/' . $data['app_directory'] . '/Application.php' );
			}
			else
			{
				$className = "\\IPS\\{$data['app_directory']}\\Application";

				if( !class_exists( $className ) )
				{
					$code = <<<EOF
namespace IPS\\{$data['app_directory']};
class Application extends \\IPS\\Application{}
EOF;
					eval( $code );
				}
			}
		}
		else
		{
			require_once \IPS\ROOT_PATH . '/applications/' . $data['app_directory'] . '/Application.php';
		}

		/* Initiate an object */
		$classname = 'IPS\\' . $data['app_directory'] . '\\Application';
		$obj = new $classname;
		$obj->_new = FALSE;
		
		/* Import data */
		foreach ( $data as $k => $v )
		{
			if( static::$databasePrefix )
			{
				$k = \substr( $k, \strlen( static::$databasePrefix ) );
			}
			
			$obj->_data[ $k ] = $v;
		}
		$obj->changed = array();
				
		/* Return */
		return $obj;
	}
	
	/**
	 * @brief	Modules Store
	 */
	protected $modules = NULL;
	
	/**
	 * Get Modules
	 *
	 * @see		self::$modules
	 * @param	string	$location	Location (e.g. "admin" or "front")
	 * @return	array
	 */
	public function modules( $location=NULL )
	{
		/* Don't have an instance? */
		if( $this->modules === NULL )
		{
			$modules = \IPS\Application\Module::modules();
			$this->modules = $modules[ $this->directory ];
		}
		
		/* Return */
		return isset( $this->modules[ $location ] ) ? $this->modules[ $location ] : array();
	}
	
	/**
	 * Returns the ACP Menu JSON for this application.
	 *
	 * @return array
	 */
	public function acpMenu()
	{
		return json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/acpmenu.json" ), TRUE );
	}
	
	/**
	 * ACP Menu Numbers
	 *
	 * @param	array	$queryString	Query String
	 * @return	int
	 */
	public function acpMenuNumber( $queryString )
	{
		return 0;
	}
	
	/**
	 * Get Extensions
	 *
	 * @param	\IPS\Application|string		$app		    The app key of the application which owns the extension
	 * @param	string						$extension	    Extension Type
	 * @param	bool						$construct	    Should an object be returned? (If false, just the classname will be returned)
	 * @param	\IPS\Member|bool			$checkAccess	Check access permission for extension against supplied member (or logged in member, if TRUE)
	 * @return	array
	 */
	public function extensions( $app, $extension, $construct=TRUE, $checkAccess=FALSE )
	{		
		$app = ( is_string( $app ) ? $app : $app->directory );
		
		$classes = array();
		$directory = \IPS\ROOT_PATH . "/applications/{$this->directory}/extensions/{$app}/{$extension}";
		
		if ( is_dir( $directory ) )
		{
			$dir = new \DirectoryIterator( $directory );
						
			foreach ( $dir as $file )
			{
				if ( !$file->isDir() and !$file->isDot() and mb_substr( $file, -4 ) === '.php' )
				{
					$classname = 'IPS\\' . $this->directory . '\extensions\\' . $app . '\\' . $extension . '\\' . mb_substr( $file, 0, -4 );
					
					if ( method_exists( $classname, 'generate' ) )
					{
						$classes = array_merge( $classes, $classname::generate() );
					}
					elseif ( !$construct )
					{
						$classes[ mb_substr( $file, 0, -4 ) ] = $classname;
					}
					else
					{
						try
						{
							$classes[ mb_substr( $file, 0, -4 ) ] = new $classname;
						}
						catch( \RuntimeException $e ){}
					}
				}
			}
		}
		
		return $classes;
	}

	/**
	 * [Node] Get Node Title
	 *
	 * @return	string
	 */
	protected function get__title()
	{
		$key = "__app_{$this->directory}";
		return \IPS\Member::loggedIn()->language()->addToStack( $key );
	}
	
	/**
	 * [Node] Get Node Icon
	 *
	 * @return	string
	 */
	protected function get__icon()
	{
		return 'cubes';
	}
			
	/**
	 * [Node] Does this node have children?
	 *
	 * @param	string|NULL			$permissionCheck	The permission key to check for or NULl to not check permissions
	 * @param	\IPS\Member|NULL	$member				The member to check permissions for or NULL for the currently logged in member
	 * @param	bool				$subnodes			Include subnodes?
	 * @param	array				$_where				Additional WHERE clause
	 * @return	bool
	 */
	public function hasChildren( $permissionCheck='view', $member=NULL, $subnodes=TRUE, $_where=array() )
	{
		return $subnodes;
	}

	/**
	 * [Node] Does the currently logged in user have permission to delete this node?
	 *
	 * @return	bool
	 */
	public function canDelete()
	{
		if( !static::restrictionCheck( 'delete' ) )
		{
			return FALSE;
		}

		if( $this->_data['protected'] )
		{
			return FALSE;
		}
		else
		{
			return TRUE;
		}
	}

	/**
	 * @brief	Cached URL
	 */
	protected $_url	= NULL;

	/**
	 * Get URL
	 *
	 * @return	\IPS\Http\Url
	 */
	public function url()
	{
		if( $this->_url === NULL )
		{
			$this->_url = \IPS\Http\Url::internal( "app={$this->directory}" );
		}

		return $this->_url;
	}

	/**
	 * [Node] Get buttons to display in tree
	 * Example code explains return value
	 *
	 * @code
	 	array(
	 		array(
	 			'icon'	=>	array(
	 				'icon.png'			// Path to icon
	 				'core'				// Application icon belongs to
	 			),
	 			'title'	=> 'foo',		// Language key to use for button's title parameter
	 			'link'	=> \IPS\Http\Url::internal( 'app=foo...' )	// URI to link to
	 			'class'	=> 'modalLink'	// CSS Class to use on link (Optional)
	 		),
	 		...							// Additional buttons
	 	);
	 * @endcode
	 * @param	string	$url	Base URL
	 * @param	bool	$subnode	Is this a subnode?
	 * @return	array
	 */
	public function getButtons( $url, $subnode=FALSE )
	{
		/* Get normal buttons */
		$buttons	= parent::getButtons( $url );
		$edit = NULL;
		$uninstall = NULL;
		if( \IPS\IN_DEV and isset( $buttons['edit'] ) )
		{
			$edit = $buttons['edit'];
		}
		unset( $buttons['edit'] );
		unset( $buttons['copy'] );
		if( isset( $buttons['delete'] ) )
		{
			$buttons['delete']['title']	= 'uninstall';
			$buttons['delete']['data']	= array( 'delete' => '' );
			
			$uninstall = $buttons['delete'];
			unset( $buttons['delete'] );
		}
		
		/* Default */
		if( $this->directory != 'core' and $this->enabled )
		{
			$buttons['default']	= array(
				'icon'		=> $this->default ? 'star' : 'star-o',
				'title'		=> 'make_default_app',
				'link'		=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=setAsDefault" ),
			);
		}
		
		/* Online/offline */
		if( !$this->protected )
		{
			$buttons['offline']	= array(
				'icon'	=> 'lock', 
				'title'	=> 'permissions',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&id={$this->_id}&do=permissions" ),
				'data'	=> array( 'ipsDialog' => '', 'ipsDialog-forceReload' => 'true', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('permissions') )
			);
		}
		
		/* View Details */
		$buttons['details']	= array(
			'icon'	=> 'search',
			'title'	=> 'app_view_details',
			'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&do=details&id={$this->_id}" ),
			'data'	=> array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('app_view_details') )
		);
		
		/* Upgrade */
		if( !$this->protected )
		{
			$buttons['upgrade']	= array(
				'icon'	=> 'upload',
				'title'	=> 'upload_new_version',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=upload" ),
				'data'	=> array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('upload_new_version') )
			);
		}
		
		/* Uninstall */
		if ( $uninstall )
		{
			$buttons['delete'] = $uninstall;
			if ( $this->default )
			{
				$buttons['delete']['data'] = array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('uninstall') );
			}
		}
				
		/* Developer */
		if( \IPS\IN_DEV )
		{			
			if ( $edit )
			{
				$buttons['edit'] = $edit;
			}
			
			$buttons['compilejs'] = array(
				'icon'	=> 'cog',
				'title'	=> 'app_compile_js',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=compilejs" )
			);
			
			$buttons['build'] = array(
				'icon'	=> 'cog',
				'title'	=> 'app_build',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=build" )
			);
			
			$buttons['export']	= array(
				'icon'	=> 'download',
				'title'	=> 'download',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=download" ),
				'data'	=> array(
					'controller'	=> 'core.admin.system.buildApp',
					'downloadURL'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=download&type=download" ),
					'buildURL'		=> \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=download&type=build" ),
				)
			);

			$buttons['developer']	= array(
				'icon'	=> 'cogs',
				'title'	=> 'developer_mode',
				'link'	=> \IPS\Http\Url::internal( "app=core&module=applications&controller=developer&appKey={$this->_id}" ),
			);
		}
		
		return $buttons;
	}
	
	/**
	 * [Node] Get whether or not this node is enabled
	 *
	 * @note	Return value NULL indicates the node cannot be enabled/disabled
	 * @return	bool|null
	 */
	protected function get__enabled()
	{
		if ( $this->directory == 'core' )
		{
			return TRUE;
		}
		return $this->enabled and ( !in_array( $this->directory, static::$ipsApps ) or $this->long_version == \IPS\Application::load('core')->long_version );
	}

	/**
	 * [Node] Set whether or not this node is enabled
	 *
	 * @param	bool|int	$enabled	Whether to set it enabled or disabled
	 * @return	void
	 */
	protected function set__enabled( $enabled )
	{
		$this->enabled = $enabled;
		$this->save();
		\IPS\Plugin\Hook::writeDataFile();
	}
	
	/**
	 * [Node] Get whether or not this node is locked to current enabled/disabled status
	 *
	 * @note	Return value NULL indicates the node cannot be enabled/disabled
	 * @return	bool|null
	 */
	protected function get__locked()
	{
		if ( $this->directory == 'core' )
		{
			return TRUE;
		}
		if ( !$this->_enabled and in_array( $this->directory, static::$ipsApps ) and $this->long_version != \IPS\Application::load('core')->long_version )
		{
			return TRUE;
		}
		return FALSE;
	}
	
	/**
	 * [Node] Get Node Description
	 *
	 * @return	string|null
	 */
	protected function get__description()
	{
		if ( $this->_locked and $this->directory != 'core' )
		{
			return \IPS\Member::loggedIn()->language()->addToStack('app_force_disabled');
		}
		elseif ( $this->disabled_groups )
		{
			$groups = array();
			if ( $this->disabled_groups != '*' )
			{				
				foreach ( explode( ',', $this->disabled_groups ) as $groupId )
				{
					try
					{
						$groups[] = \IPS\Member\Group::load( $groupId )->name;
					}
					catch ( \OutOfRangeException $e ) { }
				}
			}
			
			if ( empty( $groups ) )
			{
				return \IPS\Member::loggedIn()->language()->addToStack('app_offline_to_all');
			}
			else
			{
				return \IPS\Member::loggedIn()->language()->addToStack( 'app_offline_to_groups', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $groups ) ) ) );
			}
		}
		return NULL;
	}

	/**
	 * Return the custom badge for each row
	 *
	 * @return	NULL|array		Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
	 */
	public function get__badge()
	{
		if( $this->update_version )
		{
			$data	= json_decode( $this->update_version, TRUE );
			
			if( !empty($data['longversion']) AND $data['longversion'] > $this->long_version )
			{
				$released	= NULL;

				if( $data['released'] AND intval($data['released']) == $data['released'] AND \strlen($data['released']) == 10 )
				{
					$released	= (string) \IPS\DateTime::ts( $data['released'] )->localeDate();
				}
				else if( $data['released'] )
				{
					$released	= $data['released'];
				}

				return array(
					0	=> 'new',
					1	=> '',
					2	=> \IPS\Theme::i()->getTemplate( 'global', 'core' )->updatebadge( $data['version'], $data['updateurl'], $released )
				);
			}
		}

		return NULL;
	}

	/**
	 * [Node] Does the currently logged in user have permission to add a child node?
	 *
	 * @return	bool
	 * @note	Modules are added via the developer center and should not be added by a regular admin via the standard node controller
	 */
	public function canAdd()
	{
		return false;
	}

	/**
	 * [Node] Does the currently logged in user have permission to add aa root node?
	 *
	 * @return	bool
	 * @note	If IN_DEV is on, the admin can create a new application
	 */
	public static function canAddRoot()
	{
		return ( \IPS\IN_DEV ) ? true : false;
	}
	
	/**
	 * [Node] Does the currently logged in user have permission to edit permissions for this node?
	 *
	 * @return	bool
	 * @note	We don't allow permissions to be set for applications - they are handled by modules and by the enabled/disabled mode
	 */
	public function canManagePermissions()
	{
		return false;
	}
	
	/**
	 * Add or edit an application
	 *
	 * @param	\IPS\Helpers\Form	$form	Form object we can add our fields to
	 * @return	void
	 */
	public function form( &$form )
	{
		if ( !$this->directory )
		{
			$form->add( new \IPS\Helpers\Form\Text( 'app_title', NULL, FALSE, array( 'app' => 'core', 'key' => ( !$this->directory ) ? NULL : "__app_{$this->directory}" ) ) );
		}

		$form->add( new \IPS\Helpers\Form\Text( 'app_directory', $this->directory, TRUE, array( 'disabled' => $this->id ? TRUE : FALSE, 'regex' => '/^[a-zA-Z][a-zA-Z0-9]+$/' ) ) );
		$form->add( new \IPS\Helpers\Form\Text( 'app_author', $this->author ) );
		$form->add( new \IPS\Helpers\Form\Url( 'app_website', $this->website ) );
		$form->add( new \IPS\Helpers\Form\Url( 'app_update_check', $this->update_check ) );
		$form->add( new \IPS\Helpers\Form\YesNo( 'app_protected', $this->protected, FALSE ) );

		if( !$this->id )
		{
			$form->add( new \IPS\Helpers\Form\Custom( 'app_versions', array(), TRUE, array( 'getHtml' => function( $element )
			{
				return \IPS\Theme::i()->getTemplate( 'applications' )->versionFormField();
			} ), NULL, NULL, NULL, 'app_versions' ) );
		}
	}

	/**
	 * [Node] Format form values from add/edit form for save
	 *
	 * @param	array	$values	Values from the form
	 * @return	array
	 */
	public function formatFormValues( $values )
	{
		/* New application stuff */
		if ( !$this->id )
		{
			/* Check dir is writable */
			if( !is_writable( \IPS\ROOT_PATH . '/applications/' ) )
			{
				\IPS\Output::i()->error( 'app_dir_not_write', '4S134/2', 403, '' );
			}
			
			/* Check key isn't in use */
			$values['app_directory'] = mb_strtolower( $values['app_directory'] );
			try
			{
				$test = \IPS\Application::load( $values['app_directory'] );
				\IPS\Output::i()->error( 'app_error_key_used', '1S134/1', 403, '' );
			}
			catch ( \OutOfRangeException $e ) { }
			
			/* Attempt to create the basic directory structure for the developer */
			if( is_writable( \IPS\ROOT_PATH . '/applications/' ) )
			{
				/* If we can make the root dir, we can create the subfolders */
				if( @mkdir( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] ) )
				{
					@chmod( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'], \IPS\FOLDER_PERMISSION_NO_WRITE );

					/* Create directories */
					foreach ( array( 'data', 'dev', 'dev/css', 'dev/email', 'dev/html', 'dev/resources', 'dev/js', 'extensions', 'extensions/core', 'hooks', 'interface', 'modules', 'modules/admin', 'modules/front', 'setup', '/setup/upg_' . $values['app_versions'][1], 'sources', 'tasks' ) as $f )
					{
						@mkdir( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f );
						@chmod( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f, \IPS\FOLDER_PERMISSION_NO_WRITE );
						\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/' . $f . '/index.html', '' );
					}
										
					/* Create files */
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/schema.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/settings.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/tasks.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/themesettings.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/acpmenu.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/modules.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/widgets.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/acpsearch.json', '{}' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/hooks.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/setup/upg_' . $values['app_versions'][1] . '/queries.json', '[]' );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/versions.json', json_encode( array( $values['app_versions'][1] => $values['app_versions'][0] ) ) );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/dev/lang.php', '<?' . "php\n\n\$lang = array(\n\t'__app_{$values['app_directory']}'\t=> \"{$values['app_title']}\"\n);\n" );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/dev/jslang.php', '<?' . "php\n\n\$lang = array(\n\n);\n" );
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/Application.php', str_replace(
						array(
							'{app}',
							'{website}',
							'{author}',
							'{year}',
							'{subpackage}',
							'{date}'
						),
						array(
							$values['app_directory'],
							$values['app_website'],
							$values['app_author'],
							date('Y'),
							$values['app_title'],
							date( 'd M Y' ),
						),
						file_get_contents( \IPS\ROOT_PATH . "/applications/core/data/defaults/Application.txt" )
					) );
	
					@\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $values['app_directory'] . '/data/application.json', json_encode( array(
						'application_title'	=> $values['app_title'],
						'app_author'		=> $values['app_author'],
						'app_directory'		=> $values['app_directory'],
						'app_protected'		=> $values['app_protected'],
						'app_website'		=> $values['app_website'],
						'app_update_check'	=> $values['app_update_check'],
					) ) );
				}
			}
			
			/* Enable it */
			$values['enabled']		= TRUE;
			$values['app_added']	= time();
		}

		/* Save it */
		if( isset( $values['app_versions'] ) )
		{
			$values['app_version'] = $values['app_versions'][0];
			$values['app_long_version'] = $values['app_versions'][1];
			unset( $values['app_versions'] );
		}

		if( isset( $values['app_title'] ) )
		{
			unset( $values['app_title'] );
		}

		return $values;
	}

	/**
	 * [Node] Perform actions after saving the form
	 *
	 * @param	array	$values	Values from the form
	 * @return	void
	 */
	public function postSaveForm( $values )
	{
		/* Clear out member's cached "Create Menu" contents */
		\IPS\Member::clearCreateMenu();
		unset( \IPS\Data\Store::i()->applications );
		unset( \IPS\Data\Store::i()->settings );
	}

	/**
	 * Install database changes from the schema.json file
	 *
	 * @param	bool	$skipInserts	Skip inserts
	 * @throws \Exception
	 */
	public function installDatabaseSchema( $skipInserts=FALSE )
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ) )
		{
			$schema	= json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ), TRUE );

			foreach( $schema as $table => $definition )
			{
				/* Look for missing tables first */
				if( !\IPS\DB::i()->checkForTable( $table ) )
				{
					\IPS\DB::i()->createTable( $definition );
				}
				else
				{
					/* If the table exists, look for missing columns */
					if( is_array( $definition['columns'] ) AND count( $definition['columns'] ) )
					{
						/* Get the table definition first */
						$tableDefinition = \IPS\DB::i()->getTableDefinition( $table );

						foreach( $definition['columns'] as $column )
						{
							/* Column does not exist in the table definition?  Add it then. */
							if( empty($tableDefinition['columns'][ $column['name'] ]) )
							{
								\IPS\DB::i()->addColumn( $table, $column );
							}
						}
					}
				}

				if ( isset( $definition['inserts'] ) AND !$skipInserts )
				{
					foreach ( $definition['inserts'] as $insertData )
					{
						$adminName = \IPS\Member::loggedIn()->name;
						try
						{
							\IPS\Db::i()->insert( $definition['name'], array_map( function( $column ) use( $adminName ) {
	                              if( !is_string( $column ) )
	                              {
	                                  return $column;
	                              }

	                              $column = str_replace( '<%TIME%>', time(), $column );
	                              $column = str_replace( '<%ADMIN_NAME%>', $adminName, $column );
	                              $column = str_replace( '<%IP_ADDRESS%>', $_SERVER['REMOTE_ADDR'], $column );
	                              return $column;
	                          }, $insertData ) );
						}
						catch( \IPS\Db\Exception $e )
						{}
					}
				}
			}
		}
		
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ) )
		{
			$schema	= json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/install/queries.json" ), TRUE );

			ksort($schema);

			foreach( $schema as $instruction )
			{
				if ( $instruction['method'] === 'addColumn' )
				{
					/* Check to see if it exists first */
					$tableDefinition = \IPS\DB::i()->getTableDefinition( $instruction['params'][0] );
					
					if ( ! empty( $tableDefinition['columns'][ $instruction['params'][1]['name'] ] ) )
					{
						/* Run an alter instead */
						\IPS\Db::i()->changeColumn( $instruction['params'][0], $instruction['params'][1]['name'], $instruction['params'][1] );
						continue;
					}
				}
				
				try
				{
					if( isset( $instruction['params'][1] ) and is_array( $instruction['params'][1] ) )
					{
						$groups	= array_filter( iterator_to_array( \IPS\Db::i()->select( 'g_id', 'core_groups' ) ), function( $groupId ) {
							if( $groupId == 2 )
							{
								return FALSE;
							}

							return TRUE;
						});

						foreach( $instruction['params'][1] as $column => $value )
						{
							if( $value === "<%NO_GUESTS%>" )
							{
								$instruction['params'][1][ $column ]	= implode( ",", $groups );
							}
						}
					}

					call_user_func_array( array( \IPS\DB::i(), $instruction['method'] ), $instruction['params'] );
				}
				catch( \Exception $e )
				{
					if( $instruction['method'] == 'insert' )
					{
						return;
					}

					throw $e;
				}
			}
		}
	}

	/**
	 * Install database changes from an upgrade schema file
	 *
	 * @param	int		$version		Version to execute database updates from
	 * @param	int		$lastJsonIndex	JSON index to begin from
	 * @param	int		$limit			Limit updates
	 * @param	bool	$return			Check table size first and return queries for larger tables instead of running automatically
	 * @return	array					Returns an array: ( count: count of queries run, queriesToRun: array of queries to run)
	 * @note	We ignore some database errors that shouldn't prevent us from continuing.
	 * @li	1007: Can't create database because it already exists
	 * @li	1008: Can't drop database because it does not exist
	 * @li	1050: Can't rename a table as it already exists
	 * @li	1060: Can't add a column as it already exists
	 * @li	1062: Can't add an index as index already exists
	 * @li	1062: Can't add a row as PKEY already exists
	 * @li	1091: Can't drop key or column because it does not exist
	 */
	public function installDatabaseUpdates( $version=0, $lastJsonIndex=0, $limit=50, $return=FALSE )
	{
		$toReturn    = array();
		$tableCounts = array();
		$count  = 0;

		/* Try to prevent timeouts to the extent possible */
		$cutOff			= null;

		if( $maxExecution = @ini_get( 'max_execution_time' ) )
		{
			$cutOff	= time() + ( $maxExecution * .5 );
		}

		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/upg_{$version}/queries.json" ) )
		{
			$schema	= json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/setup/upg_{$version}/queries.json" ), TRUE );
			
			ksort($schema, SORT_NUMERIC);

			foreach( $schema as $jsonIndex => $instruction['params'] )
			{
				if ( $lastJsonIndex AND ( $jsonIndex <= $lastJsonIndex ) )
				{
					continue;
				}
				
				if ( $count >= $limit )
				{
					return array( 'count' => $count, 'queriesToRun' => $toReturn );
				}
				else if( $cutOff !== null AND time() >= $cutOff )
				{
					return array( 'count' => $count, 'queriesToRun' => $toReturn );
				}
				
				$_SESSION['lastJsonIndex'] = $jsonIndex;
				
				$count++;

				$_table	= $instruction['params']['params'][0];

				if ( ! is_string( $_table ) )
				{
					$_table	= $instruction['params']['params'][0]['name'];
				}
				
				if ( ! isset( $tableCounts[ $_table ] ) and \IPS\Db::i()->checkForTable( $_table ) )
				{
					$tableCounts[ $_table ] = \IPS\Db::i()->select( 'count(*)', $_table )->first();
				}

				/* Check table size first and store query if requested */
				if( $return === TRUE )
				{
					if( isset( $tableCounts[ $_table ] ) AND $tableCounts[ $_table ] > \IPS\UPGRADE_MANUAL_THRESHOLD AND !in_array( $instruction['params']['method'], array( 'dropTable', 'insert', 'renameTable' ) ) )
					{
						\IPS\Log::i( LOG_DEBUG )->write( "Big table " . $_table . ", storing query to run manually", 'upgrade' );

						\IPS\DB::i()->returnQuery = TRUE;
						$query = call_user_func_array( array( \IPS\DB::i(), $instruction['params']['method'] ), $instruction['params']['params'] );

						if( $query )
						{
							$toReturn[] = $query;

							if ( $instruction['params']['method'] == 'renameTable' )
							{
								$tableCounts[ $instruction['params']['params'][1] ] = $tableCounts[ $_table ];

								foreach( $toReturn as $k => $v )
								{
									$toReturn[ $k ]	= preg_replace( "/\`" . \IPS\Db::i()->prefix . $_table . "\`/", "`" . \IPS\Db::i()->prefix . $instruction['params']['params'][1] . "`", $v );
								}
							}

							return array( 'count' => $count, 'queriesToRun' => $toReturn );
						}
					}
				}

				try
				{
					call_user_func_array( array( \IPS\DB::i(), $instruction['params']['method'] ), $instruction['params']['params'] );
				}
				catch( \IPS\Db\Exception $e )
				{

					/* If the issue is with a create table other than exists, we should just throw it */
					if ( $instruction['params']['method'] == 'createTable' and ! in_array( $e->getCode(), array( 1007, 1050 ) ) )
					{
						throw $e;
					}
					
					/* Can't change a column as it doesn't exist */
					if ( $e->getCode() == 1054 )
					{
						if ( $instruction['params']['method'] == 'changeColumn' )
						{
							if ( \IPS\Db::i()->checkForTable( $instruction['params']['params'][0] ) )
							{
								/* Does the column exist already? */
								if ( \IPS\DB::i()->checkForColumn( $instruction['params']['params'][0], $instruction['params']['params'][2]['name'] ) )
								{
									/* Just make sure it's up to date */
									\IPS\DB::i()->changeColumn( $instruction['params']['params'][0], $instruction['params']['params'][2]['name'], $instruction['params']['params'][2] );
									continue;
								}
								else
								{
									/* The table exists, so lets just add the column */
									\IPS\DB::i()->addColumn( $instruction['params']['params'][0], $instruction['params']['params'][2] );
								
									continue;
								}
							}
						}
						
						throw $e;
					}
					/* Can't rename a table as it doesn't exist */
					else if ( $e->getCode() == 1017 )
					{
						if ( $instruction['params']['method'] == 'renameTable' )
						{
							if ( \IPS\Db::i()->checkForTable( $instruction['params']['params'][1] ) )
							{
								/* The table we are renaming to *does* exist */
								continue;
							}
						}
						
						throw $e;
					}
					/* Possibly trying to change a column to not null that has NULL values */
					else if ( $e->getCode() == 1138 )
					{
						if ( $instruction['params']['method'] == 'changeColumn' and ! $instruction['params']['params'][2]['allow_null'] )
						{
							$currentDefintion = \IPS\Db::i()->getTableDefinition( $instruction['params']['params'][1] );
							$column = $currentDefintion[ $instruction['params']['params'][2]['name'] ];
							
							if ( isset( $currentDefintion['columns'][ $column ] ) AND $currentDefintion['columns'][ $column ]['allow_null'] )
							{
								\IPS\Db::i()->update( $instruction['params']['params'][1], array( $column => '' ), array( $column . ' IS NULL' ) );
								
								/* Just make sure it's up to date */
								\IPS\DB::i()->changeColumn( $instruction['params']['params'][0], $instruction['params']['params'][2]['name'], $instruction['params']['params'][2] );
								
								continue;
							}
						}
						
						throw $e;
					}
					/* If the error isn't important we should ignore it */
					else if( !in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1091 ) ) )
					{
						throw $e;
					}
				}
			}
		}

		return array( 'count' => $count, 'queriesToRun' => $toReturn );
	}

	/**
	 * Rebuild common data during an install or upgrade. This is a shortcut method which
	 * * Installs module data from JSON file
	 * * Installs task data from JSON file
	 * * Installs setting data from JSON file
	 * * Installs ACP live search keywords from JSON file
	 * * Installs hooks from JSON file
	 * * Updates latest version in the database
	 *
	 * @param	bool	$skipMember		Skip clearing member cache clearing
	 * @return void
	 */
	public function installJsonData( $skipMember=FALSE )
	{
		/* Rebuild modules */
		$this->installModules();

		/* Rebuild tasks */
		$this->installTasks();

		/* Rebuild settings */
		$this->installSettings();
		
		/* Rebuild sidebar widgets */
		$this->installWidgets();

		/* Rebuild search keywords */
		$this->installSearchKeywords();
		
		/* Rebuild hooks */
		$this->installHooks();

		/* Update app version data */
		$versions		= $this->getAllVersions();
		$longVersions	= array_keys( $versions );
		$humanVersions	= array_values( $versions );

		if( count($versions) )
		{
			$latestLVersion	= array_pop( $longVersions );
			$latestHVersion	= array_pop( $humanVersions );

			\IPS\Db::i()->update( 'core_applications', array( 'app_version' => $latestHVersion, 'app_long_version' => $latestLVersion ), array( 'app_directory=?', $this->directory ) );
		}

		unset( \IPS\Data\Store::i()->applications );

		if( !$skipMember )
		{
			\IPS\Member::clearCreateMenu();
		}
	}

	/**
	 * Install the application's modules
	 *
	 * @return	void
	 */
	public function installModules()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ) )
		{
			$currentModules	= array();
			$moduleStore = array();

			foreach ( \IPS\Db::i()->select( '*', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) as $row )
			{
				$currentModules[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = array(
					'default_controller'	=> $row['sys_module_default_controller'],
					'protected'				=> $row['sys_module_protected']
				);
				$moduleStore[ $row['sys_module_area'] ][ $row['sys_module_key'] ] = $row;
			}
			
			$insert	= array();
			$update	= array();

			$position = 0;
			foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/modules.json" ), TRUE ) as $area => $modules )
			{
				foreach ( $modules as $key => $data )
				{
					if ( !isset( $currentModules[ $area ][ $key ] ) )
					{
						$module = new \IPS\Application\Module;
					}
					elseif ( $currentModules[ $area ][ $key ] != $data )
					{
						$module = \IPS\Application\Module::constructFromData( $moduleStore[ $area ][ $key ] );
					}
					else
					{
						continue;
					}

					$module->application = $this->directory;
					$module->key = $key;
					$module->protected = intval( $data['protected'] );
					$module->visible = TRUE;
					$module->position = ++$position;
					$module->area = $area;
					$module->default_controller = $data['default_controller'];
					$module->save( TRUE );
				}
			}
		}
	}

	/**
	 * Install the application's tasks
	 *
	 * @return	void
	 */
	public function installTasks()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/tasks.json" ) )
		{
			$currentTasks = iterator_to_array( \IPS\Db::i()->select( '*', 'core_tasks', array( 'app=?', $this->directory ) )->setKeyField( 'key' )->setValueField( 'frequency' ) );

			foreach ( array_diff_key( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/tasks.json" ), TRUE ), $currentTasks ) as $key => $frequency )
			{
				\IPS\Db::i()->replace( 'core_tasks', array(
					'app'		=> $this->directory,
					'key'		=> $key,
					'frequency'	=> $frequency,
				) );
			}
		}
	}
	
	/**
	 * Install the application's extension data where required
	 *
	 * @return	void
	 */
	public function installExtensions()
	{
		/* File storage */
		$settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
		
		try
		{
			$fileSystem = \IPS\Db::i()->select( '*', 'core_file_storage', array( 'method=?', 'FileSystem' ), 'id ASC' )->first();
		}
		catch( \UnderflowException $ex )
		{
			$fileSystem = \IPS\Db::i()->select( '*', 'core_file_storage', NULL, 'id ASC' )->first();
		}
		
		foreach( $this->extensions( 'core', 'FileStorage' ) as $key => $path )
		{
			$settings[ 'filestorage__' . $this->directory . '_' . $key ] = $fileSystem['id'];
		}
		
		\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => json_encode( $settings ) ), array( 'conf_key=?', 'upload_settings' ) );
		\IPS\Settings::i()->upload_settings = json_encode( $settings );
		unset( \IPS\Data\Store::i()->settings );
		
		$inserts = array();
		foreach( $this->extensions( 'core', 'Notifications' ) as $key => $class )
		{
			if ( method_exists( $class, 'getConfiguration' ) )
			{
				$defaults = $class->getConfiguration( NULL );
				
				foreach( $defaults AS $k => $config )
				{
					$inserts[] = array(
						'notification_key'	=> $k,
						'default'			=> implode( ',', $config['default'] ),
						'disabled'			=> implode( ',', $config['disabled'] ),
					);
				}
			}
		}
		
		if( count( $inserts ) )
		{
			\IPS\Db::i()->insert( 'core_notification_defaults', $inserts );
		}
	}

	/**
	 * Install the application's settings
	 *
	 * @return	void
	 */
	public function installSettings()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/settings.json" ) )
		{
			$currentDefaults = iterator_to_array( \IPS\Db::i()->select( '*', 'core_sys_conf_settings' )->setKeyField('conf_key')->setValueField('conf_default') );

			$insert	= array();
			$update	= array();

			foreach ( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/settings.json" ), TRUE ) as $setting )
			{
				if ( ! array_key_exists( $setting['key'], $currentDefaults ) )
				{
					$insert[]	= array( 'conf_key' => $setting['key'], 'conf_value' => $setting['default'], 'conf_default' => $setting['default'], 'conf_app' => $this->directory );
				}
				elseif ( $currentDefaults[ $setting['key'] ] != $setting['default'] )
				{
					$update[]	= array( array( 'conf_default' => $setting['default'] ), array( 'conf_key=?', $setting['key'] ) );
				}
			}

			if ( !empty( $insert ) )
			{
				\IPS\Db::i()->insert( 'core_sys_conf_settings', $insert );
			}
			
			foreach ( $update as $data )
			{
				\IPS\Db::i()->update( 'core_sys_conf_settings', $data[0], $data[1] );
			}
			
			unset( \IPS\Data\Store::i()->settings );
		}
	}

	/**
	 * Install the application's language strings
	 *
	 * @param	int|null		$offset Offset to begin import from
	 * @param	int|null		$limit	Number of rows to import
	 * @return	int				Rows inserted
	 */
	public function installLanguages( $offset=null, $limit=null )
	{
		$languages	= array_keys( \IPS\Lang::languages() );
		$inserted	= 0;
		
		$current = array();
		foreach( $languages as $languageId )
		{
			foreach( iterator_to_array( \IPS\Db::i()->select( 'word_key, word_default, word_js', 'core_sys_lang_words', array( 'word_app=? AND lang_id=?', $this->directory, $languageId ) ) ) as $word )
			{
				$current[ $languageId ][ $word['word_key'] . '-.-' . $word['word_js'] ] = $word['word_default'];
			}
		}

		if ( !$offset and file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/installLang.json" ) )
		{
			$inserts = array();
			foreach ( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/installLang.json" ), TRUE ) as $key => $default )
			{
				foreach( $languages as $languageId )
				{
					if ( !isset( $current[ $languageId ][ $key . '-.-0' ] ) )
					{
						$inserts[]	= array(
							'word_app'				=> $this->directory,
							'word_key'				=> $key,
							'lang_id'				=> $languageId,
							'word_default'			=> $default,
							'word_custom'			=> $default,
							'word_default_version'	=> $this->long_version,
							'word_custom_version'	=> $this->long_version,
							'word_js'				=> 0,
							'word_export'			=> 0,
						);
					}
				}
			}
			
			if ( count( $inserts ) )
			{
				\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
			}
		}
		
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/lang.xml" ) )
		{			
			/* Open XML file */
			$xml = new \XMLReader;
			$xml->open( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/lang.xml" );
			$xml->read();

			/* Get the version */
			$xml->read();
			$xml->read();
			$version	= $xml->getAttribute('version');

			/* Get all installed languages */
			$inserts	 = array();
			$batchSize   = 25;
			$batchesDone = 0;
			$i           = 0;
			
			/* Try to prevent timeouts to the extent possible */
			$cutOff			= null;

			if( $maxExecution = @ini_get( 'max_execution_time' ) )
			{
				$cutOff	= time() + ( $maxExecution * .5 );
			}

			/* Start looping through each word */
			while ( $xml->read() )
			{
				if( $xml->name != 'word' OR $xml->nodeType != \XMLReader::ELEMENT )
				{
					continue;
				}

				if( $cutOff !== null AND time() >= $cutOff )
				{
					return $inserted;
				}
				
				$i++;
				
				if ( $offset !== null )
				{
					if ( $i - 1 < $offset )
					{
						$xml->next();
						continue;
					}
				}

				$inserted++;
				
				$key = $xml->getAttribute('key');
				$value = $xml->readString();
				foreach( $languages as $languageId )
				{
					if ( !isset( $current[ $languageId ][ $key . '-.-' . (int) $xml->getAttribute('js') ] ) or $current[ $languageId ][ $key . '-.-' . (int) $xml->getAttribute('js') ] != $value )
					{
						$inserts[]	= array(
							'word_app'				=> $this->directory,
							'word_key'				=> $key,
							'lang_id'				=> $languageId,
							'word_default'			=> $value,
							'word_default_version'	=> $version,
							'word_js'				=> (int) $xml->getAttribute('js'),
							'word_export'			=> 1,
						);
					}
				}
				
				$done = ( $limit !== null AND $i === ( $limit + $offset ) );
				
				if ( $done OR $i % $batchSize === 0 )
				{
					if ( count( $inserts ) )
					{
						\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
						$inserts = array();
					}
					$batchesDone++;
				}
				
				if ( $done )
				{
					break;
				}
				
				$xml->next();
			}
			
			if ( count( $inserts ) )
			{
				\IPS\Db::i()->insert( 'core_sys_lang_words', $inserts, TRUE );
			}
		}

		return $inserted;
	}

	/**
	 * Install the application's email templates
	 *
	 * @return	void
	 */
	public function installEmailTemplates()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/emails.xml" ) )
		{
			/* First, delete any existing email templates for this app. Even customized templates are associated by ID and will be deleted. */
			\IPS\DB::i()->delete( 'core_email_templates', array( 'template_app=?', $this->directory ) );

			/* Open XML file */
			$xml = new \XMLReader;
			$xml->open( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/emails.xml" );
			$xml->read();

			/* Start looping through each word */
			while ( $xml->read() and $xml->name == 'template' )
			{
				if( $xml->nodeType != \XMLReader::ELEMENT )
				{
					continue;
				}

				$insert	= array(
					'template_parent'	=> 0,
					'template_app'		=> $this->directory,
					'template_edited'	=> 0,
				);

				while ( $xml->read() and $xml->name != 'template' )
				{
					if( $xml->nodeType != \XMLReader::ELEMENT )
					{
						continue;
					}

					switch( $xml->name )
					{
						case 'template_name':
							$insert['template_name']				= $xml->readString();
							$insert['template_key']					= md5( $this->directory . ';' . $insert['template_name'] );
						break;

						case 'template_data':
							$insert['template_data']				= $xml->readString();
						break;

						case 'template_content_html':
							$insert['template_content_html']		= $xml->readString();
						break;

						case 'template_content_plaintext':
							$insert['template_content_plaintext']	= $xml->readString();
						break;
					}
				}

				\IPS\Db::i()->replace( 'core_email_templates', $insert );
			}

			\IPS\Data\Cache::i()->clearAll();
			\IPS\Data\Store::i()->clearAll();
		}
	}

	/**
	 * Install the application's skin templates, CSS files and resources
	 *
	 * @param	bool	$update		If set to true, do not overwrite current theme setting values
	 * @return	void
	 */
	public function installSkins( $update=FALSE )
	{
		/* Clear old caches */
		\IPS\Data\Cache::i()->clearAll();
		\IPS\Data\Store::i()->clearAll();

		/* Install the stuff */
		$this->installThemeSettings( $update );
		$this->clearTemplates();
		$this->installTemplates( $update );
	}

	/**
	 * Install the application's theme settings
	 *
	 * @param	bool	$update		If set to true, do not overwrite current theme setting values
	 * @return	void
	 */
	public function installThemeSettings( $update=FALSE )
	{
		if ( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/themesettings.json" ) )
		{
			unset( \IPS\Data\Store::i()->themes );
			$currentSettings	= iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=? AND sc_app=?', \IPS\Theme::defaultTheme(), $this->directory ) )->setKeyField('sc_key') );
			$json				= json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/themesettings.json" ), TRUE );
			
			/* Add */
			foreach( $json as $key => $data)
			{
				$insertedSetting = FALSE;
				
				if ( ! isset( $currentSettings[ $data['sc_key'] ] ) )
				{
					$insertedSetting = TRUE;
					
					$currentId = \IPS\Db::i()->insert( 'core_theme_settings_fields', array(
						'sc_set_id'		 => \IPS\Theme::defaultTheme(),
						'sc_key'		 => $data['sc_key'],
						'sc_tab_key'	 => $data['sc_tab_key'],
						'sc_type'		 => $data['sc_type'],
						'sc_multiple'	 => $data['sc_multiple'],
						'sc_default'	 => $data['sc_default'],
						'sc_content'	 => $data['sc_content'],
						'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
						'sc_updated'	 => time(),
						'sc_app'		 => $this->directory,
						'sc_title'		 => $data['sc_title'],
						'sc_order'		 => $data['sc_order']
					) );
				}
				else
				{
					/* Update */
					\IPS\Db::i()->update( 'core_theme_settings_fields', array(
						'sc_tab_key'	 => $data['sc_tab_key'],
						'sc_type'		 => $data['sc_type'],
						'sc_multiple'	 => $data['sc_multiple'],
						'sc_default'	 => $data['sc_default'],
						'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
						'sc_content'	 => $data['sc_content'],
						'sc_title'		 => $data['sc_title'],
						'sc_order'		 => $data['sc_order']
					), array( 'sc_set_id=? AND sc_key=? AND sc_app=?', \IPS\Theme::defaultTheme(), $data['sc_key'], $this->directory ) );
			
					$currentId = $currentSettings[ $data['sc_key'] ]['sc_id'];
				}

				/* Are we updating the value? */
				if( $update === FALSE OR $insertedSetting === TRUE )
				{
					\IPS\Db::i()->delete('core_theme_settings_values', array('sv_id=?', $currentId ) );
					\IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $currentId, 'sv_value' => (string)$data['sc_default'] ) );
				}
			}
		
			if ( $update )
			{
				foreach( \IPS\Theme::themes() as $theme )
				{
					$currentSettings = iterator_to_array( \IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $theme->id ) )->setKeyField('sc_key') );
					$added           = FALSE;
					$save            = json_decode( $theme->template_settings, TRUE );

					/* Add */
					foreach( $json as $key => $data )
					{
						if ( ! isset( $currentSettings[ $data['sc_key'] ] ) )
						{
							$added = TRUE;
							$save[ $data['sc_key'] ] = $data['sc_default'];

							\IPS\Db::i()->insert( 'core_theme_settings_fields', array(
								'sc_set_id'		 => $theme->id,
								'sc_key'		 => $data['sc_key'],
								'sc_tab_key'	 => $data['sc_tab_key'],
								'sc_type'		 => $data['sc_type'],
								'sc_multiple'	 => $data['sc_multiple'],
								'sc_default'	 => $data['sc_default'],
								'sc_content'	 => $data['sc_content'],
								'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
								'sc_updated'	 => time(),
								'sc_app'		 => $this->directory,
								'sc_title'		 => $data['sc_title'],
								'sc_order'		 => $data['sc_order'],
							) );
						}
					}

					if ( $added )
					{
						$theme->template_settings = json_encode( $save );
						$theme->save();
					}
				}
			}
		}
	}

	/**
	 * Clear out existing templates before installing new ones
	 *	
	 * @return	void
	 */
	public function clearTemplates()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/theme.xml" ) )
		{
			unset( \IPS\Data\Store::i()->themes );
			\IPS\Theme::removeTemplates( $this->directory );
			\IPS\Theme::removeCss( $this->directory );
			\IPS\Theme::removeResources( $this->directory );
		}
	}

	/**
	 * Install the application's templates
	 * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
	 *
	 * @param	bool		$update	If set to true, do not overwrite current theme setting values
	 * @param	int|null	$offset Offset to begin import from
	 * @param	int|null	$limit	Number of rows to import	
	 * @return	int			Rows inserted
	 */
	public function installTemplates( $update=FALSE, $offset=null, $limit=null )
	{
		$i			= 0;
		$inserted	= 0;

		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/theme.xml" ) )
		{
			unset( \IPS\Data\Store::i()->themes );
			
			/* Try to prevent timeouts to the extent possible */
			$cutOff			= null;

			if( $maxExecution = @ini_get( 'max_execution_time' ) )
			{
				$cutOff	= time() + ( $maxExecution * .5 );
			}

			/* Open XML file */
			$xml = new \XMLReader;
			$xml->open( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/theme.xml" );
			$xml->read();

			while( $xml->read() )
			{
				if( $xml->nodeType != \XMLReader::ELEMENT )
				{
					continue;
				}

				if( $cutOff !== null AND time() >= $cutOff )
				{
					break;
				}

				$i++;

				if ( $offset !== null )
				{
					if ( $i - 1 < $offset )
					{
						$xml->next();
						continue;
					}
				}

				$inserted++;

				if( $xml->name == 'template' )
				{
					$template	= array(
						'app'		=> $this->directory,
						'group'		=> $xml->getAttribute('template_group'),
						'name'		=> $xml->getAttribute('template_name'),
						'variables'	=> $xml->getAttribute('template_data'),
						'content'	=> $xml->readString(),
						'location'	=> $xml->getAttribute('template_location'),
						'_default_template' => true
					);

					try
					{
						\IPS\Theme::addTemplate( $template );
					}
					catch( \OverflowException $e )
					{
						if ( ! $update )
						{
							throw $e;
						}
					}
				}
				else if( $xml->name == 'css' )
				{
					$css	= array(
						'app'		=> $this->directory,
						'location'	=> $xml->getAttribute('css_location'),
						'path'		=> $xml->getAttribute('css_path'),
						'name'		=> $xml->getAttribute('css_name'),
						'content'	=> $xml->readString(),
						'_default_template' => true
					);

					try
					{
						\IPS\Theme::addCss( $css );
					}
					catch( \OverflowException $e )
					{
						if( ! $update )
						{
							throw $e;
						}
					}
				}
				else if( $xml->name == 'resource' )
				{
					$resource	= array(
						'app'		=> $this->directory,
						'location'	=> $xml->getAttribute('location'),
						'path'		=> $xml->getAttribute('path'),
						'name'		=> $xml->getAttribute('name'),
						'content'	=> base64_decode( $xml->readString() ),
					);

					\IPS\Theme::addResource( $resource, TRUE );
				}

				if( $limit !== null AND $i === ( $limit + $offset ) )
				{
					break;
				}
			}
		}

		return $inserted;
	}
	
	/**
	 * Install the application's javascript
	 *
	 * @param	int|null	$offset Offset to begin import from
	 * @param	int|null	$limit	Number of rows to import	
	 * @return	int			Rows inserted
	 */
	public function installJavascript( $offset=null, $limit=null )
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/javascript.xml" ) )
		{
			return \IPS\Output\Javascript::importXml( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/javascript.xml", $offset, $limit );
		}
	}
	
	/**
	 * Install the application's ACP search keywords
	 *
	 * @return	void
	 */
	public function installSearchKeywords()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/acpsearch.json" ) )
		{
			\IPS\Db::i()->delete( 'core_acp_search_index', array( 'app=?', $this->directory ) );
			
			$inserts	= array();
			$maxInserts	= 50;

			foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/acpsearch.json" ), TRUE ) as $url => $data )
			{
				foreach ( $data['keywords'] as $word )
				{
					$inserts[] = array(
						'url'			=> $url,
						'keyword'		=> $word,
						'app'			=> $this->directory,
						'lang_key'		=> $data['lang_key'],
						'restriction'	=> $data['restriction'] ?: NULL
					);

					if( count( $inserts ) >= $maxInserts )
					{
						\IPS\Db::i()->insert( 'core_acp_search_index', $inserts );
						$inserts = array();
					}
				}
			}
			
			if( count( $inserts ) )
			{
				\IPS\Db::i()->insert( 'core_acp_search_index', $inserts );
			}
		}
	}
	
	/**
	 * Install hooks
	 *
	 * @return	void
	 */
	public function installHooks()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/hooks.json" ) )
		{
			\IPS\Db::i()->delete( 'core_hooks', array( 'app=?', $this->directory ) );
			
			$inserts = array();
			$templatesToRecompile = array();
			foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/hooks.json" ), TRUE ) as $filename => $data )
			{
				$inserts[] = array(
					'app'			=> $this->directory,
					'type'			=> $data['type'],
					'class'			=> $data['class'],
					'filename'		=> $filename
				);
				
				if ( $data['type'] === 'S' )
				{
					$templatesToRecompile[ $data['class'] ] = $data['class'];
				}
			}
			
			if( count( $inserts ) )
			{
				\IPS\Db::i()->insert( 'core_hooks', $inserts );
			}
			
			\IPS\Plugin\Hook::writeDataFile();
			
			foreach ( $templatesToRecompile as $k )
			{
				$exploded = explode( '_', $k );
				\IPS\Theme::deleteCompiledTemplate( $exploded[1], $exploded[2], $exploded[3] );
			}
		}
	}
	
	/**
	 * Install the application's widgets
	 *
	 * @return	void
	 */
	public function installWidgets()
	{
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/widgets.json" ) )
		{
			$currentWidgets = iterator_to_array( \IPS\Db::i()->select( '`key`', 'core_widgets', array( 'app=?', $this->directory ) ) );
	
			$inserts = array();
			foreach ( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/widgets.json" ), TRUE ) as $key => $json )
			{
				if ( ! in_array( $key, $currentWidgets ) )
				{
					$inserts[] = array(
							'app'		   => $this->directory,
							'key'		   => $key,
							'class'		   => $json['class'],
							'restrict'     => json_encode( $json['restrict'] ),
							'default_area' => ( isset( $json['default_area'] ) ? $json['default_area'] : NULL ),
							'allow_reuse'  => ( isset( $json['allow_reuse'] ) ? $json['allow_reuse'] : 0 ),
							'menu_style'   => ( isset( $json['menu_style'] ) ? $json['menu_style'] : 'menu' ),
							'embeddable'   => ( isset( $json['embeddable'] ) ? $json['embeddable'] : 0 ),
					);
				}
			}
			
			if( count( $inserts ) )
			{
				\IPS\Db::i()->insert( 'core_widgets', $inserts, TRUE );
				unset( \IPS\Data\Store::i()->widgets );
			}
		}
	}

	/**
	 * Install 'other' items. Left blank here so that application classes can override for app
	 *  specific installation needs. Always run as the last step.
	 *
	 * @return void
	 */
	public function installOther()
	{

	}
	
	/**
	 * Database check
	 *
	 * @return	array	Queries needed to correct database in the following format ( table => x, query = x );
	 */
	public function databaseCheck()
	{
		$db = \IPS\Db::i();
		$changesToMake = array();
		
		/* Loop the tables in the schema */
		foreach( json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ), TRUE ) as $tableName => $tableDefinition )
		{
			/* Get our local definition of this table */
			try
			{
				$localDefinition	= \IPS\Db::i()->getTableDefinition( $tableName );
				$localDefinition	= \IPS\Db::i()->normalizeDefinition( $localDefinition );
				$compareDefinition	= \IPS\Db::i()->normalizeDefinition( $tableDefinition );
				$tableDefinition	= \IPS\Db::i()->updateDefinitionIndexLengths( $tableDefinition );

				if ( $compareDefinition != $localDefinition )
				{
					/* Normalise it a little to prevent unnecessary conflicts */
					foreach ( $tableDefinition['columns'] as $k => $c )
					{
						foreach ( array( 'length', 'decimals' ) as $i )
						{
							if ( isset( $c[ $i ] ) )
							{
								$tableDefinition['columns'][ $k ][ $i ] = intval( $c[ $i ] );
							}
							else
							{
								$tableDefinition['columns'][ $k ][ $i ] = NULL;
							}
						}
						
						if ( !isset( $c['values'] ) )
						{
							$tableDefinition['columns'][ $k ]['values'] = array();
						}
						
						if ( $c['type'] === 'BIT' )
						{
							$tableDefinition['columns'][ $k ]['default'] = ( is_null($c['default']) ) ? NULL : "b'{$c['default']}'";
						}
						
						ksort( $tableDefinition['columns'][ $k ] );
					}
					
					/* Loop the columns */
					foreach ( $tableDefinition['columns'] as $columnName => $columnData )
					{
						/* If it doesn't exist in the local database, create it */
						if ( !isset( $localDefinition['columns'][ $columnName ] ) )
						{
							$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}` ADD COLUMN {$db->compileColumnDefinition( $columnData )};" );
						}
						/* Or if it's wrong, change it */
						elseif ( $columnData != $localDefinition['columns'][ $columnName ] )
						{
							$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}` CHANGE COLUMN `{$db->escape_string( $columnName )}` {$db->compileColumnDefinition( $columnData )};" );
						}
					}
					
					/* Loop the index */
					foreach ( $compareDefinition['indexes'] as $indexName => $indexData )
					{
						if ( !isset( $localDefinition['indexes'][ $indexName ] ) )
						{
							$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER TABLE `{$db->prefix}{$db->escape_string( $tableName )}` {$db->buildIndex( $tableName, $tableDefinition['indexes'][ $indexName ] )};" );
						}
						elseif ( $indexData != $localDefinition['indexes'][ $indexName ] )
						{
							$changesToMake[] = array( 'table' => $tableName, 'query' => "ALTER IGNORE TABLE `{$db->prefix}{$db->escape_string( $tableName )}` " . ( ( $indexName == 'PRIMARY KEY' ) ? "DROP " . $indexName . ", " : "DROP INDEX `" . $db->escape_string( $indexName ) . "`, " ) . $db->buildIndex( $tableName, $tableDefinition['indexes'][ $indexName ] ) . ';' );
						}
					}
				}
			}
			/* If the table doesn't exist, create it */
			catch ( \OutOfRangeException $e )
			{
				$changesToMake[] = array( 'table' => $tableName, 'query' => $db->_createTableQuery( $tableDefinition ) );
			}
		}
		
		return $changesToMake;
	}
	
	/**
	 * Build application for release
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function build()
	{
		/* Write the application data to the application.json file */
		$applicationData	= array(
			'application_title'	=> \IPS\Member::loggedIn()->language()->get('__app_' . $this->directory ),
			'app_author'		=> $this->author,
			'app_directory'		=> $this->directory,
			'app_protected'		=> $this->protected,
			'app_website'		=> $this->website,
			'app_update_check'	=> $this->update_check,
		);
		
		\IPS\Application::writeJson( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/application.json', $applicationData );

		/* Update app version data */
		$versions		= $this->getAllVersions();
		$longVersions	= array_keys( $versions );
		$humanVersions	= array_values( $versions );

		if( count($versions) )
		{
			$latestLVersion	= array_pop( $longVersions );
			$latestHVersion	= array_pop( $humanVersions );

			\IPS\Db::i()->update( 'core_applications', array( 'app_version' => $latestHVersion, 'app_long_version' => $latestLVersion ), array( 'app_directory=?', $this->directory ) );

			$this->long_version = $latestLVersion;
			$this->version		= $latestHVersion;
		}

		/* Take care of languages for this app */
		$this->buildLanguages();
		$this->installLanguages();

		/* Take care of skins for this app */
		$this->buildThemeTemplates();
		$this->installSkins( TRUE );

		/* Take care of emails for this app */
		$this->buildEmailTemplates();
		$this->installEmailTemplates();
		
		/* Take care of javascript for this app */
		$this->buildJavascript();
		$this->installJavascript();
		
		/* Take care of hooks for this app */
		$this->buildHooks();

		foreach( $this->extensions( 'core', 'Build' ) as $builder )
		{
			$builder->build();
		}
	}

	/**
	 * Build skin templates for an app
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function buildThemeTemplates()
	{
		/* Delete compiled items */
		\IPS\Theme::deleteCompiledTemplate( $this->directory );
		\IPS\Theme::deleteCompiledCss( $this->directory );
		\IPS\Theme::deleteCompiledResources( $this->directory );
		
		\IPS\Theme::i()->importDevHtml( $this->directory, 0 );
		\IPS\Theme::i()->importDevCss( $this->directory, 0 );

		/* Build XML and write to app directory */
		$xml = new \XMLWriter;
		$xml->openMemory();
		$xml->setIndent( TRUE );
		$xml->startDocument( '1.0', 'UTF-8' );
		
		/* Root tag */
		$xml->startElement('theme');
		$xml->startAttribute('name');
		$xml->text( "Default" );
		$xml->endAttribute();
		$xml->startAttribute('author_name');
		$xml->text( "Invision Power Services, Inc" );
		$xml->endAttribute();
		$xml->startAttribute('author_url');
		$xml->text( "http://www.invisionpower.com" );
		$xml->endAttribute();
		
		/* Skin settings */
		foreach (
			\IPS\Db::i()->select(
				'core_theme_settings_fields.*',
				'core_theme_settings_fields',
				array( 'sc_set_id=? AND sc_app=?', 1, $this->directory ), 
				'sc_key ASC'
			)
			as $row
		)
		{
			/* Initiate the <fields> tag */
			$xml->startElement('field');
			
			unset( $row['sc_id'], $row['sc_set_id'] );
			
			foreach( $row as $k => $v )
			{
				if ( $k != 'sc_content' )
				{
					$xml->startAttribute( $k );
					$xml->text( $v );
					$xml->endAttribute();
				}
			}
			
			/* Write value */
			if ( preg_match( '/<|>|&/', $row['sc_content'] ) )
			{
				$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $row['sc_content'] ) );
			}
			else
			{
				$xml->text( $row['sc_content'] );
			}
			
			/* Close the <fields> tag */
			$xml->endElement();
		}
		
		/* Templates */
		foreach ( \IPS\Db::i()->select( '*', 'core_theme_templates', array( 'template_set_id=? AND template_user_added=? AND template_app=?', 0, 0 , $this->directory ), 'template_group, template_name, template_location' ) as $template )
		{
			/* Initiate the <template> tag */
			$xml->startElement('template');
			
			foreach( $template as $k => $v )
			{
				if ( in_array( \substr( $k, 9 ), array('app', 'location', 'group', 'name', 'data' ) ) )
				{
					$xml->startAttribute( $k );
					$xml->text( $v );
					$xml->endAttribute();
				}
			}
			
			/* Write value */
			if ( preg_match( '/<|>|&/', $template['template_content'] ) )
			{
				$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $template['template_content'] ) );
			}
			else
			{
				$xml->text( $template['template_content'] );
			}
			
			/* Close the <template> tag */
			$xml->endElement();
		}

		/* Css */
		foreach ( \IPS\Db::i()->select( '*', 'core_theme_css', array( 'css_set_id=? AND css_added_to=? AND css_app=?', 0, 0 , $this->directory ), 'css_path, css_name, css_location' ) as $css )
		{
			$xml->startElement('css');

			foreach( $css as $k => $v )
			{
				if ( in_array( \substr( $k, 4 ), array('app', 'location', 'path', 'name', 'attributes' ) ) )
				{
					$xml->startAttribute( $k );
					$xml->text( $v );
					$xml->endAttribute();
				}
			}

			/* Write value */
			if ( preg_match( '/<|>|&/', $css['css_content'] ) )
			{
				$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $css['css_content'] ) );
			}
			else
			{
				$xml->text( $css['css_content'] );
			}
			
			$xml->endElement();
		}
		
		/* Resources */
		$_resources	= $this->_buildThemeResources();
		
		foreach ( $_resources as $data )
		{
			$xml->startElement('resource');
					
			$xml->startAttribute('name');
			$xml->text( $data['resource_name'] );
			$xml->endAttribute();
			
			$xml->startAttribute('app');
			$xml->text( $data['resource_app'] );
			$xml->endAttribute();
			
			$xml->startAttribute('location');
			$xml->text( $data['resource_location'] );
			$xml->endAttribute();
			
			$xml->startAttribute('path');
			$xml->text( $data['resource_path'] );
			$xml->endAttribute();
			
			/* Write value */
			$xml->text( base64_encode( $data['resource_data'] ) );
			
			$xml->endElement();
		}
		
		/* Finish */
		$xml->endDocument();
		
		/* Write it */
		if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
		{
			\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/theme.xml', $xml->outputMemory() );
		}
		else
		{
			throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
		}
	}

	/**
	 * Build Resources ready for non IN_DEV use
	 *
	 * @return	array
	 */
	protected function _buildThemeResources()
	{
		$resources = array();
		$path	= \IPS\ROOT_PATH . "/applications/" . $this->directory . "/dev/resources/";

		\IPS\Theme::i()->importDevResources( $this->directory, 0 );

		if ( is_dir( $path ) )
		{
			foreach( new \DirectoryIterator( $path ) as $location )
			{
				if ( $location->isDot() || \substr( $location->getFilename(), 0, 1 ) === '.' )
				{
					continue;
				}

				if ( $location->isDir() )
				{
					$resources	= $this->_buildResourcesRecursive( $location->getFilename(), '/', $resources );
				}
			}
		}

		return $resources;
	}
	
	/**
	 * Build Resources ready for non IN_DEV use (Iterable)
	 * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
	 *
	 * @param	string	$location	Location Folder Name
	 * @param	string	$path		Path
	 * @param	array	$resources	Array of resources to append to
	 * @return	array
	 */
	protected function _buildResourcesRecursive( $location, $path='/', $resources=array() )
	{
		$root = \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/resources/{$location}";
	
		foreach( new \DirectoryIterator( $root . $path ) as $file )
		{
			if ( $file->isDot() || \substr( $file->getFilename(), 0, 1 ) === '.' || $file == 'index.html' )
			{
				continue;
			}
	
			if ( $file->isDir() )
			{
				$resources	= $this->_buildResourcesRecursive( $location, $path . $file->getFilename() . '/', $resources );
			}
			else
			{
				$resources[] = array(
					'resource_app'		=> $this->directory,
					'resource_location'	=> $location,
					'resource_path'		=> $path,
					'resource_name'		=> $file->getFilename(),
					'resource_data'		=> \file_get_contents( $root . $path . $file->getFilename() ),
					'resource_added'	=> time()
				);
			}
		}

		return $resources;
	}

	/**
	 * Build languages for an app
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function buildLanguages()
	{
		/* Create the lang.xml file */
		$xml = new \XMLWriter;
		$xml->openMemory();
		$xml->setIndent( TRUE );
		$xml->startDocument( '1.0', 'UTF-8' );
				
		/* Root tag */
		$xml->startElement('language');

		/* Initiate the <app> tag */
		$xml->startElement('app');
		
		/* Set key */
		$xml->startAttribute('key');
		$xml->text( $this->directory );
		$xml->endAttribute();
		
		/* Set version */
		$xml->startAttribute('version');
		$xml->text( $this->long_version );
		$xml->endAttribute();
		
		/* Import the language files */
		$lang	= array();

		require \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/lang.php";
		foreach ( $lang as $k => $v )
		{
			/* Start */
			$xml->startElement( 'word' );
			
			/* Add key */
			$xml->startAttribute('key');
			$xml->text( $k );
			$xml->endAttribute();

			/* Add javascript flag */
			$xml->startAttribute('js');
			$xml->text( 0 );
			$xml->endAttribute();
							
			/* Write value */
			if ( preg_match( '/<|>|&/', $v ) )
			{
				$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $v ) );
			}
			else
			{
				$xml->text( $v );
			}
			
			/* End */
			$xml->endElement();
		}

		$lang	= array();

		require \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/jslang.php";
		foreach ( $lang as $k => $v )
		{
			/* Start */
			$xml->startElement( 'word' );
			
			/* Add key */
			$xml->startAttribute('key');
			$xml->text( $k );
			$xml->endAttribute();

			/* Add javascript flag */
			$xml->startAttribute('js');
			$xml->text( 1 );
			$xml->endAttribute();
							
			/* Write value */
			if ( preg_match( '/<|>|&/', $v ) )
			{
				$xml->writeCData( str_replace( ']]>', ']]]]><![CDATA[>', $v ) );
			}
			else
			{
				$xml->text( $v );
			}
			
			/* End */
			$xml->endElement();
		}

		/* Finish */
		$xml->endDocument();
			
		/* Write it */
		if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
		{
			\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/lang.xml', $xml->outputMemory() );
		}
		else
		{
			throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
		}
	}

	/**
	 * Build email templates for an app
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function buildEmailTemplates()
	{
		/* Where are we looking? */
		$path = \IPS\ROOT_PATH . "/applications/{$this->directory}/dev/email";
		
		/* We create an array and store the templates temporarily so we can merge plaintext and HTML together */
		$templates		= array();
		$templateKeys	= array();

		/* Loop over files in the directory */
		if ( is_dir( $path ) )
		{
			foreach( new \DirectoryIterator( $path ) as $location )
			{
				if ( $location->isDir() and mb_substr( $location, 0, 1 ) !== '.' and ( $location->getFilename() === 'plain' or $location->getFilename() === 'html' ) )
				{
					foreach( new \DirectoryIterator( $path . '/' . $location->getFilename() ) as $sublocation )
					{
						if ( $sublocation->isDir() and mb_substr( $sublocation, 0, 1 ) !== '.' )
						{
							foreach( new \DirectoryIterator( $path . '/' . $location->getFilename() . '/' . $sublocation->getFilename() ) as $file )
							{
								if ( $file->isDot() or !$file->isFile() or mb_substr( $file, 0, 1 ) === '.' or $file->getFilename() === 'index.html' )
								{
									continue;
								}
								
								$data = $this->_buildEmailTemplateFromInDev( $path . '/' . $location->getFilename() . '/' . $sublocation->getFilename(), $file, $sublocation->getFilename() . '__' );
								$extension = mb_substr( $file->getFilename(), mb_strrpos( $file->getFilename(), '.' ) + 1 );
								$type = ( $extension === 'txt' ) ? "plaintext" : "html";
								
								if ( ! isset( $templates[ $data['template_name'] ] ) )
								{
									$templates[ $data['template_name'] ] = array();
								}
				
								$templates[ $data['template_name'] ] = array_merge( $templates[ $data['template_name'] ], $data );
				
								/* Delete the template in the store */
								$key = $templates[ $data['template_name'] ]['template_key'] . '_email_' . $type;
								unset( \IPS\Data\Store::i()->$key );
				
								/* Remember our templates */
								$templateKeys[]	= $data['template_key'];
							}
						}
					}

				}
				else
				{
					if ( $location->isDot() or !$location->isFile() or mb_substr( $location, 0, 1 ) === '.' or $location->getFilename() === 'index.html' )
					{
						continue;
					}
					
					$data = $this->_buildEmailTemplateFromInDev( $path, $location );
					$extension = mb_substr( $location->getFilename(), mb_strrpos( $location->getFilename(), '.' ) + 1 );
					$type = ( $extension === 'txt' ) ? "plaintext" : "html";
					
					if ( ! isset( $templates[ $data['template_name'] ] ) )
					{
						$templates[ $data['template_name'] ]	= array();
					}
	
					$templates[ $data['template_name'] ] = array_merge( $templates[ $data['template_name'] ], $data );
	
					/* Delete the template in the store */
					$key = $templates[ $data['template_name'] ]['template_key'] . '_email_' . $type;
					unset( \IPS\Data\Store::i()->$key );
	
					/* Remember our templates */
					$templateKeys[]	= $data['template_key'];
				}
			}
		}

		/* Clear out invalid templates */
		\IPS\Db::i()->delete( 'core_email_templates', array( "template_app=? AND template_key NOT IN('" . implode( "','", $templateKeys ) . "')", $this->directory ) );

		/* If we have any templates, put them in the database */
		if( count($templates) )
		{
			foreach( $templates as $template )
			{
				\IPS\Db::i()->insert( 'core_email_templates', $template, TRUE );
			}

			/* Build the executable copies */
			$this->parseEmailTemplates();
		}

		$xml = \IPS\Xml\SimpleXML::create('emails');

		/* Templates */
		foreach ( \IPS\Db::i()->select( '*', 'core_email_templates', array( 'template_parent=? AND template_app=?', 0, $this->directory ), 'template_key ASC' ) as $template )
		{
			$forXml = array();
			foreach( $template as $k => $v )
			{
				if ( in_array( \substr( $k, 9 ), array('app', 'name', 'content_html', 'data', 'content_plaintext' ) ) )
				{
					$forXml[ $k ] = $v;
				}
			}
			
			$xml->addChild( 'template', $forXml );
		}

		/* Write it */
		if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
		{
			\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/emails.xml', $xml->asXML() );
		}
		else
		{
			throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
		}
	}
	
	/**
	 * Imports an IN_DEV email template into the database
	 *
	 * @param	string		$path			Path to file
	 * @param	object		$file			DirectoryIterator File Object
	 * @param	string|null	$namePrefix		Name prefix
	 * @return  array
	 */
	protected function _buildEmailTemplateFromInDev( $path, $file, $namePrefix='' )
	{
		/* Get the content */
		$html	= file_get_contents( $path . '/' . $file->getFilename() );
		$params	= array();
		
		/* Parse the header tag */
		preg_match( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', $html, $params );
		
		/* Strip the params tag */
		$html	= str_replace( $params[0], '', $html );
		
		/* Figure out some details */
		$extension = mb_substr( $file->getFilename(), mb_strrpos( $file->getFilename(), '.' ) + 1 );
		$name	= $namePrefix . str_replace( '.' . $extension, '', $file->getFilename() );
		$type	= ( $extension === 'txt' ) ? "plaintext" : "html";

		$return = array(
			'template_app'				=> $this->directory,
			'template_name'				=> $name,
			'template_data'				=> ( isset( $params[1] ) ) ? $params[1] : '',
			'template_content_' . $type	=> $html,
			'template_key'				=> md5( $this->directory . ';' . $name ),
		);

		return $return;
	}
	
	/**
	 * Build javascript for this app
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function buildJavascript()
	{
		/* Remove existing file object maps */
		$map = isset( \IPS\Data\Store::i()->javascript_map ) ? \IPS\Data\Store::i()->javascript_map : array();
		$map[ $this->directory ] = array();
		
		\IPS\Data\Store::i()->javascript_map = $map;
		
		$xml = \IPS\Output\Javascript::createXml( $this->directory );
	
		/* Write it */
		if ( is_writable( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data' ) )
		{
			\file_put_contents( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/javascript.xml', $xml->outputMemory() );
		}
		else
		{
			throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
		}
	}
	
	/**
	 * Build hooks for an app
	 *
	 * @return	void
	 * @throws	\RuntimeException
	 */
	public function buildHooks()
	{
		/* Build data */
		$data = array();
		foreach ( \IPS\Db::i()->select( '*', 'core_hooks', array( 'app=?', $this->directory ) ) as $hook )
		{
			$data[ $hook['filename'] ] = array(
				'type'		=> $hook['type'],
				'class'		=> $hook['class'],
			);
		}
				
		/* Write it */
		try
		{
			\IPS\Application::writeJson( \IPS\ROOT_PATH . '/applications/' . $this->directory . '/data/hooks.json', $data );
		}
		catch ( \RuntimeException $e )
		{
			throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack('dev_could_not_write_data') );
		}
	}

	/**
	 * Compile email template into executable template
	 *
	 * @return	void
	 */
	public function parseEmailTemplates()
	{
		foreach( \IPS\Db::i()->select( '*','core_email_templates', NULL, 'template_parent DESC', NULL, 'template_key' ) as $template )
		{
			/* Rebuild built copies */
			$htmlFunction	= 'namespace IPS\Theme;' . "\n" . \IPS\Theme::compileTemplate( $template['template_content_html'], "email_html_{$template['template_app']}_{$template['template_name']}", $template['template_data'] );
			$ptFunction		= 'namespace IPS\Theme;' . "\n" . \IPS\Theme::compileTemplate( $template['template_content_plaintext'], "email_plaintext_{$template['template_app']}_{$template['template_name']}", $template['template_data'] );

			$key	= $template['template_key'] . '_email_html';
			\IPS\Data\Store::i()->$key = $htmlFunction;

			$key	= $template['template_key'] . '_email_plaintext';
			\IPS\Data\Store::i()->$key = $ptFunction;
		}
	}
	
	/**
	 * Write JSON file
	 *
	 * @param	string	$file	Filepath
	 * @param	array	$data	Data to write
	 * @return	void
	 * @throws	\RuntimeException	Could not write
	 */
	public static function writeJson( $file, $data )
	{
		/* Format the JSON if we can (JSON_PRETTY_PRINT) is only available on PHP 5.4+ */
		if ( version_compare( PHP_VERSION, '5.4.0' ) >= 0 )
		{
			$json = json_encode( $data, JSON_PRETTY_PRINT );
			
			/* No idea why, but for some people blank structures have line breaks in them and for some people they don't
				which unecessarily makes version control think things have changed - so let's make it the same for everyone */
			$json = preg_replace( '/\[\s*\]/', '[]', $json );
			$json = preg_replace( '/\{\s*\}/', '{}', $json );
		}
		else
		{
			$json = json_encode( $data );
		}
		
		/* Write it */
		if( \file_put_contents( $file, $json ) === FALSE )
		{
			throw new \RuntimeException;
		}
		@chmod( $file, 0777 );
	}

	/**
	 * Can the user access this application?
	 *
	 * @param	\IPS\Member|NULL	$member		Member we are checking against or NULL for currently logged on user
	 * @return	bool
	 */
	public function canAccess( $member=NULL )
	{
		/* If it's not enabled, we can't */
		if( !$this->enabled )
		{
			return FALSE;
		}

		/* If all groups have access, we can */
		if( $this->disabled_groups === NULL )
		{
			return TRUE;
		}
		
		/* If all groups have access, we can */
		if( $this->disabled_groups == '*' )
		{
			return FALSE;
		}

		/* Check member */
		$member	= ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
		$memberGroups	= array_merge( array( $member->member_group_id ), array_filter( explode( ',', $member->mgroup_others ) ) );
		$accessGroups	= explode( ',', $this->disabled_groups );

		/* Are we in an allowed group? */
		if( count( array_intersect( $accessGroups, $memberGroups ) ) )
		{
			return TRUE;
		}

		return FALSE;
	}
	
	/**
	 * Can manage the widgets
	 *
	 * @param	\IPS\Member|NULL	$member		Member we are checking against or NULL for currently logged on user
	 * @return 	boolean
	 */
	public function canManageWidgets( $member=NULL )
	{
		/* Check member */
		$member	= ( $member === NULL ) ? \IPS\Member::loggedIn() : $member;
		
		return $member->modPermission('can_manage_sidebar');
	}
	
	/**
	 * Save Changes
	 *
	 * @param	bool	$skipMember		Skip clearing member cache clearing
	 * @return	void
	 */
	public function save( $skipMember=FALSE )
	{
		parent::save();
		unset( \IPS\Data\Store::i()->applications );

		/* Clear out member's cached "Create Menu" contents */
		if( !$skipMember )
		{
			\IPS\Member::clearCreateMenu();
		}
	}
	
	/**
	 * Delete Record
	 *
	 * @return	void
	 */
	public function delete()
	{
		/* Get our uninstall callback script(s) if present. They are stored in an array so that we only create one object per extension, instead of one each time we loop. */
		$uninstallExtensions	= array();
		foreach( $this->extensions( 'core', 'Uninstall', TRUE ) as $extension )
		{
			$uninstallExtensions[]	= $extension;
		}

		/* Call preUninstall() so that application may perform any necessary cleanup before other data is removed (i.e. database tables) */
		foreach( $uninstallExtensions as $extension )
		{
			if( method_exists( $extension, 'preUninstall' ) )
			{
				$extension->preUninstall( $this->directory );
			}
		}

		$templatesToRecompile = array();

		/* Note any templates that will need recompiling */
		foreach ( \IPS\Db::i()->select( 'class', 'core_hooks', array( 'app=? AND type=?', $this->directory, 'S' ) ) as $class )
		{
			$templatesToRecompile[ $class ] = $class;
		}

		/* Delete data from shared tables */
		\IPS\Content\Search\Index::i()->removeApplicationContent( $this );
		\IPS\DB::i()->delete( 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id IN(?)', 'core', 'module', \IPS\Db::i()->select( 'sys_module_id', 'core_modules', array( 'sys_module_application=?', $this->directory ) ) ) );
		\IPS\DB::i()->delete( 'core_modules', array( 'sys_module_application=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_dev', array( 'app_key=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_hooks', array( 'app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_item_markers', array( 'item_app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_permission_index', array( 'app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_upgrade_history', array( 'upgrade_app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_admin_logs', array( 'appcomponent=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_sys_conf_settings', array( 'conf_app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_queue', array( 'app=?', $this->directory ) );

		$classes = array();
		foreach( $this->extensions( 'core', 'ContentRouter' ) AS $contentRouter )
		{
			foreach ( $contentRouter->classes as $class )
			{
				$classes[]	= $class;

				if ( isset( $class::$commentClass ) )
				{
					$classes[]	= $class::$commentClass;
				}

				if ( isset( $class::$reviewClass ) )
				{
					$classes[]	= $class::$reviewClass;
				}
			}
		}

		$queueWhere = array();
		$queueWhere[] = array( 'app=?', 'core' );
		$queueWhere[] = array( \IPS\Db::i()->in( '`key`', array( 'rebuildPosts', 'RebuildReputationIndex' ) ) );

		foreach ( \IPS\Db::i()->select( '*', 'core_queue', $queueWhere ) as $queue )
		{
			$queue['data'] = json_decode( $queue['data'], TRUE );
			if( in_array( $queue['data']['class'], $classes ) )
			{
				\IPS\Db::i()->delete( 'core_queue', array( 'id=?', $queue['id'] ) );
			}
		}

		\IPS\Db::i()->delete( 'core_notifications', \IPS\Db::i()->in( 'item_class', $classes ) );

		unset( \IPS\Data\Store::i()->settings );

		/* Delete tasks and task logs */
		\IPS\DB::i()->delete( 'core_tasks', array( 'app=?', $this->directory ) );
		\IPS\DB::i()->delete( 'core_tasks_log', array( 'task IN(?)', \IPS\Db::i()->select( 'id', 'core_tasks', array( 'app=?', $this->directory ) ) ) );

		/* Delete language strings */
		\IPS\DB::i()->delete( 'core_sys_lang_words', array( 'word_app=?', $this->directory ) );

		/* Delete email templates */
		$emailTemplates	= \IPS\DB::i()->select( '*', 'core_email_templates', array( 'template_app=?', $this->directory ) );

		if( $emailTemplates->count() )
		{
			foreach( $emailTemplates as $template )
			{
				if( $template['template_content_html'] )
				{
					$k = $template['template_key'] . '_email_html';
					unset( \IPS\Data\Store::i()->$k );
				}

				if( $template['template_content_plaintext'] )
				{
					$k = $template['template_key'] . '_email_plaintext';
					unset( \IPS\Data\Store::i()->$k );
				}
			}

			\IPS\DB::i()->delete( 'core_email_templates', array( 'template_app=?', $this->directory ) );
		}

		/* Delete skin template/CSS/etc. */
		\IPS\Theme::removeTemplates( $this->directory, NULL, NULL, NULL, TRUE );
		\IPS\Theme::removeCss( $this->directory, NULL, NULL, NULL, TRUE );
		\IPS\Theme::removeResources( $this->directory, NULL, NULL, NULL, TRUE );

		/* Delete any stored files */
		foreach( $this->extensions( 'core', 'FileStorage', TRUE ) as $extension )
		{
			$extension->delete();
		}

		$notificationTypes = array();
		foreach( $this->extensions( 'core', 'Notifications' ) as $key => $class )
		{
			if ( method_exists( $class, 'getConfiguration' ) )
			{
				$defaults = $class->getConfiguration( NULL );

				foreach( $defaults AS $k => $config )
				{
					$notificationTypes[] =  $k;
				}
			}
		}

		if( count( $notificationTypes ) )
		{
			\IPS\Db::i()->delete( 'core_notification_defaults', "notification_key IN('" . implode( "','", $notificationTypes ) . "')");
			\IPS\Db::i()->delete( 'core_notification_preferences', "notification_key IN('" . implode( "','", $notificationTypes ) . "')");
		}

		/* Delete database tables */
		if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ) )
		{
			$schema	= @json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->directory}/data/schema.json" ), TRUE );

			if( is_array( $schema ) AND count( $schema ) )
			{
				foreach( $schema as $tableName => $definition )
				{
					\IPS\DB::i()->dropTable( $tableName, TRUE );
				}
			}
		}

		/* delete widgets */
		\IPS\Db::i()->delete( 'core_widgets', array( 'app = ?', $this->directory ) );
		\IPS\Db::i()->delete( 'core_widget_areas', array( 'app = ?', $this->directory ) );

		/* clean up widget areas table */
		foreach ( \IPS\Db::i()->select( '*', 'core_widget_areas' ) as $row )
		{
			$data = json_decode( $row['widgets'], true );

			foreach ( $data as $key => $widget)
			{
				if ( isset( $widget['app'] ) and $widget['app'] == $this->directory )
				{
					unset( $data[$key]) ;
				}
			}

			\IPS\Db::i()->update( 'core_widget_areas', array( 'widgets' => json_encode( $data ) ), array( 'id=?', $row['id'] ) );
		}


		/* Call postUninstall() so that application may perform any necessary cleanup after other data is removed */
		foreach( $uninstallExtensions as $extension )
		{
			if( method_exists( $extension, 'postUninstall' ) )
			{
				$extension->postUninstall( $this->directory );
			}
		}
		
		/* Delete from DB */
		\IPS\File::unclaimAttachments( 'core_Admin', $this->id, NULL, 'appdisabled' );
		parent::delete();

		/* Rebuild hooks file */
		\IPS\Plugin\Hook::writeDataFile();
		foreach ( $templatesToRecompile as $k )
		{
			$exploded = explode( '_', $k );
			\IPS\Theme::deleteCompiledTemplate( $exploded[1], $exploded[2], $exploded[3] );
		}

		/* Clear out member's cached "Create Menu" contents */
		\IPS\Member::clearCreateMenu();
		unset( \IPS\Data\Store::i()->modules );
		unset( \IPS\Data\Store::i()->applications );
		unset( \IPS\Data\Store::i()->settings );
		unset( \IPS\Data\Store::i()->widgets );
	}

	/**
	 * Return an array of version upgrade folders this application contains
	 *
	 * @param	int		$start	If provided, only upgrade steps above this version will be returned
	 * @return	array
	 */
	public function getUpgradeSteps( $start=0 )
	{
		$path	= \IPS\ROOT_PATH . "/applications/{$this->directory}/setup";

		if( !is_dir( $path ) )
		{
			return array();
		}

		$versions	= array();

		foreach( new \DirectoryIterator( $path ) as $file )
		{
			if( $file->isDir() AND !$file->isDot() )
			{
				if( mb_substr( $file->getFilename(), 0, 4 ) == 'upg_' )
				{
					$_version	= intval( mb_substr( $file->getFilename(), 4 ) );

					if( $_version > $start )
					{
						$versions[]	= $_version;
					}
				}
			}
		}

		/* Sort the versions lowest to highest */
		sort( $versions, SORT_NUMERIC );

		return $versions;
	}
}