<?php
/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.4.6
 * Hooks Management
 * Last Updated: $Date: 2013-05-20 16:38:23 -0400 (Mon, 20 May 2013) $
 * </pre>
 *
 * @author 		$Author: bfarber $
 * @copyright	(c) 2001 - 2009 Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/company/standards.php#license
 * @package		IP.Board
 * @subpackage	Core
 * @link		http://www.invisionpower.com
 * @since		3.0.0
 * @version		$Revision: 12261 $
 */

if ( ! defined( 'IN_ACP' ) )
{
	print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded 'admin.php'.";
	exit();
}

class admin_core_applications_hooks extends ipsCommand
{
	/**
	 * Skin object
	 *
	 * @var		object			Skin templates
	 */
	protected $html;
	
	/**
	 * Hooks library
	 *
	 * @var		object			Hooks library
	 */
	protected $hooksFunctions;
	
	/**
	 * Existing hooks
	 *
	 * @var		array 			Existing hooks
	 */
	protected $hooks;	
	
	/**
	 * Array of all the cache versions
	 * for the required applications
	 * 
	 * @var	$cachedVersions
	 */
	protected $cachedVersions = array();
	
	/**
	 * Main class entry point
	 *
	 * @param	object		ipsRegistry reference
	 * @return	@e void		[Outputs to screen]
	 */
	public function doExecute( ipsRegistry $registry ) 
	{
		//-----------------------------------------
		// Load skin
		//-----------------------------------------
		
		$this->html = $this->registry->output->loadTemplate('cp_skin_applications');
		
		//-----------------------------------------
		// Load hooks library
		//-----------------------------------------
		
		$classToLoad = IPSLib::loadLibrary( IPSLib::getAppDir('core') . '/sources/classes/hooksFunctions.php', 'hooksFunctions' );
		$this->hooksFunctions = new $classToLoad( $registry );
		
		// lang already loaded by hookFunctions
		//$this->registry->class_localization->loadLanguageFile( array( 'admin_applications' ), 'core' );

		if( defined('NO_WRITE_HOOKS') )
		{
			$this->registry->output->showError( $this->lang->words['no_write_hooks'] );
		}

		//-----------------------------------------
		// Set up stuff
		//-----------------------------------------
		
		$this->form_code	= $this->html->form_code	= 'module=applications&amp;section=hooks&amp;';
		$this->form_code_js	= $this->html->form_code_js	= 'module=applications&section=hooks&';
		
		//-----------------------------------------
		// What to do...
		//-----------------------------------------
		
		switch( $this->request['do'] )
		{
			case 'reenable_all_hooks':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_reenableAllHooks();
			break;
			case 'disable_all_hooks':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_disableAllHooks();
			break;
				
			break;
			case 'disable_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_disableHook();
			break;
			case 'enable_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_enableHook();
			break;
			
			case 'uninstall_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_delete' );
				$this->_uninstallHook();
			break;
			case 'install_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_installHook();
			break;
			
			case 'reorder':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_reorder();
			break;
			
			case 'view_details':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_viewDetails();
			break;
			
			case 'check_requirements':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_checkRequirements();
			break;
			
			case 'export_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_exportHook();
			break;
			
			case 'do_export_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_doExportHook();
			break;
			
			case 'create_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_hookForm( 'add' );
			break;
			
			case 'edit_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_hookForm( 'edit' );
			break;
			
			case 'do_create_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_hookSave( 'add' );
			break;
			
			case 'do_edit_hook':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->_hookSave( 'edit' );
			break;
			
			case 'removeDeadCaches':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->removeDeadCaches();
			break;

			case 'reimport_apps':
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->reimportAppHooks();
			break;

			case 'hooks_overview':
			default:
				$this->registry->getClass('class_permissions')->checkPermissionAutoMsg( 'hooks_manage' );
				$this->request['do'] = 'hooks_overview';
				$this->_hooksOverview();
			break;
		}
		
		//-----------------------------------------
		// Pass to CP output hander
		//-----------------------------------------
		
		$this->registry->getClass('output')->html_main .= $this->registry->getClass('output')->global_template->global_frame_wrapper();
		$this->registry->getClass('output')->sendOutput();
	}
	
	/**
	 * Reimport all hooks for all installed applications
	 *
	 * @return	@e void
	 */
	protected function reimportAppHooks()
	{
		$stats	= array( 'inserted' => 0, 'updated' => 0, 'skipped' => 0 );
		
		foreach( ipsRegistry::$applications as $app )
		{
			$_stats	= $this->installAppHooks( $app['app_directory'] );
			
			$stats['inserted']	= $stats['inserted'] + $_stats['inserted'];
			$stats['updated']	= $stats['updated'] + $_stats['updated'];
			$stats['skipped']	= $stats['skipped'] + $_stats['skipped'];
		}
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Rebuild global caches */
		try
		{
			IPSLib::cacheGlobalCaches();
		}
		catch( Exception $e ){}
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		$skinCaching->flagSetForRecache();
		
		/* Redirect */
		$this->registry->output->global_message = sprintf( $this->lang->words['app_hooks_rebuilt'], $stats['inserted'], $stats['updated'], $stats['skipped'] );
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * Rebuild skins following hook import that included a template
	 *
	 * @return	@e void
	 */
	protected function removeDeadCaches()
	{
		$messages = array();
		$keep	  = array();
		$unlink	  = array();
		
		/* Grab all current hooks caches */
		$this->DB->build( array( 'select' => '*',
								 'from'   => 'core_hooks_files' ) );
								
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			$keep[] = $row['hook_file_stored'];
		}
		
		try
		{
			foreach( new DirectoryIterator( IPS_HOOKS_PATH ) as $file )
			{
				if ( ! $file->isDot() )
				{
					$_name = $file->getFileName();
					
					if ( preg_match( "#_[a-z0-9]{32}\.php$#", $_name ) )
					{
						if ( ! in_array( $_name, $keep ) )
						{
							$unlink[] = $_name;
						}
					}
				}
			}
		} catch ( Exception $e ) { print $e->getMessage(); }
		
		/* Anything to unlink? */
		if ( count( $unlink ) )
		{
			foreach( $unlink as $file )
			{
				@unlink( IPS_HOOKS_PATH . $file );
			}
		}
		
		/* Print message */
		$this->registry->output->global_message = sprintf( $this->lang->words['hook_removed_files'], count( $unlink ) ) . '<br />' . implode( '<br />', $unlink );
		$this->_hooksOverview();
	}

	/**
	 * Reorder hooks
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _reorder()
	{
		//-----------------------------------------
		// INIT
		//-----------------------------------------

		$classToLoad = IPSLib::loadLibrary( IPS_KERNEL_PATH . 'classAjax.php', 'classAjax' );
		$ajax		 = new $classToLoad();
		
		//-----------------------------------------
		// Checks...
		//-----------------------------------------

		if( $this->registry->adminFunctions->checkSecurityKey( $this->request['md5check'], true ) === false )
		{
			$ajax->returnString( $this->lang->words['postform_badmd5'] );
		}
 		
 		//-----------------------------------------
 		// Save new position
 		//-----------------------------------------

 		$position	= 1;
 		
 		if( is_array($this->request['hooks']) AND count($this->request['hooks']) )
 		{
 			/* Reset all hooks to 0 temporarily */
 			$this->DB->update( 'core_hooks', array( 'hook_position' => 0 ) );
 			
 			foreach( $this->request['hooks'] as $this_id )
 			{
 				$this->DB->update( 'core_hooks', array( 'hook_position' => $position ), 'hook_id=' . $this_id );
 				
 				$position++;
 			}
 		}
 		
		$this->rebuildHooksCache();

 		$ajax->returnString('OK');
	}
	
	/**
	 * Install a new hook
	 *
	 * @return   void
	 */
	protected function _installHook()
	{
		//-----------------------------------------
		// Get uploaded schtuff
		//-----------------------------------------

		$tmp_name = $_FILES['FILE_UPLOAD']['name'];
		$tmp_name = preg_replace( "#\.gz$#", "", $tmp_name );
		
		$content  = ipsRegistry::getClass('adminFunctions')->importXml( $tmp_name );
		
		if( ! $content )
		{
			$this->registry->output->showError( $this->lang->words['h_noxml'], 1110 );
		}

		$this->installHook( $content, TRUE, FALSE );
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Rebuild global caches */
		try
		{
			IPSLib::cacheGlobalCaches();
		}
		catch( Exception $e ){}
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		$skinCaching->flagSetForRecache();
		
		/* Redirect */
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * Function mostly used in installer/upgrader
	 * Inserts new hooks
	 *
	 * @param	string		Application to install hooks for
	 */
	public function installAppHooks( $app )
	{
		static $hooks	= array();

		$msgs    = array( 'inserted' => 0, 'updated' => 0, 'skipped' => 0 );
		$xmlData = array();
		$path    = IPSLib::getAppDir( $app ) . '/xml';
		
		if ( is_file( $path . '/hooks.xml' ) AND is_dir( $path . '/hooks' ) )
		{
			if( !count($hooks) )
			{
				/* Fetch current hooks */
				$this->DB->build( array( 'select' => '*',
										 'from'   => 'core_hooks' ) );
				$this->DB->execute();
				
				while( $row = $this->DB->fetch() )
				{
					if ( $row['hook_key'] )
					{
						$hooks[ $row['hook_key'] ] = $row;
					}
				}
			}
			
			/* Alright. We're in. Read the XML file */
			require_once( IPS_KERNEL_PATH . 'classXML.php' );/*noLibHook*/
			$xml    = new classXML( IPS_DOC_CHAR_SET );

			/* Grab wrapper file */
			$xml->load(  $path . '/hooks.xml' );

			foreach( $xml->fetchElements('hook') as $data )
			{
				$xmlData[] = $xml->fetchElementsFromRecord( $data );
			}

			/* Examine XML */
			if ( is_array( $xmlData ) AND count( $xmlData ) )
			{
				foreach( $xmlData as $x )
				{
					if ( is_file( $path . '/hooks/' . $x['file'] ) )
					{
						$xml->load(  $path . '/hooks/' . $x['file'] );
						
						foreach( $xml->fetchElements('config') as $data )
						{
							$config	= $xml->fetchElementsFromRecord( $data );
						}
						
						if ( ! isset( $hooks[ $config['hook_key'] ] ) )
						{
							/* Add it */
							$msgs['inserted']++;
							$this->installHook( file_get_contents( $path . '/hooks/' . $x['file'] ), FALSE, FALSE, $x['enabled'] );
						} 
						else
						{
							$this->installHook( file_get_contents( $path . '/hooks/' . $x['file'] ), FALSE, FALSE, $x['enabled'] );
							$msgs['updated']++;
						}
					}
				}
			}
		}
		
		return $msgs;
	}
	
	/**
	 * Public install hook so we can use it in the installer and elsewhere
	 *
	 * @param	string		XML data
	 * @param	boolean		Add message to output->global_message
	 * @param	boolean		Allow skins to recache
	 * @param	int			Install enabled
	 * @return	@e void
	 */
	public function installHook( $content, $addMessage=FALSE, $allowSkinRecache=TRUE, $enabled=1 )
	{
		//-----------------------------------------
		// Hooks directory writable?
		//-----------------------------------------
		
		if( !is_writable( IPS_HOOKS_PATH ) )
		{
			if( !$addMessage )
			{
				return false;
			}

			$this->registry->output->showError( $this->lang->words['h_dir_notwritable'], 111159 );
		}
		
		//-----------------------------------------
		// Got our hooks?
		//-----------------------------------------
		
		if( !is_array($this->hooks) OR !count($this->hooks) )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$this->hooks[ $r['hook_key'] ]	= $r;
			}
		}

		//-----------------------------------------
		// Get xml mah-do-dah
		//-----------------------------------------
		
		require_once( IPS_KERNEL_PATH . 'classXML.php' );/*noLibHook*/
		$xml    = new classXML( IPS_DOC_CHAR_SET );
		
		//-----------------------------------------
		// Unpack the datafile
		//-----------------------------------------
		
		$xml->loadXML( $content );

		foreach( $xml->fetchElements('config') as $data )
		{
			$config	= $xml->fetchElementsFromRecord( $data );

			if( !count($config) )
			{
				$this->registry->output->showError( $this->lang->words['h_xmlwrong'], 1111 );
			}
		}
		
		//-----------------------------------------
		// Temp
		//-----------------------------------------
		
		$tempExtraData = unserialize( $config['hook_extra_data'] );

		//-----------------------------------------
		// Set config
		//-----------------------------------------
		
		$config = array('hook_name'				=> $config['hook_name'],
						'hook_desc'				=> $config['hook_desc'],
						'hook_author'			=> $config['hook_author'],
						'hook_email'			=> $config['hook_email'],
						'hook_website'			=> $config['hook_website'],
						'hook_update_check'		=> $config['hook_update_check'],
						'hook_requirements'		=> $config['hook_requirements'],
						'hook_version_human'	=> $config['hook_version_human'],
						'hook_version_long'		=> $config['hook_version_long'],
						'hook_key'				=> $config['hook_key'],
						'hook_global_caches'	=> $config['hook_global_caches'],
						'hook_enabled'			=> $enabled,
						'hook_updated'			=> IPS_UNIX_TIME_NOW,
						);

		$extra_data	= array();

		//-----------------------------------------
		// Set files
		//-----------------------------------------
		
		$files		= array();

		foreach( $xml->fetchElements('hookfiles') as $node )
		{
			foreach( $xml->fetchElements('file', $node) as $_file )
			{
				$file	= $xml->fetchElementsFromRecord( $_file );
	
				if( $file['hook_type'] )
				{
					$files[] 	= array(
										'hook_file_real'	=> $file['hook_file_real'],
										'hook_type'			=> $file['hook_type'],
										'hook_classname'	=> $file['hook_classname'],
										'hook_data'			=> $file['hook_data'],
										'hooks_source'		=> $file['hooks_source'],
										);
				}
			}
		}

		//-----------------------------------------
		// Set the custom script
		//-----------------------------------------

		$custom		 = array();
		$customClass = null;

		foreach( $xml->fetchElements('hookextras_custom') as $node )
		{
			foreach( $xml->fetchElements('file', $node) as $_file )
			{
				$file	= $xml->fetchElementsFromRecord( $_file );
				
				if( count($file) )
				{
					$custom	= array( 'filename'	=> $file['filename'],
									 'source'	=> $file['source'],
									);
				}
			}
		}
		
		/* Got any custom file to initialize? */
		if ( $custom['filename'] && $custom['source'] )
		{
			# Our dirty trick to avoid saving the file on disk just yet...
			$_custom = preg_replace( '#^(\s*)<\?(php)+#i', '', $custom['source'] );
			eval($_custom);
			
			$classname = str_replace( '.php', '', $custom['filename'] );
			
			if( class_exists( $classname ) )
			{
				$customClass = new $classname( $this->registry );
				
				if( method_exists( $customClass, 'pre_install' ) )
				{
					$customClass->pre_install();
				}
			}
		}
		
		//-----------------------------------------
		// Set the settings
		//-----------------------------------------
		
		$settings		= array();
		$settingGroups	= array();

		foreach( $xml->fetchElements('hookextras_settings') as $node )
		{
			foreach( $xml->fetchElements('setting', $node) as $_setting )
			{
				$setting	= $xml->fetchElementsFromRecord( $_setting );
	
				if( $setting['conf_is_title'] == 1)
				{
					$settingGroups[]	= array(
												'conf_title_title'		=> $setting['conf_title_title'],
												'conf_title_desc'		=> $setting['conf_title_desc'],
												'conf_title_noshow'		=> $setting['conf_title_noshow'],
												'conf_title_keyword'	=> $setting['conf_title_keyword'],
												'conf_title_app'		=> $setting['conf_title_app'],
												'conf_title_tab'		=> $setting['conf_title_tab'],
												);
				}
				else
				{
					$settings[]			= array(
												'conf_title'			=> $setting['conf_title'],
												'conf_description'		=> $setting['conf_description'],
												'conf_group'			=> $setting['conf_group'],
												'conf_type'				=> $setting['conf_type'],
												'conf_key'				=> $setting['conf_key'],
												'conf_default'			=> $setting['conf_default'],
												'conf_extra'			=> $setting['conf_extra'],
												'conf_evalphp'			=> $setting['conf_evalphp'],
												'conf_protected'		=> $setting['conf_protected'],
												'conf_position'			=> $setting['conf_position'],
												'conf_start_group'		=> $setting['conf_start_group'],
												'conf_add_cache'		=> $setting['conf_add_cache'],
												'conf_title_keyword'	=> $setting['conf_title_keyword'],
												);
				}
			}
		}

		//-----------------------------------------
		// Set the lang bits
		//-----------------------------------------
		
		$language		= array();

		foreach( $xml->fetchElements('hookextras_language') as $node )
		{
			foreach( $xml->fetchElements('language', $node) as $_langbit )
			{
				$langbit	= $xml->fetchElementsFromRecord( $_langbit );
				
				$language[]	= array(
									'word_app'		=> $langbit['word_app'],
									'word_pack'		=> $langbit['word_pack'],
									'word_key'		=> $langbit['word_key'],
									'word_default'	=> $langbit['word_default'],
									'word_custom'	=> $langbit['word_custom'],
									'word_js'		=> intval($langbit['word_js']),
									);
			}
		}
		
		//-----------------------------------------
		// Set the modules
		//-----------------------------------------
		
		$modules		= array();

		foreach( $xml->fetchElements('hookextras_modules') as $node )
		{
			foreach( $xml->fetchElements('module', $node) as $_module )
			{
				$module		= $xml->fetchElementsFromRecord( $_module );
				$modules[]	= array(
									'sys_module_title'			=> $module['sys_module_title'],
									'sys_module_application'	=> $module['sys_module_application'],
									'sys_module_key'			=> $module['sys_module_key'],
									'sys_module_description'	=> $module['sys_module_description'],
									'sys_module_version'		=> $module['sys_module_version'],
									'sys_module_protected'		=> $module['sys_module_protected'],
									'sys_module_visible'		=> $module['sys_module_visible'],
									'sys_module_position'		=> $module['sys_module_position'],
									'sys_module_admin'			=> $module['sys_module_admin'],
									);
			}
		}
		
		//-----------------------------------------
		// Set the help files
		//-----------------------------------------
		
		$help			= array();

		foreach( $xml->fetchElements('hookextras_help') as $node )
		{
			foreach( $xml->fetchElements('help', $node) as $_helpfile )
			{
				$helpfile	= $xml->fetchElementsFromRecord( $_helpfile );
				$help[]		= array(
									'title'			=> $helpfile['title'],
									'text'			=> $helpfile['text'],
									'description'	=> $helpfile['description'],
									'position'		=> $helpfile['position'],
									);
			}
		}
		
		//-----------------------------------------
		// Set the templates
		//-----------------------------------------
		
		$templates		= array();

		foreach( $xml->fetchElements('hookextras_templates') as $node )
		{
			foreach( $xml->fetchElements('templates', $node) as $_template )
			{
				$template		= $xml->fetchElementsFromRecord( $_template );
				$templates[]	= array(
										'template_set_id'		=> 0,
										'template_group'		=> $template['template_group'],
										'template_content'		=> $template['template_content'],
										'template_name'			=> $template['template_name'],
										'template_data'			=> $template['template_data'],
										'template_updated'		=> $template['template_updated'],
										'template_removable'	=> $template['template_removable'],
										'template_added_to'		=> intval($template['template_added_to']),
										'template_user_added'	=> 1,
										'template_user_edited'  => 0,
										'template_master_key'   => $template['template_master_key'] ? $template['template_master_key'] : 'root',
										);
			}
		}
		
		//-----------------------------------------
		// Set the CSS
		//-----------------------------------------
		
		$css			= array();

		foreach( $xml->fetchElements('hookextras_css') as $node )
		{
			foreach( $xml->fetchElements('css', $node) as $_css )
			{
				$_css	= $xml->fetchElementsFromRecord( $_css );
				$css[]	= array(
									'css_set_id'		=> 0,
									'css_group'			=> $_css['css_group'],
									'css_content'		=> $_css['css_content'],
									'css_position'		=> $_css['css_position'],
									'css_added_to'		=> 1,
									'css_app'			=> $_css['css_app'],
									'css_app_hide'		=> $_css['css_app_hide'],
									'css_attributes'	=> $_css['css_attributes'],
									'css_removed'		=> $_css['css_removed'],
									'css_modules'		=> $_css['css_modules'],
									'css_master_key'	=> $_css['css_master_key'],
									);
			}
		}
		
		//-----------------------------------------
		// Set the Replacements
		//-----------------------------------------
		
		$replacements	= array();
		
		foreach( $xml->fetchElements('hookextras_replacements') as $node )
		{
			foreach( $xml->fetchElements('replacements', $node) as $_r )
			{
				$_r	= $xml->fetchElementsFromRecord( $_r );
				$replacements[]	= array(
									'replacement_key'		=> $_r['replacement_key'],
									'replacement_content'	=> $_r['replacement_content'],
									);
			}
		}
		
		//-----------------------------------------
		// Set the tasks
		//-----------------------------------------
		
		$tasks			= array();

		foreach( $xml->fetchElements('hookextras_tasks') as $node )
		{
			foreach( $xml->fetchElements('tasks', $node) as $_task )
			{
				$task		= $xml->fetchElementsFromRecord( $_task );
				$tasks[]	= array(
									'task_title'		=> $task['task_title'],
									'task_file'			=> $task['task_file'],
									'task_week_day'		=> $task['task_week_day'],
									'task_month_day'	=> $task['task_month_day'],
									'task_hour'			=> $task['task_hour'],
									'task_minute'		=> $task['task_minute'],
									'task_cronkey'		=> $task['task_cronkey'],
									'task_log'			=> $task['task_log'],
									'task_description'	=> $task['task_description'],
									'task_enabled'		=> $task['task_enabled'],
									'task_key'			=> $task['task_key'],
									'task_safemode'		=> $task['task_safemode'],
									'task_locked'		=> $task['task_locked'],
									'task_application'	=> $task['task_application'],
									);
			}
		}

		//-----------------------------------------
		// Set the database changes
		//-----------------------------------------
		
		$database		= array(
								'create'	=> array(),
								'alter'		=> array(),
								'update'	=> array(),
								'insert'	=> array(),
								);

		foreach( $xml->fetchElements('hookextras_database_create') as $node )
		{
			foreach( $xml->fetchElements('create', $node) as $_table )
			{
				$table		= $xml->fetchElementsFromRecord( $_table );
				$database['create'][]	= array(
											'name'		=> $table['name'],
											'fields'	=> $table['fields'],
											'tabletype'	=> $table['tabletype'],
											);
			}
		}

		foreach( $xml->fetchElements('hookextras_database_alter') as $node )
		{
			foreach( $xml->fetchElements('alter', $node) as $_table )
			{
				$table		= $xml->fetchElementsFromRecord( $_table );
				$database['alter'][]	= array(
											'altertype'	=> $table['altertype'],
											'table'		=> $table['table'],
											'field'		=> $table['field'],
											'newfield'	=> $table['newfield'],
											'fieldtype'	=> $table['fieldtype'],
											'default'	=> $table['default'],
											);
			}
		}

		foreach( $xml->fetchElements('hookextras_database_update') as $node )
		{
			foreach( $xml->fetchElements('update', $node) as $_table )
			{
				$table		= $xml->fetchElementsFromRecord( $_table );
				$database['update'][]	= array(
											'table'		=> $table['table'],
											'field'		=> $table['field'],
											'newvalue'	=> $table['newvalue'],
											'oldvalue'	=> $table['oldvalue'],
											'where'		=> $table['where'],
											);
			}
		}

		foreach( $xml->fetchElements('hookextras_database_insert') as $node )
		{
			foreach( $xml->fetchElements('insert', $node) as $_table )
			{
				$table		= $xml->fetchElementsFromRecord( $_table );
				$database['insert'][]	= array(
											'table'		=> $table['table'],
											'updates'	=> $table['updates'],
											'fordelete'	=> $table['fordelete'],
											);
			}
		}

		//-----------------------------------------
		// Set some vars for display tallies
		//-----------------------------------------

		$filesInserted			= 0;
		$settingGroupsInserted	= 0;
		$settingsInserted		= 0;
		$settingsUpdated		= 0;
		$languageInserted		= 0;
		$languageUpdated		= 0;
		$modulesInserted		= 0;
		$modulesUpdated			= 0;
		$helpInserted			= 0;
		$helpUpdated			= 0;
		$templatesInserted		= 0;
		$templatesUpdated		= 0;
		$templateHooks		    = false;
		$tasksInserted			= 0;
		$tasksUpdated			= 0;
		$createQueries			= 0;
		$alterQueries			= 0;
		$updateQueries			= 0;
		$insertQueries			= 0;
		$cssInserted			= 0;
		$cssUpdated				= 0;
		$replacementsInserted	= 0;
		$replacementsUpdated	= 0;
		
		//-----------------------------------------
		// Insert/update DB records
		//-----------------------------------------
		
		if( $this->hooks[ $config['hook_key'] ]['hook_id'] )
		{
			//-----------------------------------------
			// Don't change enabled/disabled status
			//-----------------------------------------
			
			unset( $config['hook_enabled'] );
			
			$this->DB->update( 'core_hooks', $config, 'hook_id=' . $this->hooks[ $config['hook_key'] ]['hook_id'] );
			
			$hook_id	= $this->hooks[ $config['hook_key'] ]['hook_id'];
			
			$extra_data	= unserialize( $this->hooks[ $config['hook_key'] ]['hook_extra_data'] );
			
			/* Reset DB */
			$extra_data['database'] = array();
		}
		else
		{
			$config['hook_installed']			= IPS_UNIX_TIME_NOW;
			$this->hook[ $config['hook_key'] ]	= $config;
			
			$this->DB->insert( 'core_hooks', $config );
			
			$hook_id	= $this->DB->getInsertId();
			
			$this->hook[ $config['hook_key'] ]['hook_id'] = $hook_id;
			
			$extra_data['display']	= $tempExtraData['display'];
		}

		if( count($files) )
		{
			//-----------------------------------------
			// If we are updating, remove old files
			//-----------------------------------------
			
			if( $this->hooks[ $config['hook_key'] ]['hook_id'] )
			{
				$this->DB->build( array( 'select' => 'hook_file_id, hook_file_stored', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . $this->hooks[ $config['hook_key'] ]['hook_id'] ) );
				$outer = $this->DB->execute();
				
				while( $r = $this->DB->fetch($outer) )
				{
					@unlink( IPS_HOOKS_PATH . $r['hook_file_stored'] );
					$this->DB->delete( 'core_hooks_files', 'hook_file_id=' . $r['hook_file_id'] );
				}
			}
				
			foreach( $files as $file )
			{
				//-----------------------------------------
				// Store new files
				//-----------------------------------------
				
				$filename	= $file['hook_classname'] . '_' . md5( uniqid( microtime(), true ) ) . '.php';
				
				file_put_contents( IPS_HOOKS_PATH . $filename, $file['hooks_source'] );
				chmod( IPS_HOOKS_PATH . $filename, IPS_FILE_PERMISSION );
				
				$file['hook_file_stored']	= $filename;
				$file['hook_hook_id']		= $hook_id;
				
				$this->DB->insert( 'core_hooks_files', $file );
				$filesInserted++;
				
				/* Need to recache skins? */
				if ( $file['hook_type'] == 'templateHooks' )
				{
					$templateHooks = true;
				}
			}
		}
		
		//-----------------------------------------
		// Put custom install/uninstall file
		//-----------------------------------------
		
		if( is_object($customClass) )
		{
			$extra_data['display']['custom'] = $custom['filename'];
			
			file_put_contents( IPS_HOOKS_PATH . 'install_' . $custom['filename'], $custom['source'] );
			chmod( IPS_HOOKS_PATH . 'install_' . $custom['filename'], IPS_FILE_PERMISSION );
		}
				
		//-----------------------------------------
		// (1) Settings
		//-----------------------------------------
		
		if( count($settingGroups) OR count($settings) )
		{
			$setting_groups = array();
			
			$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_settings_titles' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$setting_groups[ $r['conf_title_id'] ] = $r;
				$setting_groups_by_key[ $r['conf_title_keyword'] ] = $r;
			}
			
			//-----------------------------------------
			// Get current settings.
			//-----------------------------------------
			
			$this->DB->build( array( 'select' => 'conf_id, conf_key', 'from' => 'core_sys_conf_settings' ) );
			$this->DB->execute();
			
			while ( $r = $this->DB->fetch() )
			{
				$cur_settings[ $r['conf_key'] ] = $r['conf_id'];
			}
		}
			
		if( count($settingGroups) )
		{
			$need_to_update = array();
			
			foreach( $settingGroups as $data )
			{
				if ( $data['conf_title_title'] AND $data['conf_title_keyword'] )
				{
					//-----------------------------------------
					// Get ID based on key
					//-----------------------------------------
					
					$conf_id = $setting_groups_by_key[ $data['conf_title_keyword'] ]['conf_title_id'];
					
					$save = array( 'conf_title_title'   => $data['conf_title_title'],
								   'conf_title_desc'    => $data['conf_title_desc'],
								   'conf_title_keyword' => $data['conf_title_keyword'],
								   'conf_title_app'     => $data['conf_title_app'],
								   'conf_title_tab'		=> $data['conf_title_tab'],
								   'conf_title_noshow'  => $data['conf_title_noshow']  );
					
					//-----------------------------------------
					// Not got a row, insert first!
					//-----------------------------------------
					
					if ( ! $conf_id )
					{
						$this->DB->insert( 'core_sys_settings_titles', $save );
						$conf_id = $this->DB->getInsertId();
						$settingGroupsInserted++;
						$extra_data['settingGroups'][] = $save['conf_title_keyword'];
					}
					else
					{
						//-----------------------------------------
						// Update...
						//-----------------------------------------
						
						$this->DB->update( 'core_sys_settings_titles', $save, 'conf_title_id='.$conf_id );
					}
					
					//-----------------------------------------
					// Update settings cache
					//-----------------------------------------
					
					$save['conf_title_id']									= $conf_id;
					$setting_groups_by_key[ $save['conf_title_keyword'] ]	= $save;
					$setting_groups[ $save['conf_title_id'] ]				= $save;
						
					//-----------------------------------------
					// Set need update...
					//-----------------------------------------
					
					$need_update[] = $conf_id;
				}
			}
		}
		
		if( count($settings) )
		{
			foreach( $settings as $idx => $data )
			{
				$data['conf_group'] = $setting_groups_by_key[ $data['conf_title_keyword'] ]['conf_title_id'];
				
				//-----------------------------------------
				// Remove from array
				//-----------------------------------------
				
				unset( $data['conf_title_keyword'] );
				
				if ( $cur_settings[ $data['conf_key'] ] )
				{
					$this->DB->update( 'core_sys_conf_settings', $data, 'conf_id='.$cur_settings[ $data['conf_key'] ] );
					$settingsUpdated++;
				}
				else
				{
					$this->DB->insert( 'core_sys_conf_settings', $data );
					$settingsInserted++;
					$extra_data['settings'][] = $data['conf_key'];
				}
			}
		}
		
		//-----------------------------------------
		// Update group counts...
		//-----------------------------------------
		
		if ( count( $need_update ) )
		{
			foreach( $need_update as $i => $idx )
			{
				$conf = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count', 'from' => 'core_sys_conf_settings', 'where' => 'conf_group=' . $idx ) );
			
				$count = intval($conf['count']);
				
				$this->DB->update( 'core_sys_settings_titles', array( 'conf_title_count' => $count ), 'conf_title_id='.$idx );
			}
		}
		
		if( count($settingGroups) OR count($settings) )
		{
			$this->cache->rebuildCache( 'settings', 'global' );
		}

		//-----------------------------------------
		// (2) Languages
		//-----------------------------------------

		if( count($language) )
		{
			$langPacks	= array();
			
			$this->DB->build( array( 'select' => 'lang_id', 'from' => 'core_sys_lang' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$langPacks[] = $r['lang_id'];
			}

			foreach( $language as $langbit )
			{
				foreach( $langPacks as $lang_id )
				{
					
					$langbit['lang_id'] = $lang_id;
					
					// See if it exists
					$cnt = $this->DB->buildAndFetch( array( 'select' => 'word_id', 'from' => 'core_sys_lang_words', 'where' => "lang_id={$lang_id} AND word_app='{$langbit['word_app']}' AND word_key='{$langbit['word_key']}' AND word_pack='{$langbit['word_pack']}'" ) );
					
					if( $cnt['word_id'] )
					{
						$this->DB->update( 'core_sys_lang_words', $langbit, 'word_id=' . $cnt['word_id'] );
						$languageUpdated++;
					}
					else
					{
						$this->DB->insert( 'core_sys_lang_words', $langbit );
						$languageInserted++;
						$word_id = $this->DB->getInsertId();
					}
				
					$extra_data['language'][ $langbit['word_app'] . "_" . $langbit['word_pack'] ][]	= $langbit['word_key'];
				}
				
			}
			
			$classToLoad = IPSLib::loadActionOverloader( IPSLib::getAppDir('core') . '/modules_admin/languages/manage_languages.php', 'admin_core_languages_manage_languages' );
			$langLib     = new $classToLoad();
			$langLib->makeRegistryShortcuts( $this->registry );
			
			foreach( $langPacks as $langId )
			{
				$langLib->cacheToDisk( $langId );
			}
			
		}

		//-----------------------------------------
		// (3) Modules
		//-----------------------------------------
		
		if( count($modules) )
		{
			//-----------------------------------------
			// Get current modules
			//-----------------------------------------
			$cur_modules = array();
			$this->DB->build( array( 'select'	=> '*',
										'from'	=> 'core_sys_module',
										'order'	=> 'sys_module_id' ) );
			
			$this->DB->execute();
			
			while ( $r = $this->DB->fetch() )
			{
				$cur_modules[ $r['sys_module_application'] ][ $r['sys_module_key'] ] = $r['sys_module_id'];
			}
			
			foreach( $modules as $module )
			{
				//-----------------------------------------
				// Insert or update?
				//-----------------------------------------
			
				if ( $cur_modules[ $module['sys_module_application'] ][ $module['sys_module_key'] ] )
				{
					$this->DB->update( 'core_sys_module', $module, "sys_module_id=" . $cur_modules[ $module['sys_module_application'] ][ $module['sys_module_key'] ] );
					$modulesUpdated++;
				}
				else
				{
					$this->DB->insert( 'core_sys_module', $module );
					$modulesInserted++;
					
					$extra_data['modules'][] = ( $module['sys_module_admin'] ? 'admin' : 'public' ) . '-' . $module['sys_module_application'] . '-' . $module['sys_module_key'];
				}
			}
			
			$classToLoad = IPSLib::loadActionOverloader( IPSLib::getAppDir('core') . '/modules_admin/applications/applications.php', 'admin_core_applications_applications' );
			$moduleLib   = new $classToLoad();
			$moduleLib->makeRegistryShortcuts( $this->registry );
			$moduleLib->moduleRecache();
			$moduleLib->applicationsMenuDataRecache();
		}
		
		//-----------------------------------------
		// (4) Help Files
		//-----------------------------------------
		
		if( count($help) )
		{
			$keys		= array();
			
			$this->DB->build( array( 'select' => 'title', 'from' => 'faq' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$keys[] = $r['title'];
			}

			foreach( $help as $entry )
			{
				if( in_array( $entry['title'], $keys ) )
				{
					$this->DB->update( 'faq', $entry, "title='{$entry['title']}'" );
					$helpUpdated++;
				}
				else
				{	
					$this->DB->insert( 'faq', $entry );
					$helpInserted++;
					$help_id = $this->DB->getInsertId();
					$extra_data['help'][] = $entry['title'];
				}
			}
		}

		//-----------------------------------------
		// (6) Templates
		//-----------------------------------------
		
		if( count($templates) )
		{
			$bits		= array();
			
			/* Root will always be updated */
			$this->DB->build( array( 'select' => 'template_name,template_group,template_master_key', 'from' => 'skin_templates' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$bits[ $r['template_master_key'] ][ $r['template_group'] ][] = $r['template_name'];
			}

			foreach( $templates as $template )
			{
				//-----------------------------------------
				// If template is not in root set, add it to
				// prevent fatal error selecting other skins
				//-----------------------------------------
				
				if ( $template['template_master_key'] != 'root' && ( !isset($bits['root'][ $template['template_group'] ]) OR !in_array( $template['template_name'], $bits['root'][ $template['template_group'] ] ) ) )
				{
					$this->DB->insert( 'skin_templates', array_merge( $template, array( 'template_set_id' => 0, 'template_master_key' => 'root', 'template_content' => '' ) ) );
					
					$extra_data['templates'][ $template['template_group'] ][ $template['template_name'] ]	= $template['template_name'];
					$bits['root'][ $template['template_group'] ][]											= $template['template_name'];
				}
				
				//-----------------------------------------
				// Ignore unknown skin sets
				//-----------------------------------------
				
				if ( !isset($bits[ $template['template_master_key'] ]) )
				{
					continue;
				}
				
				if( isset($bits[ $template['template_master_key'] ][ $template['template_group'] ]) AND in_array( $template['template_name'], $bits[ $template['template_master_key'] ][ $template['template_group'] ] ) )
				{
					$template['template_updated'] = IPS_UNIX_TIME_NOW;

					$this->DB->update( 'skin_templates', $template, "template_master_key='{$template['template_master_key']}' AND template_group='{$template['template_group']}' AND template_name='{$template['template_name']}'" );
					$templatesUpdated++;
				}
				else
				{	
					$this->DB->insert( 'skin_templates', $template );
					$templatesInserted++;

					$extra_data['templates'][ $template['template_group'] ][ $template['template_name'] ]	= $template['template_name'];
					$bits[ $template['template_master_key'] ][ $template['template_group'] ][]				= $template['template_name'];
				}
			}
		}
		
		//-----------------------------------------
		// CSS
		//-----------------------------------------
		
		if( count( $css ) )
		{
			$bits		= array();
			
			$this->DB->build( array( 'select' => 'css_master_key, css_group', 'from' => 'skin_css' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$bits[ $r['css_master_key'] ][ $r['css_group'] ] = $r['css_group'];
			}

			foreach( $css as $entry )
			{
				//-----------------------------------------
				// Ignore unknown skin sets
				//-----------------------------------------
				
				if ( !isset( $bits[ $entry['css_master_key'] ] ) )
				{
					continue;
				}
				
				if( in_array( $entry['css_group'], $bits[ $entry['css_master_key'] ] ) )
				{
					$entry['css_updated']	= IPS_UNIX_TIME_NOW;

					$this->DB->update( 'skin_css', $entry, "css_master_key='{$entry['css_master_key']}' AND css_group='{$entry['css_group']}'" );
					$cssUpdated++;
				}
				else
				{	
					$this->DB->insert( 'skin_css', $entry );
					$cssInserted++;

					$extra_data['css'][ $entry['css_group'] ]					= $entry['css_group'];
					$bits[ $entry['css_master_key'] ][ $entry['css_group'] ]	= $entry['css_group'];
				}
			}
		}
		
		//-----------------------------------------
		// Skin Replacements
		//-----------------------------------------
				
		if( count( $replacements ) )
		{
			$bits		= array();
			
			$this->DB->build( array( 'select' => 'replacement_master_key, replacement_key', 'from' => 'skin_replacements' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$bits[ $r['replacement_master_key'] ][ $r['replacement_key'] ] = $r['replacement_key'];
			}
			
			foreach( $replacements as $entry )
			{
				if ( !isset( $bits[ $entry['replacement_master_key'] ] ) )
				{
					$entry['replacement_master_key'] = 'root';
				}
			
				if( in_array( $entry['replacement_key'], $bits[ $entry['replacement_master_key'] ] ) )
				{
					$this->DB->update( 'skin_replacements', $entry, "replacement_master_key='{$entry['replacement_master_key']}' AND replacement_key='{$entry['replacement_key']}'" );
					$replacementsUpdated++;
				}
				else
				{	
					$this->DB->insert( 'skin_replacements', $entry );
					$replacementsInserted++;

					$extra_data['replacements'][ $entry['replacement_key'] ]		= $entry['replacement_key'];
				}
			}
		}
		
		//-----------------------------------------
		// (7) Tasks
		//-----------------------------------------
		
		if( count($tasks) )
		{
			$keys		= array();
			
			$this->DB->build( array( 'select' => 'task_key', 'from' => 'task_manager' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$keys[] = $r['task_key'];
			}

			foreach( $tasks as $entry )
			{
				if( in_array( $entry['task_key'], $keys ) )
				{
					$this->DB->update( 'task_manager', $entry, "task_key='{$entry['task_key']}'" );
					$tasksUpdated++;
				}
				else
				{	
					$this->DB->insert( 'task_manager', $entry );
					$tasksInserted++;
					$task_id = $this->DB->getInsertId();
					$extra_data['tasks'][] = $entry['task_key'];
				}
			}
		}
		
		//-----------------------------------------
		// (8) Create new tables
		//-----------------------------------------
		
		if( count($database['create']) )
		{
			foreach($database['create'] as $create )
			{
				$query	= "CREATE TABLE {$this->settings['sql_tbl_prefix']}{$create['name']} (
							{$create['fields']}
							)";
				
				if( $create['tabletype'] )
				{
					$query .= " ENGINE=" . $create['tabletype'];
				}
				else if( $this->settings['sql_driver'] == 'mysql' AND $this->settings['mysql_tbl_type'] )
				{
					$query .= " ENGINE=" . $this->settings['mysql_tbl_type'];
				}
				
				//-----------------------------------------
				// Fix prefix
				//-----------------------------------------

				$this->DB->return_die = true;
				$this->DB->query( $query );
				$this->DB->return_die = false;
				$createQueries++;
				
				$extra_data['database']['create'][] = $create;
			}
		}

		//-----------------------------------------
		// (9) Alter tables
		//-----------------------------------------
		
		if( count($database['alter']) )
		{
			foreach( $database['alter'] as $alter )
			{
				$this->DB->return_die = true;
				
				$alter['default'] = ( !isset($alter['default']) || $alter['default'] === "" ) ? NULL : $alter['default'];
				
				switch( $alter['altertype'] )
				{
					case 'remove':
						$this->DB->dropField( $alter['table'], $alter['field'] );
					break;
					
					case 'add':
						$this->DB->addField( $alter['table'], $alter['field'], $alter['fieldtype'], $alter['default'] );
					break;
					
					case 'change':
						$this->DB->changeField( $alter['table'], $alter['field'], $alter['newfield'], $alter['fieldtype'], $alter['default'] );
					break;
				}
				
				$this->DB->return_die = false;
				$alterQueries++;
				$extra_data['database']['alter'][] = $alter;
			}
		}
		
		//-----------------------------------------
		// (10) Run update queries
		//-----------------------------------------
		
		if( count($database['update']) )
		{
			foreach( $database['update'] as $update )
			{
				$this->DB->return_die = true;
				$this->DB->update( $update['table'], array( $update['field'] => $update['newvalue'] ), html_entity_decode( $update['where'], ENT_QUOTES ) );
				$this->DB->return_die = false;
				$updateQueries++;
				$extra_data['database']['update'][] = $update;
			}
		}
		
		//-----------------------------------------
		// (11) Run insert queries
		//-----------------------------------------
		
	//	if( !$this->hooks[ $config['hook_key'] ]['hook_id'] )
	//	{
			if( count($database['insert']) )
			{
				foreach( $database['insert'] as $insert )
				{
					$fields		= array();
					$content	= explode( ',', $insert['updates'] );
					
					foreach( $content as $value )
					{
						list( $field, $toInsert ) = explode( '=', $value );
						
						$fields[ trim($field) ] = str_replace( '~C~', ',', $toInsert );
					}

					$this->DB->return_die = true;
					$this->DB->insert( $insert['table'], $fields );
					$this->DB->return_die = false;
					$insertQueries++;
					$extra_data['database']['insert'][] = $insert;
				}
			}
	//	}
		
		/* Got an install function to run too? */
		if( is_object($customClass) )
		{
			if( method_exists( $customClass, 'install' ) )
			{
				$customClass->install();
			}
		}

		if( count($extra_data) )
		{
			$this->DB->update( 'core_hooks', array( 'hook_extra_data' => serialize( $extra_data ) ), 'hook_id=' . $hook_id );
		}
					
		//print_r($config);
		//print_r($files);
		//print_r($custom);
		//print_r($settingGroups);
		//print_r($settings);
		//print_r($language);
		//print_r($modules);
		//print_r($templates);
		//print_r($tasks);
		//print_r($help);
		//print_r($database);

		if ( $addMessage )
		{
			$_tempMsg = <<<EOF
		<strong>{$this->lang->words['h_newhookin']}</strong>
		<ul>
			<li>{$filesInserted} {$this->lang->words['h_filesin']}</li>
			<li>{$settingGroupsInserted} {$this->lang->words['h_settinggin']}</li>
			<li>{$settingsInserted} {$this->lang->words['h_settingin']}</li>
			<li>{$settingsUpdated} {$this->lang->words['h_settingup']}</li>
			<li>{$languageInserted} {$this->lang->words['h_langbitin']}</li>
			<li>{$languageUpdated} {$this->lang->words['h_langbitup']}</li>
			<li>{$modulesInserted} {$this->lang->words['h_modin']}</li>
			<li>{$modulesUpdated} {$this->lang->words['h_modup']}</li>
			<li>{$helpInserted} {$this->lang->words['h_helpin']}</li>
			<li>{$helpUpdated} {$this->lang->words['h_helpup']}</li>
			<li>{$templatesInserted} {$this->lang->words['h_tempin']}</li>
			<li>{$templatesUpdated} {$this->lang->words['h_tempup']}</li>
			<li>{$cssInserted} {$this->lang->words['h_cssin']}</li>
			<li>{$cssUpdated} {$this->lang->words['h_cssup']}</li>
			<li>{$replacementsInserted} {$this->lang->words['h_replacementsin']}</li>
			<li>{$replacementsUpdated} {$this->lang->words['h_replacementsup']}</li>
			<li>{$tasksInserted} {$this->lang->words['h_taskin']}</li>
			<li>{$tasksUpdated} {$this->lang->words['h_taskup']}</li>
			<li>{$createQueries} {$this->lang->words['h_dbcreated']}</li>
			<li>{$alterQueries} {$this->lang->words['h_dbaltered']}</li>
			<li>{$updateQueries} {$this->lang->words['h_updateran']}</li>
			<li>{$insertQueries} {$this->lang->words['h_insertran']}</li>
		</ul>
EOF;

			$this->registry->output->setMessage( $_tempMsg, 1 );
		}
		
		//-----------------------------------------
		// Got some skin recaching to do...
		//-----------------------------------------
		
		if( $allowSkinRecache === TRUE AND ( $templatesInserted OR $templatesUpdated OR $templateHooks OR $cssUpdated OR $cssInserted ) )
		{
			/* Rebuild cache */
			$this->rebuildHooksCache();
			
			/* Flag skins for recache */
			require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
			require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
			$skinCaching = new skinCaching( $this->registry );
			$skinCaching->flagSetForRecache();
		}
	}

	/**
	 * Show the form to export a hook
	 *
	 * @return   void
	 */
	protected function _exportHook()
	{
		/* Get the hook */
		$id	= intval($this->request['id']);
		
		if( !$id )
		{
			$this->registry->output->showError( $this->lang->words['h_noexport'], 1112 );
		}
		
		$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_noexport'], 1113 );
		}
		
		/* Set defaults */
		$hookData['hook_extra_data'] = unserialize( $hookData['hook_extra_data'] );

		/* Output */
		$this->registry->output->extra_nav[] = array( $this->settings['base_url'] . $this->form_code . 'do=hooks_overview', $this->lang->words['menu__manage_hooks'] );
		$this->registry->output->html .= $this->html->hooksExport( $hookData );
	}
	
	/**
	 * Actually export the damn hook already
	 * Sorry, it has been a long day...
	 *
	 * @return	@e void
	 */
	protected function _doExportHook()
	{
		//-----------------------------------------
		// Get hook
		//-----------------------------------------
		
		$id	= intval($this->request['id']);
		
		if( !$id )
		{
			$this->registry->output->showError( $this->lang->words['h_noexport'], 1114 );
		}
		
		$hookData	= $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_noexport'], 1115 );
		}
		
		$extra_data	= unserialize( $hookData['hook_extra_data'] );
		
		//-----------------------------------------
		// Get hook files
		//-----------------------------------------
		
		$files = array();
		$index = 1;
		
		$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . $id, 'order' => 'hook_file_id ASC' ) );
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			$files[ $index ]	= $r;
			$index++;
		}

		//-----------------------------------------
		// Get XML class
		//-----------------------------------------
		
		require_once( IPS_KERNEL_PATH.'classXML.php' );/*noLibHook*/

		$xml = new classXML( IPS_DOC_CHAR_SET );
		
		$xml->newXMLDocument();
		$xml->addElement( 'hookexport' );

		//-----------------------------------------
		// Put hook data in export
		//-----------------------------------------
		
		$xml->addElement( 'hookdata', 'hookexport' );
		$content	= array();
		
		foreach( $hookData as $k => $v )
		{
			if( in_array( $k, array( 'hook_id', 'hook_enabled', 'hook_installed', 'hook_updated', 'hook_position' ) ) )
			{
				continue;
			}
			
			$content[ $k ] = $v;
		}
		
		$xml->addElementAsRecord( 'hookdata', 'config', $content );
		
		//-----------------------------------------
		// Put hook files in export
		//-----------------------------------------
		
		$xml->addElement( 'hookfiles', 'hookexport' );

		foreach( $files as $index => $r )
		{
			$content	= array();
			
			foreach( $r as $k => $v )
			{
				if( in_array( $k, array( 'hook_file_id', 'hook_hook_id', 'hook_file_stored', 'hooks_source' ) ) )
				{
					continue;
				}
				
				$content[ $k ] = $v;
			}
			
			$source	= is_file( IPS_HOOKS_PATH . $r['hook_file_stored'] ) ? file_get_contents( IPS_HOOKS_PATH . $r['hook_file_stored'] ) : '';
			
			if ( !empty($source) && in_array( $r['hook_type'], array( 'skinHooks', 'commandHooks', 'libraryHooks' ) ) )
			{
				$source	= $this->_cleanSource( $source );
			}

			$content['hooks_source'] = $source;

			$xml->addElementAsRecord( 'hookfiles', 'file', $content );
		}

		//-----------------------------------------
		// Custom install/uninstall script?
		//-----------------------------------------
		
		if( $extra_data['custom'] )
		{
			$content	= array();
			$xml->addElement( 'hookextras_custom', 'hookexport' );
		
			$content['filename']	= $extra_data['custom'];
			$content['source']		= is_file( IPS_HOOKS_PATH . 'install_' . $extra_data['custom'] ) ? file_get_contents( IPS_HOOKS_PATH . 'install_' . $extra_data['custom'] ) : '';
			
			$xml->addElementAsRecord( 'hookextras_custom', 'file', $content );
		}
		
		//-----------------------------------------
		// Settings or setting groups?
		//-----------------------------------------
		
		$entry		= array();
		$_groups	= array();
		$_settings	= array();
		$titles		= array();
		$content	= array();
		
		$xml->addElement( 'hookextras_settings', 'hookexport' );
		
		# Store group ids and setting ids for entire setting groups
		if( is_array($extra_data['settingGroups']) AND count($extra_data['settingGroups']) )
		{
			$_groups	= $extra_data['settingGroups'];
			$_groupIds	= array();
			
			$this->DB->build( array( 'select' => 'conf_title_id', 'from' => 'core_sys_settings_titles', 'where' => "conf_title_keyword IN('" . implode( "','", $_groups ) . "')" ) );
			$this->DB->execute();
			
			while( $rr = $this->DB->fetch() )
			{
				$_groupIds[] = $rr['conf_title_id'];
			}
			
			if( count($_groupIds) )
			{
				$this->DB->build( array( 'select' => 'conf_key', 'from' => 'core_sys_conf_settings', 'where' => 'conf_group IN(' . implode( ',', $_groupIds ) . ')' ) );
				$this->DB->execute();
				
				while( $setting = $this->DB->fetch() )
				{
					$_settings[] = $setting['conf_key'];
				}
			}
		}
		
		# Store group ids and setting ids for indvidual settings
		if( is_array($extra_data['settings']) AND count($extra_data['settings']) )
		{
			foreach( $extra_data['settings'] as $_aSetting )
			{
				$_settings[] = $_aSetting;
			}
			
			$this->DB->build( array( 'select'	=> 't.conf_title_keyword', 
									 'from'		=> array( 'core_sys_settings_titles' => 't' ), 
									 'where'	=> "c.conf_key IN('" . implode( "','", $extra_data['settings'] ) . "')",
									 'add_join'	=> array(
											 			array(
											 				'from'	=> array( 'core_sys_conf_settings' => 'c' ),
											 				'where'	=> 'c.conf_group=t.conf_title_id',
											 				'type'	=> 'left',
											 				)
									 					)
							)		);
			$this->DB->execute();
			
			while( $group = $this->DB->fetch() )
			{
				$_groups[] = $group['conf_title_keyword'];
			}
		}
		
		if( count($_groups) )
		{
			# Now get the group data for the XML file
			$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_settings_titles', 'where' => "conf_title_keyword IN('" . implode( "','", $_groups ) . "')" ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$content	= array();
				
				$titles[ $r['conf_title_id'] ] = $r['conf_title_keyword'];
				
				$content['conf_is_title']	= 1;
				
				foreach( $r as $k => $v )
				{
					if( in_array( $k, array( 'conf_title_tab', 'conf_title_keyword', 'conf_title_title', 'conf_title_desc', 'conf_title_app', 'conf_title_noshow' ) ) )
					{
						$content[ $k ] = $v;
					}
				}

				$xml->addElementAsRecord( 'hookextras_settings', 'setting', $content );
			}
		}
		
		if( count($_settings) )
		{
			# Now get the group data for the XML file
			$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_conf_settings', 'where' => "conf_key IN('" . implode( "','", $_settings ) . "')" ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$r['conf_value']			= '';
				$r['conf_title_keyword']	= $titles[ $r['conf_group'] ];
				$r['conf_is_title']			= 0;

				$xml->addElementAsRecord( 'hookextras_settings', 'setting', $r );
			}
		}

		//-----------------------------------------
		// Language strings/files
		//-----------------------------------------
		
		$entry		= array();
		
		$xml->addElement( 'hookextras_language', 'hookexport' );
		
		if( is_array($extra_data['language']) AND count($extra_data['language']) )
		{
			foreach( $extra_data['language'] as $file => $strings )
			{
				$bits = explode( '_', $file );
				$app  = $bits[0];
				$pack = str_replace( $app.'_', '', $file );
				
				$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_lang_words', 'where' => "word_app='{$app}' AND word_pack='{$pack}' AND word_key IN('" . implode( "','", $strings ) . "')" ) );
				$this->DB->execute();
				
				while( $r = $this->DB->fetch() )
				{
					$content	= array();
					
					foreach( $r as $k => $v )
					{
						if( !in_array( $k, array( 'word_app', 'word_pack', 'word_key', 'word_default' ) ) )
						{
							continue;
						}
						
						$content[ $k ] = $v;
					}
	
					$xml->addElementAsRecord( 'hookextras_language', 'language', $content );
				}
			}
		}

		//-----------------------------------------
		// Modules
		//-----------------------------------------

		$xml->addElement( 'hookextras_modules', 'hookexport' );
		
		if( is_array($extra_data['modules']) AND count($extra_data['modules']) )
		{
			foreach ( $extra_data['modules'] as $module )
			{
				list( $_side, $_app, $_module ) = explode( '-', $module );
				$_is_admin = ( $_side == 'admin' ) ? 1 : 0;
				
				$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_module', 'where' => "sys_module_application='{$_app}' AND sys_module_key='{$_module}' AND sys_module_admin={$_is_admin}" ) );
				$this->DB->execute();
				
				while( $r = $this->DB->fetch() )
				{
					unset($r['sys_module_id']);
					
					$xml->addElementAsRecord( 'hookextras_modules', 'module', $r );
				}
			}
		}
		
		//-----------------------------------------
		// Help files
		//-----------------------------------------

		$xml->addElement( 'hookextras_help', 'hookexport' );
		
		if( is_array($extra_data['help']) AND count($extra_data['help']) )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'faq', 'where' => "title IN('" . implode( "','", $extra_data['help'] ) . "')" ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				unset($r['id']);

				$xml->addElementAsRecord( 'hookextras_help', 'help', $r );
			}
		}
		
		//-----------------------------------------
		// Skin templates
		//-----------------------------------------

		$remapData	= $this->registry->output->buildRemapData( true );

		$xml->addElement( 'hookextras_templates', 'hookexport' );
				
		if( is_array($extra_data['templates']) AND count($extra_data['templates']) )
		{
			foreach( $extra_data['templates'] as $file => $templates )
			{
				$this->DB->build( array( 'select' => '*', 'from' => 'skin_templates', 'where' => "template_set_id IN ('" . implode( "','", $remapData['export'] ) . "') AND template_master_key != '' AND template_group='{$file}' AND template_name IN('" . implode( "','", $templates ) . "')" ) );
				$this->DB->execute();
				
				while( $r = $this->DB->fetch() )
				{
					unset($r['template_id']);
					unset($r['template_set_id']);
	
					$xml->addElementAsRecord( 'hookextras_templates', 'templates', $r );
				}
			}
		}
		
		//-----------------------------------------
		// CSS
		//-----------------------------------------

		$xml->addElement( 'hookextras_css', 'hookexport' );
				
		if( is_array( $extra_data['css'] ) AND count ( $extra_data['css'] ) )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'skin_css', 'where' => "css_set_id IN ('" . implode( "','", $remapData['export'] ) . "') AND css_master_key != '' AND css_group IN('" . implode( "','", $extra_data['css'] ) . "')" ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				unset($r['css_id']);
				unset($r['css_set_id']);

				$xml->addElementAsRecord( 'hookextras_css', 'css', $r );
			}
		}
		
		//-----------------------------------------
		// Replacements
		//-----------------------------------------

		$xml->addElement( 'hookextras_replacements', 'hookexport' );
				
		if( is_array( $extra_data['replacements'] ) AND count ( $extra_data['replacements'] ) )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'skin_replacements', 'where' => "replacement_key IN('" . implode( "','", $extra_data['replacements'] ) . "')", 'group' => 'replacement_key' ) );
			
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				unset($r['replacement_id']);
				unset($r['replacement_set_id']);

				$xml->addElementAsRecord( 'hookextras_replacements', 'replacements', $r );
			}
		}

		//-----------------------------------------
		// Tasks
		//-----------------------------------------

		$xml->addElement( 'hookextras_tasks', 'hookexport' );
				
		if( is_array($extra_data['tasks']) AND count($extra_data['tasks']) )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'task_manager', 'where' => "task_key IN('" . implode( "','", $extra_data['tasks'] ) . "')" ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				unset($r['task_id']);
				unset($r['task_next_run']);

				$xml->addElementAsRecord( 'hookextras_tasks', 'tasks', $r );
			}
		}

		//-----------------------------------------
		// Database changes
		//-----------------------------------------

		$xml->addElement( 'hookextras_database_create', 'hookexport' );
		
		if( is_array($extra_data['database']['create']) AND count($extra_data['database']['create']) )
		{
			foreach( $extra_data['database']['create'] as $create_query )
			{
				$xml->addElementAsRecord( 'hookextras_database_create', 'create', $create_query );
			}
		}

		$xml->addElement( 'hookextras_database_alter', 'hookexport' );
		
		if( is_array($extra_data['database']['alter']) AND count($extra_data['database']['alter']) )
		{
			foreach( $extra_data['database']['alter'] as $alter_query )
			{
				$xml->addElementAsRecord( 'hookextras_database_alter', 'alter', $alter_query );
			}
		}

		$xml->addElement( 'hookextras_database_update', 'hookexport' );
		
		if( is_array($extra_data['database']['update']) AND count($extra_data['database']['update']) )
		{
			foreach( $extra_data['database']['update'] as $update_query )
			{
				$xml->addElementAsRecord( 'hookextras_database_update', 'update', $update_query );
			}
		}

		$xml->addElement( 'hookextras_database_insert', 'hookexport' );
		
		if( is_array($extra_data['database']['insert']) AND count($extra_data['database']['insert']) )
		{
			foreach( $extra_data['database']['insert'] as $insert_query )
			{
				$xml->addElementAsRecord( 'hookextras_database_insert', 'insert', $insert_query );
			}
		}

		//-----------------------------------------
		// Print to browser
		//-----------------------------------------

		$this->registry->output->showDownload( $xml->fetchDocument(), $hookData['hook_key'] . '.xml', '', 0 );
	}
	
	/**
	 * Clean source code for export..
	 *
	 * @param	string			Hook source code
	 * @return	string			"Cleaned" source code
	 */
	protected function _cleanSource( $source )
	{
		$source = preg_replace( "/class\s+(\S+)\s+extends\s+(\S+)/i", "class \\1 extends (~extends~)", $source );
		
		return $source;
	}
	
	/**
	 * Fix hook positions
	 *
	 * @return   @e void
	 */
	protected function _fixPositions()
	{
		/* Init vars */
		$new_order   = 0;
		$usedActions = array();

		$this->DB->build( array( 'select' => 'hook_id, hook_position', 'from' => 'core_hooks', 'where' => 'hook_enabled=1', 'order' => 'hook_position ASC' ) );
		$qid = $this->DB->execute();

		while ( $hook = $this->DB->fetch( $qid ) )
		{
			$new_order++;
			$this->DB->update( 'core_hooks', array ( 'hook_position' => $new_order ), "hook_id={$hook['hook_id']}" );
			
			$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks_files', 'where' => "hook_type IN ('commandHooks', 'skinHooks', 'libraryHooks' ) AND hook_hook_id=" . $hook['hook_id'] ) );
			$this->DB->execute();
		
			while( $file = $this->DB->fetch() )
			{
				if( $file['hooks_source'] )
				{
					$source = $this->_cleanSource( $file['hooks_source'] );
				}
				else
				{
					$source = is_file( IPS_HOOKS_PATH . $file['hook_file_stored'] ) ? file_get_contents( IPS_HOOKS_PATH . $file['hook_file_stored'] ) : '';
					
					if( $source )
					{
						 $source = $this->_cleanSource( $source );
					}
				}
				
				if( $source )
				{
					$hook_data	= unserialize( $file['hook_data'] );
					$overload	= $hook_data['classToOverload'];
					$newClass	= $overload;
					
					$app = ( $file['hook_type'] == 'libraryHooks' ) ? trim($hook_data['libApplication']) : '~_NO_APP_~';
					
					if( isset( $usedActions[ $app ][ $overload ] ) )
					{
						$newClass = $usedActions[ $app ][ $overload ];
					}
					else if( $file['hook_type'] == 'skinHooks' )
					{
						$newClass .= "(~id~)";
					}
					
					$source = str_replace( "(~extends~)", $newClass, $source );
					
					file_put_contents( IPS_HOOKS_PATH . $file['hook_file_stored'], $source );
					
					$usedActions[ $app ][ $hook_data['classToOverload'] ] = $file['hook_classname'];
				}
			}
		}
	}
	
	/**
	 * Check if there is an update for a hook
	 *
	 * @param	string 		URL to check
	 * @param	string		Long version number
	 * @return	bool
	 */
	public function _updateAvailable( $url, $version )
	{
		if( empty($url) || $version === '' )
		{
			return false;
		}
		
		$classToLoad = IPSLib::loadLibrary( IPS_KERNEL_PATH . '/classFileManagement.php', 'classFileManagement' );
		$checker = new $classToLoad();
		
		/* Timeout to prevent page from taking too long */
		$checker->timeout = 5;
		
		/* Setup url and check */
		$url  = str_replace( 'php&', 'php?', $url );
		$url .= '&boardVersion=' . IPB_LONG_VERSION . '&version=' . $version;
		
		$return = $checker->getFileContents( $url );
		
		/* Return the content (if valid) or return 'no update' */
		$return = ( strpos( $return, '0' ) === 0 || strpos( $return, '1' ) === 0 ) ? explode( '|', $return ) : array( 0 );
				
		/* Replace {IPSID} id necessary */
		if ( strpos( $return[1], '{IPSID}' ) )
		{
			if ( ipsRegistry::$settings['ipb_reg_number'] )
			{
				$exploded = explode( '-', ipsRegistry::$settings['ipb_reg_number'] );
				$return[1] = str_replace( '{IPSID}', $exploded[3], $return[1] );
			}
			else
			{
				$return[1] = "https://www.invisionpower.com/clients";
			}
		}
		
		return $return;
	}
	
	/**
	 * Sorts hooks by their position
	 * (this is used to avoid a mysql filesort)
	 * 
	 * @param	array		$a		First hook data
	 * @param	array		$b		Second hook data
	 * @return	@e integer
	 */
	protected function sortHooksByPosition( $a, $b )
	{
		if ( $a['hook_position'] == $b['hook_position'] )
		{
			return 0;
		}
		
		return ( $a['hook_position'] < $b['hook_position'] ) ? -1 : 1;
	}
	
	/**
	 * Hooks overview
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	public function _hooksOverview()
	{
		/* INI */
		$hooksFound			= array( 'installed' => array(), 'uninstalled' => array() );
		$hookwarnings		= 0;
		$hookUpdates		= 0;
		$checkUpdates		= !empty( $this->request['checkUpdates'] ) ? true : false;
		$message			= '';
		$confGroupIdsToLoad	= array();
		$confGroupKeysToLoad= array();
		
		/* Get current hooks */
		$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks' ) );
		$outer	= $this->DB->execute();
		
		while( $r = $this->DB->fetch($outer) )
		{
			/* Format some dates */
			$r['_updated']   = $this->registry->getClass('class_localization')->formatTime( $r['hook_updated'] );
			$r['_installed'] = $this->registry->getClass('class_localization')->formatTime( $r['hook_installed'] );
			
			/* Got updates? */
			if ( $r['hook_update_check'] && $checkUpdates === true )
			{
				$r['hook_update_available'] = $this->_updateAvailable( $r['hook_update_check'], $r['hook_version_long'] );
				
				if ( $r['hook_update_available'][0] )
				{
					$hookUpdates++;
				}
			}
			else
			{
				$r['hook_update_available']	= array( 0 );
			}
			
			/* Got some setting groups to take care of? */
			$r['_has_setting_links'] = null;
			$r['_hook_extra_data']   = IPSLib::isSerialized($r['hook_extra_data']) ? unserialize($r['hook_extra_data']) : array();
			
			if ( is_array($r['_hook_extra_data']['settingGroups']) && count($r['_hook_extra_data']['settingGroups']) )
			{
				$r['_has_setting_links'] = array();
				
				foreach( $r['_hook_extra_data']['settingGroups'] as $_group )
				{
					$confGroupKeysToLoad[ $_group ] = $_group;
				}
			}
			
			if ( is_array($r['_hook_extra_data']['settings']) && count($r['_hook_extra_data']['settings']) )
			{
				$r['_has_setting_links'] = array();
				
				$this->DB->build( array( 'select' => 'conf_group, conf_key', 'from' => 'core_sys_conf_settings', 'where' => "conf_key IN ('" . implode( "','", $r['_hook_extra_data']['settings'] ) . "')" ) );
				$this->DB->execute();
				
				while( $hsg = $this->DB->fetch() )
				{
					$confGroupIdsToLoad[ $hsg['conf_key'] ] = $hsg['conf_group'];
				}
			}
			
			/* Check requirements */
			$r['_require_errors'] = $this->checkHookRequirements( $r );
			
			/* Store */
			if( $r['hook_enabled'] )
			{
				if ( count($r['_require_errors']) )
				{
					$hookwarnings++;
				}
				
				$hooksFound['installed'][ $r['hook_id'] ]	= $r;
			}
			else
			{
				$hooksFound['uninstalled'][ $r['hook_id'] ]	= $r;
			}
		}
		
		/* Got groups to load? */
		$where = array();
		
		if ( count($confGroupKeysToLoad) )
		{
			$where[] = "conf_title_keyword IN ('" . implode( "','", $confGroupKeysToLoad ) . "')";
		}
		
		if ( count($confGroupIdsToLoad) )
		{
			$where[] = "conf_title_id IN (" . implode( ",", $confGroupIdsToLoad ) . ")";
		}
		
		if ( count($where) )
		{
			$confGroupsLoaded = array();
			
			$this->DB->build( array( 'select' => '*', 'from' => 'core_sys_settings_titles', 'where' => implode( ' OR ', $where ) ) );
			$this->DB->execute();
			
			while( $gr = $this->DB->fetch() )
			{
				if ( empty($gr['conf_title_noshow']) )
				{
					$confGroupsLoaded['ids'][ $gr['conf_title_id'] ]		= $gr;
					$confGroupsLoaded['keys'][ $gr['conf_title_keyword'] ]	= & $confGroupsLoaded['ids'][ $gr['conf_title_id'] ];
				}
			}
			
			/* Merge back the data */
			foreach( $hooksFound as $rk => $rt )
			{
				foreach( $rt as $sk => $st )
				{
					if ( is_array($st['_has_setting_links']) )
					{
						if ( is_array($st['_hook_extra_data']['settingGroups']) && count($st['_hook_extra_data']['settingGroups']) )
						{
							foreach( $st['_hook_extra_data']['settingGroups'] as $_group )
							{
								if ( isset($confGroupsLoaded['keys'][ $_group ]) )
								{
									$hooksFound[ $rk ][ $sk ]['_has_setting_links'][ $confGroupsLoaded['keys'][ $_group ]['conf_title_id'] ] = $confGroupsLoaded['keys'][ $_group ]['conf_title_title'];
								}
							}
						}
						
						if ( is_array($st['_hook_extra_data']['settings']) && count($st['_hook_extra_data']['settings']) )
						{
							foreach( $st['_hook_extra_data']['settings'] as $_id )
							{
								if ( isset($confGroupsLoaded['ids'][ $confGroupIdsToLoad[ $_id ] ]) )
								{
									$hooksFound[ $rk ][ $sk ]['_has_setting_links'][ $confGroupIdsToLoad[ $_id ] ] = $confGroupsLoaded['ids'][ $confGroupIdsToLoad[ $_id ] ]['conf_title_title'];
								}
							}
						}
					}
				}
			}
		}
		
		/* Got updates to show? */
		if ( $checkUpdates === true )
		{
			$message = ( $hookUpdates == 1 ) ? $this->lang->words['updates_string_single'] : sprintf( $this->lang->words['updates_string_more'], $hookUpdates );
		}
		
		if ( count($hooksFound['installed']) )
		{
			uasort( $hooksFound['installed'], array( $this, 'sortHooksByPosition' ) );
		}
		
		/* Output */
		$this->registry->output->html .= $this->html->hooksOverview( $hooksFound, $hookwarnings, $message );
	}
	
	/**
	 * Disables all hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _disableAllHooks()
	{
		/* INIT */
		$activeHookIds = array();
		
		/* Query Enabled Hooks */
		$this->DB->build( array( 'select' => 'hook_id', 'from' => 'core_hooks', 'where' => "hook_enabled=1" ) );
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			$activeHookIds[] = $r['hook_id'];
		}
		
		/* Make sure there were active hooks */
		if( ! count( $activeHookIds ) )
		{
			$this->registry->output->global_message = $this->lang->words['hook_disable_all_none'];
			$this->_hooksOverview();
			return;
		}
		
		/* Disable hooks */
		$this->DB->update( 'core_hooks', array( 'hook_enabled' => 0 ), 'hook_id IN( ' . implode( ',', $activeHookIds ) . ' ) ' );
		
		/* Save to cache */
		$this->cache->setCache( 'disabledHooksCache', $activeHookIds, array( 'array' => 1 ) );
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* All Done */
		$this->registry->output->global_message = $this->lang->words['hook_disabled_all'];
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * Re-enable all hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _reenableAllHooks()
	{
		/* INIT */
		$activeHookIds = $this->cache->getCache( 'disabledHooksCache' );
		
		/* Make sure there were active hooks */
		if( ! count( $activeHookIds ) )
		{
			$this->registry->output->global_message = $this->lang->words['hook_enable_all_none'];
			$this->_hooksOverview();
			return;
		}
		
		/* Disable hooks */
		$this->DB->update( 'core_hooks', array( 'hook_enabled' => 1 ), 'hook_id IN( ' . implode( ',', $activeHookIds ) . ' ) ' );
		
		/* Save to cache */
		$this->cache->setCache( 'disabledHooksCache', array(), array( 'array' => 1 ) );
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		
		/* Flag all for recache */
		$skinCaching->flagSetForRecache();
		
		/* All Done */
		$this->registry->output->global_message = $this->lang->words['hook_reenable_all'];
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * Disables a hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _disableHook()
	{
		/* INI */
		$hook_id = intval( $this->request['id'] );
		
		if( !$hook_id )
		{
			$this->registry->output->showError( $this->lang->words['h_noedit'], '1118.D' );
		}
		
		$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $hook_id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_noedit'], '1119.D' );
		}
		
		/* Do update */
		$this->DB->update( 'core_hooks', array( 'hook_enabled' => 0, 'hook_position' => 0 ), "hook_id={$hookData['hook_id']}" );
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Rebuild global caches */
		try
		{
			IPSLib::cacheGlobalCaches();
		}
		catch( Exception $e ){}
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		$skinCaching->flagSetForRecache();
		
		/* Redirect */
		$this->registry->output->setMessage( $this->lang->words['h_hasbeendisabled'] );
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}	
	
	/**
	 * Enables a hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _enableHook()
	{
		/* INI */
		$hook_id = intval($this->request['id']);
		
		if( !$hook_id )
		{
			$this->registry->output->showError( $this->lang->words['h_noedit'], '1118.E' );
		}
		
		$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $hook_id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_noedit'], '1119.E' );
		}
		
		/* Check for requirements */
		if ( empty($this->request['skipRequirements']) )
		{
			$errors = $this->checkHookRequirements( $hookData );
			
			if ( count($errors) )
			{
				$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=check_requirements&amp;fromInstall=1&amp;id='.$hook_id );
			}
		}
		
		/* Do update */
		$this->DB->update( 'core_hooks', array( 'hook_enabled' => 1 ), "hook_id={$hookData['hook_id']}" );
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Rebuild global caches */
		try
		{
			IPSLib::cacheGlobalCaches();
		}
		catch( Exception $e ){}
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		$skinCaching->flagSetForRecache();
		
		/* Redirect */
		$this->registry->output->global_message = $this->lang->words['h_hasbeenenabled'];
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * Uninstall a hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _uninstallHook()
	{
		/* Init vars */
		$hook_id	= intval( $this->request['id'] );
		
		$hook		= $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $hook_id ) );
		$extra_data	= unserialize( $hook['hook_extra_data'] );
		
		if ( ! $hook['hook_id'] )
		{
			$this->registry->output->global_message = $this->lang->words['h_hasbeenun'];
			$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
		}
		
		/* Got any custom file to initialize? */
		$_customFile = '';
		$customClass = null;
		
		if ( $extra_data['display']['custom'] )
		{
			$_customFile = IPS_HOOKS_PATH . 'install_' . $extra_data['display']['custom'];
			
			if( is_file( $_customFile ) )
			{
				require_once( $_customFile );/*noLibHook*/
				
				$classname = str_replace( '.php', '', $extra_data['display']['custom'] );
				
				if( class_exists( $classname ) )
				{
					$customClass = new $classname( $this->registry );
					
					if( method_exists( $customClass, 'pre_uninstall' ) )
					{
						$customClass->pre_uninstall();
					}
				}
			}
		}
		
		/* Get associated files */
		$this->DB->build( array( 'select' => 'hook_file_stored', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . $hook_id ) );
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			@unlink( IPS_HOOKS_PATH . $r['hook_file_stored'] );
		}
		
		/* Delete hook file entries */
		$this->DB->delete( 'core_hooks_files', "hook_hook_id={$hook_id}" );
		
		//-----------------------------------------
		// Settings or setting groups?
		//-----------------------------------------
		
		if( count($extra_data['settingGroups']) )
		{
			$this->DB->delete( 'core_sys_settings_titles', "conf_title_keyword IN('" . implode( "','", $extra_data['settingGroups'] ) . "')" );
		}
		
		if( count($extra_data['settings']) )
		{
			$this->DB->delete( 'core_sys_conf_settings', "conf_key IN('" . implode( "','", $extra_data['settings'] ) . "')" );
		}
		
		$this->cache->rebuildCache( 'settings', 'global' );
		
		//-----------------------------------------
		// Language strings/files
		//-----------------------------------------
		
		if( count($extra_data['language']) )
		{
			foreach( $extra_data['language'] as $file => $bit )
			{
				$this->DB->delete( 'core_sys_lang_words', "word_pack='{$file}' AND word_key IN('" . implode( "','", $bit ) . "')" );
			}
			
			$langPacks	= array();
			
			$this->DB->build( array( 'select' => 'lang_id', 'from' => 'core_sys_lang' ) );
			$this->DB->execute();
			
			while( $r = $this->DB->fetch() )
			{
				$langPacks[] = $r['lang_id'];
			}
			
			$classToLoad = IPSLib::loadActionOverloader( IPSLib::getAppDir('core') . '/modules_admin/languages/manage_languages.php', 'admin_core_languages_manage_languages' );
			$langLib     = new $classToLoad();
			$langLib->makeRegistryShortcuts( $this->registry );
			
			foreach( $langPacks as $langId )
			{
				$langLib->cacheToDisk( $langId );
			}
		}
		
		//-----------------------------------------
		// Modules
		//-----------------------------------------
		
		if( count($extra_data['modules']) )
		{
			foreach( $extra_data['modules'] as $module )
			{
				list( $_side, $_app, $_module ) = explode( '-', $module );
				$_is_admin = ( $_side == 'admin' ) ? 1 : 0;
				
				$this->DB->delete( 'core_sys_module', "sys_module_application='{$_app}' AND sys_module_key='{$_module}' AND sys_module_admin={$_is_admin}" );
			}
			
			$this->cache->rebuildCache( 'module_cache', 'global' );
			$this->cache->rebuildCache( 'app_menu_cache', 'global' );
		}
		
		//-----------------------------------------
		// Help files
		//-----------------------------------------
		
		if( count($extra_data['help']) )
		{
			$this->DB->delete( 'faq', "title IN('" . implode( "','", $extra_data['help'] ) . "')" );
		}
		
		//-----------------------------------------
		// Skin templates
		//-----------------------------------------
		
		if( count($extra_data['templates']) )
		{
			foreach( $extra_data['templates'] as $group => $bits )
			{
				$this->DB->delete( 'skin_templates', "template_group='{$group}' AND template_name IN('" . implode( "','", $bits ) . "')" );
			}

			require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
			require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
			$skinCaching = new skinCaching( $this->registry );
			$skinCaching->rebuildPHPTemplates( 0 );
		}
		
		//-----------------------------------------
		// CSS
		//-----------------------------------------
		
		if( count( $extra_data['css'] ) )
		{
			$this->DB->delete( 'skin_css', "css_group IN('" . implode( "','", $extra_data['css'] ) . "')" );
			
			if( ! $skinCaching )
			{
				require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
				require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
				$skinCaching = new skinCaching( $this->registry );
			}
			
			$skinCaching->rebuildCSSCache( 0 );
		}
		
		//-----------------------------------------
		// Tasks
		//-----------------------------------------
		
		if( count($extra_data['tasks']) )
		{
			$this->DB->delete( 'task_manager', "task_key IN('" . implode( "','", $extra_data['tasks'] ) . "')" );
		}
		
		//-----------------------------------------
		// Database changes
		//-----------------------------------------
		
		if( is_array($extra_data['database']['create']) AND count($extra_data['database']['create']) )
		{
			foreach( $extra_data['database']['create'] as $create_query )
			{
				$this->DB->return_die	= true;
				$this->DB->dropTable( $create_query['name'] );
				$this->DB->return_die	= false;
			}
		}
		
		if( is_array($extra_data['database']['alter']) AND count($extra_data['database']['alter']) )
		{
			foreach( $extra_data['database']['alter'] as $alter_query )
			{
				$this->DB->return_die	= true;
				
				if( $alter_query['altertype'] == 'add' )
				{
					if( $this->DB->checkForField( $alter_query['field'], $alter_query['table'] ) )
					{
						$this->DB->dropField( $alter_query['table'], $alter_query['field'] );
					}
				}
				else if( $alter_query['altertype'] == 'change' )
				{
					if( $this->DB->checkForField( $alter_query['newfield'], $alter_query['table'] ) )
					{
						$this->DB->changeField( $alter_query['table'] , $alter_query['newfield'], $alter_query['field'], $alter_query['fieldtype'], $alter_query['default'] );
					}
				}
				
				$this->DB->return_die	= false;
			}
		}
		
		if( is_array($extra_data['database']['update']) AND count($extra_data['database']['update']) )
		{
			foreach( $extra_data['database']['update'] as $update_query )
			{
				$this->DB->return_die	= true;
				$this->DB->update( $update_query['table'], array( $update_query['field'] => $update_query['oldvalue'] ), $update_query['where'] );
				$this->DB->return_die	= false;
			}
		}
		
		if( is_array($extra_data['database']['insert']) AND count($extra_data['database']['insert']) )
		{
			foreach( $extra_data['database']['insert'] as $insert_query )
			{
				if( $insert_query['fordelete'] )
				{
					$this->DB->return_die	= true;
					$this->DB->delete( $insert_query['table'], $insert_query['fordelete'] );
					$this->DB->return_die	= false;
				}
			}
		}
		
		//-----------------------------------------
		// Custom install/uninstall script?
		//-----------------------------------------
		
		if ( $_customFile )
		{
			if( is_object($customClass) && method_exists( $customClass, 'uninstall' ) )
			{
				$customClass->uninstall();
			}
			
			@unlink( $_customFile );
		}
		
		/* Delete main hook entry */
		$this->DB->delete( 'core_hooks', "hook_id={$hook_id}" );
		
		$this->rebuildHooksCache();
		
		/* Done */
		$this->registry->output->global_message = $this->lang->words['h_hasbeenun'];
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}	
	
	/**
	 * Hook add/edit form
	 * This dynamic form allows users to associate multiple files with a single hook.
	 *
	 * @param	string		[add|edit]
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _hookForm( $type='add' )
	{
		/* Init vars */
		$form			= array();
		$files			= array();
		$hookData		= array();
		$requirements	= array();
		$entryPoints	= array( 'foreach' => array( array( 'outer.pre', $this->lang->words['h_outerpre'] ),
													 array( 'inner.pre', $this->lang->words['h_innerpre'] ),
													 array( 'inner.post', $this->lang->words['h_innerpost'] ),
													 array( 'outer.post', $this->lang->words['h_outerpost'] )
													),
								 'if' => array( array( 'pre.startif', $this->lang->words['h_prestartif'] ),
												array( 'post.startif', $this->lang->words['h_poststartif'] ),
												array( 'pre.else', $this->lang->words['h_preelse']),
												array( 'post.else', $this->lang->words['h_postelse'] ),
												array( 'pre.endif', $this->lang->words['h_preendif'] ),
												array( 'post.endif', $this->lang->words['h_postendif'] )
												)
								);
		
		/* Edit time? */
		if( $type == 'edit' )
		{
			$id	= intval($this->request['id']);
			
			if( !$id )
			{
				$this->registry->output->showError( $this->lang->words['h_noedit'], 1116 );
			}
			
			$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
			
			if( !$hookData['hook_id'] )
			{
				$this->registry->output->showError( $this->lang->words['h_noedit'], 1117 );
			}
			
			/* Sort out extra stuff and requirements */
			$hookData['hook_extra_data']	= unserialize( $hookData['hook_extra_data'] );
			$hookData['hook_requirements']	= unserialize( $hookData['hook_requirements'] );
			
			/* Old data? - @todo: we should remove that around 3.3(4?) */
			if ( ! isset($hookData['hook_requirements']['required_applications']['core']) && isset($hookData['hook_requirements']['hook_ipb_version_min']) && ( $hookData['hook_requirements']['hook_ipb_version_min'] > 0 || $hookData['hook_requirements']['hook_ipb_version_max'] > 0 ) )
			{
				$requirements['core'] = array( 'min_version' => intval($hookData['hook_requirements']['hook_ipb_version_min']), 'max_version' => intval($hookData['hook_requirements']['hook_ipb_version_max']) );
			}
			
			if ( is_array($hookData['hook_requirements']['required_applications']) && count($hookData['hook_requirements']['required_applications']) )
			{
				/* Get the setup class */
				require_once( IPS_ROOT_PATH . 'setup/sources/base/setup.php' );/*noLibHook*/
				
				foreach( $hookData['hook_requirements']['required_applications'] as $appKey => $versionData )
				{
					/* Fetch and check versions */
					if ( !isset($this->cachedVersions[ $appKey ]) )
					{
						$this->cachedVersions[ $appKey ] = IPSSetUp::fetchXmlAppVersions( $appKey );
					}
					
					$_versions = $this->cachedVersions[ $appKey ];
					
					krsort($_versions);
					
					/* Setup our default 'no version' value */
					$versions = array( array( 0, $this->lang->words['h_any_version'] ) );
					
					foreach( $_versions as $long => $human )
					{
						if ( $long < 30000 && in_array( $appKey, array( 'core', 'forums', 'members' ) ) )
						{
							continue;
						}
						
						$versions[] = array( $long, $human );
					}
					
					$versionData['_versions'] = $versions;
					$requirements[ $appKey ]  = $versionData;
				}
			}
			
			/* Sort out hook files */
			$index		= 1;
			$skinGroups = $this->hooksFunctions->getSkinGroups();
			
			$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . $id ) );
			$outer = $this->DB->execute();
			
			/* Get them outside the while cycle to prevent warnings */
			$_dataHooks = IPSLib::getDataHookLocations();
			
			while( $r = $this->DB->fetch($outer) )
			{
				$r['hook_data']		= unserialize( $r['hook_data'] );

				if( $r['hook_type'] == 'templateHooks' )
				{
					$templates	= $this->hooksFunctions->getSkinMethods( $r['hook_data']['skinGroup'] );
					$hookIds	= $this->hooksFunctions->getHookIds( $r['hook_data']['skinFunction'], $r['hook_data']['type'] );
					
					$r['_skinDropdown']		= $this->registry->output->formDropdown( "skinGroup[{$index}]", $skinGroups, $r['hook_data']['skinGroup'], "skinGroup[{$index}]", "onchange='getTemplatesForAdd({$index});'" );
					$r['_templateDropdown']	= $this->registry->output->formDropdown( "skinFunction[{$index}]", $templates, $r['hook_data']['skinFunction'], "skinFunction[{$index}]", "onchange='getTypeOfHook({$index});'" );
					$r['_hookTypeDropdown']	= $this->registry->output->formDropdown( "type[{$index}]", array( array( 0, $this->lang->words['a_selectone'] ), array( 'foreach', $this->lang->words['hook_foreach_loop'] ), array( 'if', $this->lang->words['hook_if_statement'] ) ), $r['hook_data']['type'], "type[{$index}]", "onchange='getHookIds({$index});'" );
					$r['_hookIdsDropdown']	= $this->registry->output->formDropdown( "id[{$index}]", $hookIds, $r['hook_data']['id'], "id[{$index}]", "onchange='getHookEntryPoints({$index});'" );
					$r['_hookEPDropdown']	= $this->registry->output->formDropdown( "position[{$index}]", $r['hook_data']['type'] == 'foreach' ? $entryPoints['foreach'] : $entryPoints['if'], $r['hook_data']['position'] );
				}
				
				if( $r['hook_type'] == 'dataHooks' )
				{
					$r['_dataLocationDropdown']	= $this->registry->output->formDropdown( "dataLocation[{$index}]", $_dataHooks, $r['hook_data']['dataLocation'] );
				}
				
				$files[ $index ] = $r;
				$index++;
			}
			
			$action = 'do_edit_hook';
		}
		else
		{
			$action = 'do_create_hook';
		}
		
		/* Info */
		foreach( array( 'hook_name', 'hook_desc', 'hook_key', 'hook_version_human', 'hook_version_long', 'hook_author', 'hook_email', 'hook_website', 'hook_update_check' ) as $_hook_key )
		{
			$form[ $_hook_key ] = $this->registry->output->formSimpleInput( $_hook_key , isset($this->request[ $_hook_key ]) ? $this->request[ $_hook_key ] : $hookData[ $_hook_key ], 60 );
		}
		
		/* Requirements */
		foreach( array( 'hook_php_version_min', 'hook_php_version_max' ) as $_version_key )
		{
			$form[ $_version_key ] = $this->registry->output->formSimpleInput( $_version_key , isset($this->request[ $_version_key ]) ? $this->request[ $_version_key ] : $hookData['hook_requirements'][ $_version_key ], 20 );
		}
		
		/* Setup the global caches DD */
		$_caches = array();
		
		$this->DB->build( array( 'select' => 'cs_key',
								 'from'   => 'cache_store',
								 'order'  => 'cs_key ASC'
						 )		);
		$this->DB->execute();
		
		while( $gc = $this->DB->fetch() )
		{
			$_caches[] = array( $gc['cs_key'], $gc['cs_key'] );
		}
		
		sort( $_caches );
		
		$form['hook_global_caches'] = $this->registry->output->formMultiDropdown( 'hook_global_caches[]', $_caches, isset($_POST['hook_global_caches']) ? $_POST['hook_global_caches'] : explode(',',$hookData['hook_global_caches']), 15 );
		
		/* Get some data for the javascript */
		$form['jsDataTypes']  = IPSText::br2nl( $this->registry->output->formDropdown( "type[#{index}]", array( array( 0, $this->lang->words['a_selectone'] ), array( 'foreach', $this->lang->words['hook_foreach_loop'] ), array( 'if', $this->lang->words['hook_if_statement'] ) ), '', "type[#{index}]", "onchange='getHookIds(#{index});'" ) );
		$form['jsDataPoints'] = IPSText::br2nl( $this->registry->output->formDropdown( "position[#{index}]", $r['hook_data']['type'] == 'foreach' ? $entryPoints['foreach'] : $entryPoints['if'] ) );
		
		/* Output */
		$this->registry->output->extra_nav[] = array( $this->settings['base_url'] . $this->form_code . 'do=hooks_overview', $this->lang->words['menu__manage_hooks'] );
		$this->registry->output->html .= $this->html->hookForm( $form, $action, $hookData, $files, $requirements );
	}

	/**
	 * Hook add/edit save
	 * Save the new (or updated) hook record
	 *
	 * @param	string		[add|edit]
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _hookSave( $type='add' )
	{
		/* Init vars */
		$hookData   = array();
		$newFiles	= array();
		$requireApp	= array();
		$_hookFiles	= array();
		$extraKey   = '';
		
		/* Get data if we are editing */
		if( $type == 'edit' )
		{
			$id	= intval($this->request['hook_id']);
			
			if( !$id )
			{
				$this->registry->output->showError( $this->lang->words['h_noedit'], 1118 );
			}
			
			$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
			
			if( !$hookData['hook_id'] )
			{
				$this->registry->output->showError( $this->lang->words['h_noedit'], 1119 );
			}
			
			$extraKey = ' AND hook_id != ' . $hookData['hook_id'];
		}
		
		/* Error Checking */
		if( ! $this->request['hook_name'] )
		{
			$errors[] = $this->lang->words['hook_form_no_title'];
		}
		
		if( empty( $this->request['hook_key'] ) )
		{
			$errors[] = $this->lang->words['hook_form_no_key'];
		}
		else
		{
			$keyCheck = $this->DB->buildAndFetch( array( 'select' => 'count(*) as found', 'from' => 'core_hooks', 'where' => "hook_key='" . $this->DB->addSlashes( $this->request['hook_key'] ) . "'" . $extraKey ) );
			
			if ( $keyCheck['found'] > 0 )
			{
				$errors[] = $this->lang->words['hook_form_dupe_key'];
			}
		}
		
		/* Got any errors? */
		if ( is_array( $errors ) && count( $errors ) )
		{
			$this->registry->output->global_error = implode( '<br />', $errors );
			$this->_hookForm();
			return;
		}
		
		/* Not IN_DEV? */
		if ( ! IN_DEV )
		{
			$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . intval($this->request['hook_id']) ) );
			$this->DB->execute();
			
			while( $_r = $this->DB->fetch() )
			{
				$_hookFiles[ $_r['hook_classname'] ] = $_r['hook_file_stored'];
			}
		}
		
		/* Check for requirements */
		if ( is_array( $this->request['requireApp'] ) AND count( $this->request['requireApp'] ) )
		{
			foreach( $this->request['requireApp'] as $_index => $app_key )
			{
				if ( $app_key )
				{
					$minVersion = intval($this->request['minVersion'][ $_index ]);
					$maxVersion = intval($this->request['maxVersion'][ $_index ]);
					
					$requireApp[ $app_key ] = array( 'app_name'		=> ipsRegistry::$applications[ $app_key ]['app_title'],
													 'min_version'	=> $minVersion,
													 'max_version'	=> $maxVersion
													);
				}
			}
		}
		
		/* Check for files */
		if ( is_array( $this->request['file'] ) AND count( $this->request['file'] ) )
		{
			foreach( $this->request['file'] as $index => $file )
			{
				if ( $file )
				{
					$newFiles[ $index ]	= array('hook_file_real'	=> $file,
												'hook_type'			=> $this->request['hook_type'][ $index ],
												'hook_classname'	=> $this->request['hook_classname'][ $index ],
												'hook_data'			=> serialize( array('dataLocation'		=> trim( $this->request['dataLocation'][ $index ] ),
																						'libApplication'	=> trim( $this->request['libApplication'][ $index ] ),
																						'classToOverload'	=> trim($this->request['classToOverload'][ $index ]),
																						'skinGroup'			=> $this->request['skinGroup'][ $index ],
																						'skinFunction'		=> $this->request['skinFunction'][ $index ],
																						'type'				=> $this->request['type'][ $index ],
																						'id'				=> $this->request['id'][ $index ],
																						'position'			=> $this->request['position'][ $index ],
																						)
																				 )
												);

					/**
					 * @link	http://community.invisionpower.com/tracker/issue-22084-hook-edit-does-not-preserve-correct-name/
					 * We don't want to reset the stored name if you are not in developer mode
					 */
					if ( IN_DEV )
					{
						$newFiles[ $index ]['hook_file_stored']	= $file; // During import this is a random name, but for devs it's actual file
					}
					else
					{
						$newFiles[ $index ]['hook_file_stored']	= $_hookFiles[ $this->request['hook_classname'][ $index ] ] ? $_hookFiles[ $this->request['hook_classname'][ $index ] ] : $file;
					}
				}
			}
		}
		
		/* Get position */
		if ( $type == 'add' )
		{
			$position = $this->DB->buildAndFetch( array( 'select' => 'MAX(hook_position) as newPos', 'from' => 'core_hooks' ) );
			
			$position['newPos'] = intval($position['newPos']) + 1;
		}
		else
		{
			$position['newPos'] = $hookData['hook_position'];
		}

		$mainHookRecord	= array('hook_name'				=> trim($this->request['hook_name']),
								'hook_key'				=> substr( trim($this->request['hook_key']), 0, 32 ),
								'hook_global_caches'	=> ( is_array($this->request['hook_global_caches']) && count($this->request['hook_global_caches']) ) ? implode(',', $this->request['hook_global_caches']) : '',
								'hook_desc'				=> trim($this->request['hook_desc']),
								'hook_version_human'	=> trim($this->request['hook_version_human']),
								'hook_version_long'		=> trim($this->request['hook_version_long']),
								'hook_author'			=> trim($this->request['hook_author']),
								'hook_email'			=> trim($this->request['hook_email']),
								'hook_website'			=> trim($this->request['hook_website']),
								'hook_update_check'		=> trim($this->request['hook_update_check']),
								'hook_enabled'			=> $type == 'add' ? 1 : $hookData['hook_enabled'],
								'hook_installed'		=> $type == 'add' ? IPS_UNIX_TIME_NOW : $hookData['hook_installed'],
								'hook_updated'			=> $type == 'add' ? 0 : IPS_UNIX_TIME_NOW,
								'hook_position'			=> $position['newPos'],
								'hook_requirements'		=> serialize( array('required_applications'	=> $requireApp,
																			'hook_php_version_min'	=> trim($this->request['hook_php_version_min']),
																			'hook_php_version_max'	=> trim($this->request['hook_php_version_max'])
																			)
																	 )
								);
		
		if( $type == 'edit' )
		{
			$this->DB->update( 'core_hooks', $mainHookRecord, 'hook_id=' . $hookData['hook_id'] );
			
			$this->DB->delete( 'core_hooks_files', 'hook_hook_id=' . $hookData['hook_id'] );
		}
		else
		{
			$this->DB->insert( 'core_hooks', $mainHookRecord );
			
			$hookData['hook_id'] = $this->DB->getInsertId();
		}
		
		foreach( $newFiles as $index => $toInsert )
		{
			$toInsert['hook_hook_id'] = $hookData['hook_id'];
			
			$this->DB->insert( 'core_hooks_files', $toInsert );
		}
		
		/* Rebuild cache */
		$this->rebuildHooksCache();
		
		/* Rebuild global caches */
		try
		{
			IPSLib::cacheGlobalCaches();
		}
		catch( Exception $e ){}
		
		/* Flag skins for recache */
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinFunctions.php' );/*noLibHook*/
		require_once( IPS_ROOT_PATH . 'sources/classes/skins/skinCaching.php' );/*noLibHook*/
		$skinCaching = new skinCaching( $this->registry );
		$skinCaching->flagSetForRecache();
		
		/* Redirect */
		$this->registry->output->setMessage( $this->lang->words['h_saved'] );
		$this->registry->output->silentRedirectWithMessage( $this->settings['base_url'] . $this->form_code . '&amp;do=hooks_overview' );
	}
	
	/**
	 * View details about a hook
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _viewDetails()
	{
		$id	= intval($this->request['id']);
		
		if( !$id )
		{
			$this->registry->output->showError( $this->lang->words['h_nodetails'], 11110 );
		}
		
		$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_nodetails'], 11111 );
		}
		
		/* Extra data */
		$hookData['_updated']		 = $this->registry->getClass('class_localization')->formatTime( $hookData['hook_updated'] );
		$hookData['hook_extra_data'] = unserialize( $hookData['hook_extra_data'] );
		
		/* Meets requirements? */
		$hookData['_require_errors'] = $this->checkHookRequirements( $hookData );
		
		/* Got updates? */
		if ( $hookData['hook_update_check'] )
		{
			$hookData['hook_update_available']	= $this->_updateAvailable( $hookData['hook_update_check'], $hookData['hook_version_long'] );
		}
		else
		{
			$hookData['hook_update_available']	= array( 0 );
		}
		
		/* Get files */
		$files = array();
		$index = 1;
		
		$this->DB->build( array( 'select' => '*', 'from' => 'core_hooks_files', 'where' => 'hook_hook_id=' . $id ) );
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			$r['hook_data']  = unserialize( $r['hook_data'] );
			$files[ $index ] = $r;
			$index++;
		}

		/* Output */
		$this->registry->output->extra_nav[] = array( $this->settings['base_url'] . $this->form_code . 'do=hooks_overview', $this->lang->words['menu__manage_hooks'] );
		$this->registry->output->html .= $this->html->hookDetails( $hookData, $files );
	}
	
	/**
	 * Check if the hook meets all the requirements
	 * 
	 * @param	mixed	$hook	Can be an hook ID or an array of the hook data
	 * @return	@e array Errors found
	 */
	public function checkHookRequirements( $hook )
	{
		/* Init vars */
		$hookData = array();
		$reqsData = array();
		$errors   = array();
		
		/* We already have some data? */
		if ( is_array($hook) && count($hook) )
		{
			$hookData = $hook;
		}
		elseif ( is_int($hook) )
		{
			$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $hook ) );
		}
			
		/* Requirements are still serialized? */
		if ( IPSLib::isSerialized($hookData['hook_requirements']) )
		{
			$hookData['hook_requirements'] = unserialize($hookData['hook_requirements']);
		}
		
		/* Make our var shorter... */
		$reqsData = &$hookData['hook_requirements'];
		
		/* Old data? - @todo: remove this check around 3.3(4?) */
		if ( ! isset($reqsData['required_applications']['core']) && isset($reqsData['hook_ipb_version_min']) && ( $reqsData['hook_ipb_version_min'] > 0 || $reqsData['hook_ipb_version_max'] > 0 ) )
		{
			$reqsData['hook_ipb_version_min'] = ( $reqsData['hook_ipb_version_min'] < 30000 ) ? 30000 : $reqsData['hook_ipb_version_min'];
			
			$reqsData['required_applications']['core'] = array( 'min_version' => intval($reqsData['hook_ipb_version_min']), 'max_version' => intval($reqsData['hook_ipb_version_max']) );
		}
		
		//-----------------------------------------
		// Let's start checking requirements
		//-----------------------------------------
		
		/* PHP */
		if( $reqsData['hook_php_version_min'] OR $reqsData['hook_php_version_max'] )
		{
			if( $reqsData['hook_php_version_min'] AND version_compare( PHP_VERSION, $reqsData['hook_php_version_min'], '<' ) == true )
			{
				$errors['php_min'] = sprintf( $this->lang->words['h_phpold'],$reqsData['hook_php_version_min'] );
			}
			
			if( $reqsData['hook_php_version_max'] AND version_compare( PHP_VERSION, $reqsData['hook_php_version_max'], '>' ) == true )
			{
				$errors['php_max'] = sprintf( $this->lang->words['h_phpnew'], $reqsData['hook_php_version_max'] );
			}
		}
		
		/* Additional applications */
		if ( is_array($reqsData['required_applications']) && count($reqsData['required_applications']) )
		{
			/* Get the setup class */
			require_once( IPS_ROOT_PATH . 'setup/sources/base/setup.php' );/*noLibHook*/
			
			/* Loop through all apps */
			foreach( $reqsData['required_applications'] as $appKey => $appData )
			{
				/* Versions file doesn't exist? */
				if ( !is_file( IPSLib::getAppDir( $appKey ) . '/xml/versions.xml' ) )
				{
					$errors[ $appKey.'_app' ] = sprintf( $this->lang->words['hook_require_appnotfound'], $appData['app_name'] );
				}
				/* App not installed/enabled? */
				elseif ( !IPSLib::appIsInstalled( $appKey ) )
				{
					$errors[ $appKey.'_app' ] = sprintf( $this->lang->words['hook_require_appdisabled'], ipsRegistry::$applications[ $appKey ]['app_title'] );
				}
				elseif ( $appData['min_version'] OR $appData['max_version'] )
				{
					/* Fetch and check versions */
					if ( !isset($this->cachedVersions[ $appKey ]) )
					{
						$this->cachedVersions[ $appKey ] = IPSSetUp::fetchXmlAppVersions( $appKey );
					}
					
					$versions = $this->cachedVersions[ $appKey ];
					
					if ( is_array($versions) && count($versions) )
					{
						if ( !isset($this->cachedUpgradeInfo[ $appKey ]) )
						{
							$_key	= in_array( $appKey, array( 'forums', 'members' ) ) ? 'core' : $appKey;
							$this->cachedUpgradeInfo[ $_key ]	= $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'upgrade_history', 'where' => "upgrade_app='{$_key}'", 'order' => 'upgrade_version_id DESC', 'limit' => array( 1 ) ) );
							
							/* Extra caching for the three core apps */
							if( in_array( $appKey, array( 'core', 'forums', 'members' ) ) )
							{
								$this->cachedUpgradeInfo['core']	= $this->cachedUpgradeInfo[ $_key ];
								$this->cachedUpgradeInfo['forums']	= $this->cachedUpgradeInfo[ $_key ];
								$this->cachedUpgradeInfo['members']	= $this->cachedUpgradeInfo[ $_key ];
							}
						}
						
						/* Do we meet tha requirements? */
						if ( $appData['min_version'] AND $this->cachedUpgradeInfo[ $appKey ]['upgrade_version_id'] < $appData['min_version'] )
						{
							$errors[ $appKey.'_min' ] = sprintf( $this->lang->words['hook_require_tooold'], isset($versions[ $appData['min_version'] ]) ? $versions[ $appData['min_version'] ] : $appData['min_version'] );
						}
						
						if ( $appData['max_version'] AND $this->cachedUpgradeInfo[ $appKey ]['upgrade_version_id'] > $appData['max_version'] )
						{
							$errors[ $appKey.'_max' ] = sprintf( $this->lang->words['hook_require_toonew'], isset($versions[ $appData['max_version'] ]) ? $versions[ $appData['max_version'] ] : $appData['max_version'] );
						}
					}
				}
			}
		}
		
		return $errors;
	}
	
	/**
	 * Check if you meet hook requirements
	 *
	 * @return	@e void		[Outputs to screen]
	 */
	protected function _checkRequirements()
	{
		$id	= intval($this->request['id']);
		
		if( !$id )
		{
			$this->registry->output->showError( $this->lang->words['h_norequirements'], 11112 );
		}
		
		$hookData = $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'core_hooks', 'where' => 'hook_id=' . $id ) );
		
		if( !$hookData['hook_id'] )
		{
			$this->registry->output->showError( $this->lang->words['h_norequirements'], 11113 );
		}
		
		/* Get requirements */
		$hookData['hook_requirements'] = unserialize($hookData['hook_requirements']);
		
		/* Check requirements */
		$errors = $this->checkHookRequirements( $hookData );
		
		/* Output */
		$this->registry->output->extra_nav[] = array( $this->settings['base_url'] . $this->form_code . 'do=hooks_overview', $this->lang->words['menu__manage_hooks'] );
		$this->registry->output->html .= $this->html->hookRequirements( $hookData, $errors, $this->cachedVersions );
	}
	
	/**
	 * Rebuild hooks cache
	 *
	 * @return	@e void
	 */
	public function rebuildHooksCache()
	{
		/* INI */
		$cache = array( 'commandHooks' => array(), 'skinHooks' => array(), 'templateHooks' => array(), 'dataHooks' => array(), 'libraryHooks' => array() );

		/* First fix positions */
		$this->_fixPositions();

		/* Get current hooks */
		$this->DB->build( array( 'select'	=> 'f.*', 
								 'from'		=> array( 'core_hooks_files' => 'f' ), 
								 'where'	=> 'c.hook_enabled=1',
								 'order'	=> 'c.hook_position ASC',
								 'add_join'	=> array( array( 'select' => 'c.hook_id',
															 'from'   => array( 'core_hooks' => 'c' ),
															 'where'  => 'c.hook_id=f.hook_hook_id',
															 'type'   => 'left' ) )
							)		);
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			/* Got no main hook id? Weird but skip.. */
			if ( empty($r['hook_id']) )
			{
				continue;
			}
			
			/* INIT */
			$_cache	= array();
			$data	= unserialize( $r['hook_data'] );
			
			/* General Stuff */
			$_cache	= array( 'filename'  => $r['hook_file_stored'],
							 'className' => $r['hook_classname']
							);
			
			/* Hook type specific stuff */
			if( $r['hook_type'] == 'templateHooks' )
			{
				$_cache['type']				= $data['type'];
				$_cache['skinFunction']		= $data['skinFunction'];
				$_cache['id']				= $data['id'];
				$_cache['position']			= $data['position'];
			}
			else if( /*$r['hook_type'] == 'dataHooks' OR*/ $r['hook_type'] == 'libraryHooks' )
			{
				$_cache['classToOverload']	= $data['classToOverload'];
			}
			
			/* Add to array */
			if( $r['hook_type'] == 'templateHooks' )
			{
				$cache[ $r['hook_type'] ][ $data['skinGroup'] ][] = $_cache;
			}
			else if( $r['hook_type'] == 'dataHooks' )
			{
				$cache[ $r['hook_type'] ][ $data['dataLocation'] ][] = $_cache;
			}
			else if( $r['hook_type'] == 'libraryHooks' )
			{
				$cache[ $r['hook_type'] ][ $data['libApplication'] ][ $data['classToOverload'] ][] = $_cache;
			}
			else
			{
				$cache[ $r['hook_type'] ][ $data['classToOverload'] ][] = $_cache;
			}
		}
		
		/* Update the cache */
		$this->cache->setCache( 'hooks', $cache, array( 'array' => 1 ) );
	}
}