Файл: admin/sources/classes/skins/skinFunctions.php
Строк: 2154
<?php
/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.3.3
 * Skin Functions
 * Last Updated: $Date: 2012-05-21 09:09:36 -0400 (Mon, 21 May 2012) $
 * </pre>
 *
 * Owner: Matt
 * @author         $Author: ips_terabyte $
 * @copyright    (c) 2001 - 2009 Invision Power Services, Inc.
 * @license        http://www.invisionpower.com/company/standards.php#license
 * @package        IP.Board
 * @link        http://www.invisionpower.com
 * @since        9th March 2005 11:03
 * @version        $Revision: 10771 $
 *
 */
if ( ! defined( 'IN_IPB' ) )
{
    print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files.";
    exit();
}
class skinFunctions
{
    /**#@+
     * Registry objects
     *
     * @access    protected
     * @var        object
     */
    protected $registry;
    protected $DB;
    protected $settings;
    protected $request;
    protected $lang;
    protected $member;
    protected $memberData;
    protected $cache;
    protected $caches;
    /**#@-*/
    /**
     * Tier Class
     *
     * @access    public
     * @var        object
     */
    public $recursiveTiers;
    /**
     * Template count
     * Maintains an array: [skin_set_id][group_name][count]
     *
     * @access protected
     * @var    array
     */
    protected $_templateCount;
    /**
     * Error handle
     *
     * @access    protected
     * @var        array
     */
    protected $_errorMsgs = array();
    /**
     * Message handle
     *
     * @access    protected
     * @var        array
     */
    protected $_generalMsgs = array();
    
    /**
     * IN_DEV remap data
     *
     * @access    public
     * @var        array
     */
    public $remapData = array();
    
    /**
     * Set cache
     * @var protected
     */
    protected $_seenSet = array();
    
    /**
     * Constructor
     *
     * @access    public
     * @param    object        Registry object
     * @return    @e void
     */
    public function __construct( ipsRegistry $registry )
    {
        //-----------------------------------------
        // Make object
        //-----------------------------------------
        $this->registry   =  $registry;
        $this->DB          =  $this->registry->DB();
        $this->settings   =& $this->registry->fetchSettings();
        $this->request    =& $this->registry->fetchRequest();
        $this->member     =  $this->registry->member();
        $this->memberData =& $this->registry->member()->fetchMemberData();
        $this->cache      =  $this->registry->cache();
        $this->caches     =& $this->registry->cache()->fetchCaches();
        $this->lang       = $this->registry->class_localization;
        
        $this->registry->class_localization->loadLanguageFile( array( 'admin_templates' ), 'core' );
        
        //-----------------------------------------
        // IN_DEV on?
        //-----------------------------------------
        
        if ( $this->registry->isClassLoaded( 'output' ) )
        {
            $this->remapData = $this->registry->output->buildRemapData( TRUE );
        }
        
        //-----------------------------------------
        // Load tier class and init it
        //-----------------------------------------
        require_once( IPS_ROOT_PATH . 'sources/classes/tiers/recursiveTiers.php' );/*noLibHook*/
        $this->recursiveTiers = new recursiveTiers( $registry, array(   'sqlPrimaryID'    => 'set_id',
                                                                        'sqlParentID'    => 'set_parent_id',
                                                                        'sqlTitle'        => 'set_name',
                                                                        'sqlOrder'        => 'set_parent_id, set_id',
                                                                        'sqlTable'        => 'skin_collections',
                                                                        'customMethod'    => '_buildSkinTiers' ) );
    }
    /**
     * Return recursive tiers handle
     *
     * @access public
     * @return object
     */
    public function getTiersFunction()
    {
        return $this->recursiveTiers;
    }
    
    /**
     * Make this skin default
     * @param int $set_id
     */
    public function makeDefault( $set_id )
    {
        $setData = $this->fetchSkinData( $set_id, true );
        
        /* Make all others non default */
        $this->DB->update( 'skin_collections', array( 'set_is_default' => 0 ), 'set_id != ' . $set_id . ' AND set_output_format='' . $setData['set_output_format'] . ''' );
        
        /* Attempt to see if we have any other skin called 'default' */
        /* Not using this currently as it causes complications in other areas */
        /*
        $default = $this->DB->buildAndFetch( array( 'select' => 'set_id', 
                                                    'from'   => 'skin_collections',
                                                    'where'  => 'set_key='default' AND set_output_format='' . $setData['set_output_format'] . ''' ) );
        if ( $set_id != $default['set_id'] )
        {
            $this->DB->update( 'skin_collections', array( 'set_key' => IPS_UNIX_TIME_NOW ), 'set_id = ' . $default['set_id'] );
        }
        */
        /* Make this default */
        $this->DB->update( 'skin_collections', array( 'set_is_default' => 1/*, 'set_key' => 'default'*/ ), 'set_id = ' . $set_id . ' AND set_output_format='' . $setData['set_output_format'] . ''' );
    }
    
    /**
     * Fetch highest order
     *
     * @access    public
     * @return    int
     */
    public function fetchHighestSetPosition()
    {
        $bill = $this->DB->buildAndFetch( array( 'select' => 'MAX(set_order) as and_ted',
                                                 'from'   => 'skin_collections' ) );
                                                 
        
        /* Excellent! (guitar solo) */
        return intval( $bill['and_ted'] );
    }
    
    /**
     * Can we use the merge system?
     *
     * @access    public
     * @return    int
     */
    public function canUseMergeSystem()
    {
        $c    = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as ount',
                                                 'from'   => 'skin_templates_previous' ) );
                                                 
        return ( $c['ount'] ) ? true : false;
    }
    
    /**
     * Fetch master template keys
     *
     * @access    public
     * @return    arraya
     */
    public function fetchMasterKeys()
    {
        if ( is_array( $this->remapData ) AND is_array( $this->remapData['masterKeys'] ) )
        {
            return $this->remapData['masterKeys'];
        }
        else
        {
            $REMAP = array();
            require( DOC_IPS_ROOT_PATH . 'cache/skin_cache/masterMap.php' );/*noLibHook*/
            
            if ( is_array($REMAP) && count($REMAP) )
            {
                $this->remapData = array( 'masterKeys' => $REMAP['masterKeys'] );
            }
        }
        
        return $this->remapData['masterKeys'];
    }
    /**
     * Returns a skin set ID based on a key
     * @param string     $key
     * @param boolean    $ignoreMasterKeys
     */
    public function fetchSetIdByKey( $key, $ignoreMasterKeys=false )
    {
        $masterKeys = $this->fetchMasterKeys();
        
        if ( in_array( $key, $masterKeys ) && $ignoreMasterKeys === false )
        {
            /* Default master keys are always 0 */
            return 0;
        }
        
        if ( ! $this->_seenSet[ 'k-' . $key ] )
        {
            $this->_seenSet[ 'k-' . $key ] = $this->DB->buildAndFetch( array( 'select' => 'set_id',
                                                                                 'from'   => 'skin_collections',
                                                                                 'where'  => 'set_key='' . addslashes( $key ) . ''' ) );
        }
        
        return ( $this->_seenSet[ 'k-' . $key ]['set_id'] ) ? $this->_seenSet[ 'k-' . $key ]['set_id'] : 0;
    }
    
    /**
     * Returns a skin set key based on a id
     * @param string $key
     */
    public function fetchSetKeyById( $id )
    {
        $masterKeys = $this->fetchMasterKeys();
        
        if ( $id == 0 )
        {
            /* Default master keys are always 0 */
            return '';
        }
        
        if ( ! $this->_seenSet[ 'i-' . $id ] )
        {
            $this->_seenSet[ 'i-' . $id ] = $this->DB->buildAndFetch( array( 'select' => 'set_key',
                                                                                'from'   => 'skin_collections',
                                                                                'where'  => 'set_id=' . intval( $id ) ) );
        }
        
        return ( $this->_seenSet[ 'i-' . $id ]['set_key'] ) ? $this->_seenSet[ 'i-' . $id ]['set_key'] : 0;
    }
    
    /**
     * Determine if the skin set is a "master" set
     *
     * @param    mixed    $id        Skin set key or skin set id
     * @return    bool    True if this is a master set, otherwise false
     */
    public function isMasterSet( $id )
    {
        $key    = is_numeric($id) ? $this->fetchSetKeyById( $id ) : $id;
        
        return in_array( $key, $this->remapData['masterKeys'] );
    }
    
    /**
     * Search template bits
     *
     * @access        public
     * @param        int            Template set id to search
     * @param        string        String to search
     * @param        boolean        Is regex
     * @param        boolean        Search all parents including master template set
     * @return        array         array( 'searchCount' => int, 'matchCount' => int, 'matches' => array )
     * Exception Codes
     * REGEX_INCORRECT            Regex is not valid
     */
    public function searchTemplates( $setID, $searchString, $isRegex=FALSE, $searchParents=FALSE )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        
        $searchString = IPSText::stripslashes( $searchString );
        $regexString  = str_replace( '#', '#', str_replace( '"', '"', $searchString ) );
        $_templates   = array();
        $_matches     = array();
        $_matchCount  = 0;
        $return          = array( 'searchCount' => 0, 'matchCount' => 0, 'matches' => array() );
        
        /* Test Regex */
        if ( $isRegex )
        {
            ob_start();
            eval( "preg_match( "#{$regexString}#i", 'sometexthere' );");
            $return = ob_get_contents();
            ob_end_clean();
            
            if ( $return )
            {
                throw new Exception("REGEX_INCORRECT");
            }
        }
        
        /* Grab templates to search in */
        if ( $searchParents )
        {
            $_templates = $this->fetchTemplates( $setID );
        }
        else
        {
            
            $this->DB->build( array( 'select' => '*',
                                     'from'   => 'skin_templates',
                                     'where'  => 'template_set_id=' . $setID ) );
                                    
            $this->DB->execute();
            
            while( $row = $this->DB->fetch() )
            {
                $_templates[ $row['template_group'] ][ strtolower( $row['template_name'] ) ] = $row;
            }
        }
        
        /* Got anything? */
        if ( ! count( $_templates ) )
        {
            return $return;
        }
        
        $_templateCount = 0;
        
        /* You may continue... */
        foreach( $_templates as $_group => $_gdata )
        {
            foreach( $_gdata as $_name => $_data )
            {
                if ( $isRegex )
                {
                    if ( preg_match( "#{$regexString}#i", $_data['template_content'] ) )
                    {
                        $_matches[ $_group ][ $_name ] = $_data;
                        $_matchCount++;
                    }
                }
                else if ( stristr( $_data['template_content'], $searchString ) )
                {
                    $_matches[ $_group ][ $_name ] = $_data;
                    $_matchCount++;
                }
                
                $_templateCount++;
            }
        }
        
        /* Return to sender */
        return array( 'searchCount' => $_templateCount, 'matchCount' => $_matchCount, 'matches' => $_matches );
    }
    
    /**
     * Returns no. of modified template bits for
     * the skin (and or group)
     *
     * @access    public
     * @param    int        Template Set ID
     * @param    int        [Optional: Template Group]
     * @return    array     Array of data
     */
    public function fetchModifiedTemplateCount( $setID, $groupName='' )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $count = 0;
        //-----------------------------------------
        // Check...
        //-----------------------------------------
        if ( ! $setID )
        {
            return 0;
        }
        //-----------------------------------------
        // Get 'em
        //-----------------------------------------
        if ( $groupName )
        {
            if ( count( $this->_templateCount ) )
            {
                return intval( $this->_templateCount[ $setID ][ $groupName ]['count'] );
            }
            else
            {
                $count = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count',
                                                                 'from'   => 'skin_templates',
                                                                 'where'  => 'template_set_id=' . $setID . ' AND template_group=''. $groupName . ''' ) );
                return intval( $count['count'] );
            }
        }
        else
        {
            if ( count( $this->_templateCount ) )
            {
                foreach( $this->_templateCount[ $setID ] as $group => $data )
                {
                    $count += intval( $data['count'] );
                }
                return intval( $count );
            }
            else
            {
                $count = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count',
                                                                 'from'   => 'skin_templates',
                                                                 'where'  => 'template_set_id=' . $setID ) );
                return intval( $count['count'] );
            }
        }
    }
    /**
     * Reset error handle
     *
     * @access    protected
     * @return    @e void
     */
    final protected function _resetErrorHandle()
    {
        $this->_errorMsgs = array();
    }
    /**
     * Add an error message
     *
     * @access    protected
     * @param    string        Error message to add
     * @return    @e void
     */
    final protected function _addErrorMessage( $error )
    {
        $this->_errorMsgs[] = $error;
    }
    /**
     * Fetch error messages
     *
     * @access     public
     * @param    boolean        Always return an array
     * @return     mixed         Array of messages or (FALSE/empty array)
     */
    public function fetchErrorMessages( $alwaysAnArray=FALSE )
    {
        return ( count( $this->_errorMsgs ) ) ? $this->_errorMsgs : ( ($alwaysAnArray === TRUE) ? array() :  FALSE );
    }
    /**
     * Reset error handle
     *
     * @access    protected
     * @return    @e void
     */
    final protected function _resetMessageHandle()
    {
        $this->_generalMsgs = array();
    }
    /**
     * Add an error message
     *
     * @access    protected
     * @param    string        Error message to add
     * @return    @e void
     */
    final protected function _addMessage( $error )
    {
        $this->_generalMsgs[] = $error;
    }
    /**
     * Fetch error messages
     *
     * @access     public
     * @param    boolean        Always return an array
     * @return     mixed         Array of messages or (FALSE/empty array)
     */
    public function fetchMessages( $alwaysAnArray=FALSE )
    {
        return ( count( $this->_generalMsgs ) ) ? $this->_generalMsgs : ( ($alwaysAnArray === TRUE) ? array() :  FALSE );
    }
    /**
     * Load a 'master_skin' template file. Used when developing
     *
     * @access    public
     * @param    string        Name of skin file
     * @return    @e void        Evals the skin to add the class in memory
     */
    public function loadMasterSkinTemplate( $name, $id )
    {
        if ( ! count( $this->remapData ) )
        {
            $this->remapData = $this->registry->output->buildRemapData();
        }
        
        $_id = ( $id == 0 ) ? $this->remapData['inDevDefault'] : $id;
        
        $_dir = $this->remapData['templates'][ $_id ];
    
        if( ! is_file(IPS_CACHE_PATH."cache/skin_cache/" . $_dir . "/".$name.".php") )
        {
            return;
        }
        
        //-----------------------------------------
        // Get data...
        //-----------------------------------------
        $data = implode( '', file( IPS_CACHE_PATH."cache/skin_cache/" . $_dir . "/".$name.".php" ) );
        //-----------------------------------------
        // Get template class
        //-----------------------------------------
        $toeval = $this->registry->templateEngine->convertCacheToEval( $data, $name . '_' . $id );
        $showme = ( strstr( $data, '{{{SHOWME}}}' ) ) ? $name : '';
        if ( ! class_exists( $name . '_' . $id ) )
        {
            ob_start();
            eval($toeval);
            $result = ob_get_contents();
             ob_end_clean();
            if ( strstr( $result, "Parse error" ) OR strstr( $result, 'Catchable fatal error:' ) OR $showme == $name )
            {    
                IPSDebug::showTemplateError( $result, $toeval );
            }
        }
    }
    /**
     * Returns whether or not a skin set is suitable for removal
     *
     * @access    public
     * @param    int        Skin set ID
     * @return    boolean    (True OK to remove, false NOT OK to remove)
     */
    public function canRemoveSet( $setID )
    {
        $skinSetData  = $this->fetchSkinData( $setID );
        if ( $skinSetData['set_is_default'] )
        {
            return FALSE;
        }
        $allSkinCount = $this->DB->buildAndFetch( array( 'select' => 'count(*) as count',
                                                         'from'   => 'skin_collections',
                                                         'where'  => 'set_parent_id=' . $setID ) );
        if ( $allSkinCount['count'] >= 1 )
        {
            return FALSE;
        }
        
        /* Root skin? */
        if ( in_array( $setID, $this->remapData['masterKeys'] ) )
        {
            return FALSE;
        }
        return TRUE;
    }
    /**
     * Removes a skin set
     *
     * @access    public
     * @param    int        Skin set ID
     * @param    bool    Force, even if it is not removable
     * @return    boolean    (True OK to remove, false NOT OK to remove)
     */
    public function removeSet( $setID, $force=false )
    {
        $skinSetData  = $this->fetchSkinData( $setID );
        if ( $force !== TRUE AND $this->canRemoveSet( $setID ) !== TRUE )
        {
            return FALSE;
        }
        /* Update any children */
        $this->DB->update( 'skin_collections', array( 'set_parent_id' => 0 ), 'set_parent_id=' . $setID );
        /* Update any members */
        $this->DB->update( 'members', array( 'skin' => 0 ), 'skin=' . $setID );
        /* Update any forums */
        $this->DB->update( 'forums', array( 'skin_id' => 0 ), 'skin_id=' . $setID );
        /* Delete skin set */
        $this->DB->delete( 'skin_collections', 'set_id=' . $setID );
        /* Delete Templates */
        $this->DB->delete( 'skin_templates', 'template_set_id=' . $setID );
        /* Delete Replacements */
        $this->DB->delete( 'skin_replacements', 'replacement_set_id=' . $setID );
        /* Delete CSS */
        $this->DB->delete( 'skin_css', 'css_set_id=' . $setID );
        /* Delete Caches */
        $this->DB->delete( 'skin_templates_cache', 'template_set_id=' . $setID );
        /* Delete URL Mapping */
        $this->DB->delete( 'skin_url_mapping', 'map_skin_set_id=' . $setID );
        /* Delete CSS Files */
        $classToLoad    = IPSLib::loadLibrary( IPS_KERNEL_PATH . '/classFileManagement.php', 'classFileManagement' );
        $fileManagement    = new $classToLoad();
        $fileManagement->removeDirectory( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_css/css_' . $setID );
        /* Delete PHP Cache Files */
        $fileManagement->removeDirectory( IPS_CACHE_PATH . 'cache/skin_cache/cacheid_' . $setID );
        
        /* Delete any diff/merge sessions */
        $this->DB->build( array( 'select' => '*',
                                 'from'   => 'skin_merge_session',
                                 'where'  => 'merge_set_id=' . $setID ) );
                                 
        $o = $this->DB->execute();
        
        while( $row = $this->DB->fetch( $o ) )
        {
            /* Delete 'em */
            $this->DB->delete('skin_merge_session', 'merge_id=' . $row['merge_id'] );
            $this->DB->delete('skin_merge_changes', 'change_session_id=' . $row['merge_id'] );
        }
        /* Generator stuff */
        $this->DB->build( array( 'select' => '*',
                                 'from'   => 'skin_generator_sessions',
                                 'where'  => 'sg_skin_set_id=' . $setID ) );
                                 
        $o = $this->DB->execute();
        
        while( $row = $this->DB->fetch( $o ) )
        {
            /* Delete 'em */
            $this->DB->delete('skin_generator_sessions', "sg_session_id='" . $row['sg_session_id'] . "'" );
            
            IPSMember::save( $row['sg_member_id'], array( 'core' => array( 'bw_using_skin_gen' => 0 ) ) );
        }
        
        
        /* Rebuild Tree Information */
        $this->rebuildTreeInformation();
        /* Rebuild Skin Set Caches */
        if ( class_exists( 'skinCaching' ) )
        {
            $this->rebuildSkinSetsCache();
            $this->rebuildURLMapCache();
            $this->cache->putWithCacheLib( 'Skin_Store_' . $setID, array(), 1 );
        }    
        
        return TRUE;
    }
    /**
     * Rebuild parent and child tree information
     *
     * @access    public
     * @param    int        [Optional Skin set id to rebuild. If no value given, all skin sets are rebuilt]
     * @return    boolean
     */
    public function rebuildTreeInformation( $setID=NULL )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $_where = ( $setID !== NULL ) ? 'set_id=' . $setID : '';
        
        /* Rebuild tiers to ensure cache isn't stale after a new set has been added */
        $this->caches['skinsets'] = array();
        $this->recursiveTiers->rebuildTiers();
        
        //-----------------------------------------
        // Grab skins and loop through 'em
        //-----------------------------------------
        $this->DB->build( array( 'select' => '*',
                                 'from'   => 'skin_collections',
                                 'where'  => $_where,
                                 'order'  => 'set_id ASC' ) );
        $outer = $this->DB->execute();
        while( $row = $this->DB->fetch( $outer ) )
        {
            $_parents  = $this->recursiveTiers->fetchItemParents( $row['set_id'] );
            $_children = $this->recursiveTiers->fetchItemChildren( $row['set_id'] );
            
            $this->DB->update( 'skin_collections', array( 'set_parent_array' => serialize( ( is_array( $_parents ) ) ? $_parents : array() ),
                                                          'set_child_array'  => serialize( ( is_array( $_children ) ) ? $_children : array() ) ), 'set_id=' . $row['set_id'] );
            
            /* Reset master key - parents */
            if( count($_parents) AND $row['set_id'] )
            {
                $_p            = $_parents;
                $_id         = array_pop( $_p );
                $rootParent = $this->fetchSkinData( $_id, true );
                
                /* If no parent, we must be a parent, then */
                if ( ! $rootParent['set_id'] )
                {
                    $rootParent = $this->fetchSkinData( $setID, true );
                }
                
                $this->DB->update( 'skin_collections', array( 'set_master_key' => ( $rootParent['set_master_key'] ) ? $rootParent['set_master_key'] : 'root' ), 'set_id IN (' . implode( ',', $_parents ) . ')' );
            }
            
            /* Reset master key - children */
            if( count($_children) AND $row['set_id'] )
            {
                if ( ! is_array( $rootParent ) )
                {
                    $_p            = $_parents;
                    $_id         = array_pop( $_p );
                    $rootParent = $this->fetchSkinData( $_id, true );
                    
                    /* If no parent, we must be a parent, then */
                    if ( ! $rootParent['set_id'] )
                    {
                        $rootParent = $this->fetchSkinData( $setID, true );
                    }
                }
                
                $this->DB->update( 'skin_collections', array( 'set_master_key' => ( $rootParent['set_master_key'] ) ? $rootParent['set_master_key'] : 'root' ), 'set_id IN (' . implode( ',', $_children ) . ')' );
            }
            
            if( count($_parents) AND $setID )
            {
                foreach( $_parents as $parent )
                {
                    $this->rebuildTreeInformation( $parent );
                }
            }
        }
        
        return TRUE;
    }
    /**
     * Fetch replacements from the tree
     * Very simple little function to return the replacements for a particular skin set
     * Figures out all the inheritence and stuff for you. It's good like that
     *
     * @access    public
     * @param    int            Skin set ID
     * @return    array         Array of css data: array[ 'replacement_key' => [ 'replacement_id' => ..., 'replacement_key' => ... , 'replacement_content' => ... 'replacement_set_id' => ..., 'replacement_added_to' => .... ]
     * <code>
     * Usage:
     * $replacementData = $skinFunctions->fetchReplacements( 1 );
     *
     * foreach( $replacementData as $key => $data )
     * {
     *    print $key . ' contains: ' . $data['replacement_content'];
     * }
     * </code>
     */
    public function fetchReplacements( $setID )
    {
        $replacements = array();
        
        //-----------------------------------------
        // Try and get the skin from the cache
        //-----------------------------------------
        
        /* Did we pass a master key? - all root skins are 0  */
        if ( ! is_numeric( $setID ) )
        {
            $skinSetData = array( 'set_id'          => 0,
                                  'set_master_key'  => $setID,
                                  'set_key'            => $setID,
                                  '_isMaster'        => 1,
                                  '_parentTree'     => array(),
                                  '_childTree'      => array(),
                                  '_userAgents'     => array(),
                                  '_cssGroupsArray' => array() );
        }
        else
        {        
            $skinSetData = $this->fetchSkinData( $setID );
        }
        
        /* Did we get a skin set? */
        if ( ! isset( $skinSetData['_parentTree'] ) OR ! is_array( $skinSetData['_parentTree'] ) )
        {
            return array();
        }
        
        //-----------------------------------------
        // Push root ID onto the END of the parent array
        //-----------------------------------------
        array_push( $skinSetData['_parentTree'], 0 );
        //-----------------------------------------
        // Push the current skin set ID onto the beginnging
        //-----------------------------------------
        if ( is_numeric( $setID ) )
        {
            array_unshift( $skinSetData['_parentTree'], $setID );
        }
    
        /* We want to capture only this set's master bits */
        $where = ' AND ( ( replacement_set_id > 0 AND ( replacement_master_key='' OR replacement_master_key IS NULL ) ) OR ( replacement_set_id=0 AND replacement_master_key='' . $skinSetData['set_master_key'] . '' ) )';
        
        //-----------------------------------------
        // Push root ID onto the END of the parent array
        //-----------------------------------------
        array_push( $skinSetData['_parentTree'], 0 );
        //-----------------------------------------
        // Push the current skin set ID onto the beginnging
        //-----------------------------------------
        if ( is_numeric( $setID ) )
        {
            array_unshift( $skinSetData['_parentTree'], $setID );
        }
        
        //-----------------------------------------
        // Geddit
        //-----------------------------------------
        $this->DB->build( array( 'select'    => '*,' .
                                                   $this->DB->buildInstring( "," . implode( ",", $skinSetData['_parentTree'] ) . ",",
                                                   $this->DB->buildConcat( array( array( ',', 'string' ), array( 'replacement_set_id' ), array( ',', 'string' ) ) ) ) . ' as theorder',
                                        'from'    => 'skin_replacements',
                                        'where'    => "replacement_set_id IN (" . implode( ",", $skinSetData['_parentTree'] ) .")" . $where,
                                        'order'    => 'replacement_key ASC, theorder DESC' ) );
        $i = $this->DB->execute();
        while( $row = $this->DB->fetch( $i ) )
        {
            $row['SAFE_replacement_content'] = IPSText::textToForm( $row['replacement_content'] );
            $replacements[ $row['replacement_key'] ] = $row;
            // Matt, the above is not consistent with the .inc file, so I'm changing it for now'
            // RIkki: There is loads more data needed, like set_id, original set ID, etc, so changing it back :p
            //$replacements[ $row['replacement_key'] ] = $row['replacement_content'];
        }
        return $replacements;
    }
    /**
     * Save replacement after edit
     *
     * @access    public
     * @param    int            Replacement ID
     * @param    int            Replacement Set ID
     * @param    string        Replacement content
     * @param    string        Replacement key
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * NO_SUCH_REPLACEMENT:        Could not locate replacement
     * REPLACEMENT_EXISTS:         Could not rename replacement
     * </code>
     */
    public function saveReplacementFromEdit( $replacementID, $setID, $replacement_content, $replacement_key )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $replacementID       = intval( $replacementID );
        $replacement_key     = IPSText::alphanumericalClean( $replacement_key );
        $replacement_content = IPSText::formToText( $replacement_content );
        //-----------------------------------------
        // Fetch replacement data
        //-----------------------------------------
        $replacement = $this->DB->buildAndFetch( array( 'select' => '*',
                                                        'from'   => 'skin_replacements',
                                                           'where'  => 'replacement_id=' . $replacementID ) );
        if ( ! $replacement['replacement_id'] )
        {
            throw new Exception( 'NO_SUCH_REPLACEMENT' );
        }
        //-----------------------------------------
        // Group?
        //-----------------------------------------
        if ( $replacement['replacement_added_to'] == $setID )
        {
            if ( $replacement['replacement_key'] != $replacement_key )
            {
                $replacementTest = $this->DB->buildAndFetch( array( 'select' => '*',
                                                                       'from'   => 'skin_replacements',
                                                                    'where'  => 'replacement_set_id=' . $setID . ' AND replacement_key='' . $replacement_key . ''' ) );
                if ( $replacementTest['replacement_id'] )
                {
                    throw new Exception( "REPLACEMENT_EXISTS" );
                }
            }
        }
        else
        {
            $replacement_group = $replacement['replacement_group'];
        }
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Same set?
        //-----------------------------------------
        if ( $setID == $replacement['replacement_set_id'] )
        {
            $this->DB->update( 'skin_replacements', array( 'replacement_content'  => $replacement_content,
                                                               'replacement_key'      => $replacement_key,
                                                             ), 'replacement_id=' . $replacementID );
            $replacement_id_new = $replacementID;
        }
        else
        {
            $this->DB->insert( 'skin_replacements', array( 'replacement_set_id'   => $setID,
                                                               'replacement_content'  => $replacement_content,
                                                               'replacement_key'      => $replacement_key,
                                                               'replacement_added_to' => $replacement['replacement_added_to'] ) );
            $replacement_id_new = $this->DB->getInsertId();
        }
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildReplacementsCache( $setID );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $replacement_id_new;
    }
    /**
     * Save replacement after add
     *
     * @access    public
     * @param    int            Replacement Set ID
     * @param    string        Replacement content
     * @param    string        Replacement key
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * REPLACEMENT_EXISTS:         Could not rename replacement
     * </code>
     */
    public function saveReplacementFromAdd( $setID, $replacement_content, $replacement_key )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $replacementID       = intval( $replacementID );
        $replacement_key     = IPSText::alphanumericalClean( $replacement_key );
        $replacement_content = IPSText::formToText( $replacement_content );
        //-----------------------------------------
        // Goddit?
        //-----------------------------------------
        $replacementTest = $this->DB->buildAndFetch( array( 'select' => '*',
                                                                      'from'   => 'skin_replacements',
                                                                   'where'  => 'replacement_set_id=' . $setID . ' AND replacement_key='' . $replacement_key . ''' ) );
        if ( $replacementTest['replacement_id'] )
        {
            throw new Exception( "REPLACEMENT_EXISTS" );
        }
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Save set?
        //-----------------------------------------
        $this->DB->insert( 'skin_replacements', array( 'replacement_set_id'   => $setID,
                                                           'replacement_content'  => $replacement_content,
                                                           'replacement_key'      => $replacement_key,
                                                           'replacement_added_to' => $setID ) );
        $replacement_id_new = $this->DB->getInsertId();
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildReplacementsCache( $setID );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $replacement_id_new;
    }
    /**
     * Reverts / Removes Replacement
     *
     * @access    public
     * @param    int            Replacement ID
     * @param    int            Template Set ID
     * @return    array         All replacements for this skin set
     * <code>
     * Exception Codes:
     * NO_SUCH_REPLACEMENT:            Could not locate replacement id#
     * CANNOT_REMOVE:                 Cannot revert / remove the css
     * </code>
     */
    public function revertReplacement( $replacementID, $setID )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $replacementID = intval( $replacementID );
        
        //-----------------------------------------
        // Fetch replacement data
        //-----------------------------------------
        $replacement = $this->DB->buildAndFetch( array( 'select' => '*',
                                                        'from'   => 'skin_replacements',
                                                           'where'  => 'replacement_id=' . $replacementID ) );
        if ( ! $replacement['replacement_id'] )
        {
            throw new Exception( 'NO_SUCH_REPLACEMENT' );
        }
        //-----------------------------------------
        // Is this a master skin bit?
        //-----------------------------------------
        if ( $replacement['replacement_set_id'] == 0 AND $replacement['replacement_added_to'] == 0 )
        {
            throw new Exception( "CANNOT_REMOVE");
        }
        //-----------------------------------------
        // Remove it...
        //-----------------------------------------
        if ( $replacement['replacement_added_to'] == $setID  )
        {
            # remove it from ALL template sets
            $this->DB->delete( 'skin_replacements', "replacement_key='" . $replacement['replacement_key'] . "'" );
        }
        else
        {
            $this->DB->delete( 'skin_replacements', 'replacement_id=' . $replacementID );
        }
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildReplacementsCache( $setID );
        //-----------------------------------------
        // Grab the next template bit inline...
        //-----------------------------------------
        $replacements = $this->fetchReplacements( $setID );
        //-----------------------------------------
        // Reeee-turn
        //-----------------------------------------
        return $replacements;
    }
    /**
     * Fetch CSS from the tree
     * Very simple little function to return the CSS for a particular skin set
     * Figures out all the inheritence and stuff for you. It's good like that
     *
     * @access    public
     * @param    int            Skin set ID
     * @param    boolean     Parse data (TRUE is default). Adds elements into the array, such as '_cssSize', etc
     * @param    boolean        Skip any 'removed' CSS files.
     * @return    array         Array of css data: array[ 'css_group' => [ 'css_id' => ..., 'css_group' => ... , 'css_content' => ... 'css_position' => ..., 'css_set_id' => .... ]
     * <code>
     * Usage:
     * $cssData = $skinFunctions->fetchCSS( 1 );
     *
     * foreach( $cssData as $group_name => $data )
     * {
     *    print $data['css_group'] . ' contains: ' . $data['css_content'];
     * }
     * </code>
     */
    public function fetchCSS( $setID, $parse=TRUE, $skipRemoved=TRUE )
    {
        $css                        = array();
        $skip                        = array();
        $where                        = '';
        
        /* Did we pass a master key? - all root skins are 0  */
        if ( ! is_numeric( $setID ) )
        {
            $skinSetData = array( 'set_id'          => 0,
                                  'set_master_key'  => $setID,
                                  'set_key'            => $setID,
                                  '_isMaster'        => 1,
                                  '_parentTree'     => array(),
                                  '_childTree'      => array(),
                                  '_userAgents'     => array(),
                                  '_cssGroupsArray' => array() );
        }
        else
        {        
            $skinSetData                = $this->fetchSkinData( $setID );
            $skinSetData['_isMaster']    = $skinSetData['_isMaster'] ? $skinSetData['_isMaster'] : 0;
        }
        
        /* Did we get a skin set? */
        if ( ! isset( $skinSetData['_parentTree'] ) OR ! is_array( $skinSetData['_parentTree'] ) )
        {
            return array();
        }
        
        //-----------------------------------------
        // Push root ID onto the END of the parent array
        //-----------------------------------------
        array_push( $skinSetData['_parentTree'], 0 );
        //-----------------------------------------
        // Push the current skin set ID onto the beginnging
        //-----------------------------------------
        if ( is_numeric( $setID ) )
        {
            array_unshift( $skinSetData['_parentTree'], $setID );
        }
    
        /* We want to capture only this set's master bits */
        $where = ' AND ( ( css_set_id > 0 AND ( css_master_key='' OR css_master_key IS NULL ) ) OR ( css_set_id=0 AND css_master_key='' . $skinSetData['set_master_key'] . '' ) )';
        //-----------------------------------------
        // Geddit
        //-----------------------------------------
        $this->DB->build( array( 'select'    => '*,' .
                                                   $this->DB->buildInstring( "," . implode( ",", $skinSetData['_parentTree'] ) . ",",
                                                   $this->DB->buildConcat( array( array( ',', 'string' ), array( 'css_set_id' ), array( ',', 'string' ) ) ) ) . ' as theorder',
                                 'from'        => 'skin_css',
                                 'where'    => "css_set_id IN (" . implode( ",", $skinSetData['_parentTree'] ) .")" . $where,
                                  'order'    => 'css_group, theorder DESC' ) );
        $i = $this->DB->execute();
        while( $row = $this->DB->fetch( $i ) )
        {
            /* skipping this group? */
            if ( in_array( $row['css_group'], $skip ) )
            {
                continue;
            }
            
            if ( $parse === TRUE )
            {
                $row['_cssSize'] = IPSLib::sizeFormat( IPSLib::strlenToBytes( strlen( $row['css_content'] ) ) );
            }
            
            /* CSS has been removed up the tree? */
            if ( $row['css_removed'] AND $skipRemoved === TRUE )
            {
                unset( $css[ $row['css_group'] ] );
                $skip[] = $row['css_group'];
                continue;
            }
            
            $css[ $row['css_group'] ] = $row;
        }
        
        return $css;
    }
    /**
     * Fetch CSS for editing
     *
     * @access    public
     * @param    int            CSS ID
     * @param    int            Template Set ID
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * NO_SUCH_CSS:        Could not locate CSS id#
     * </code>
     */
    public function fetchCSSForEdit( $cssID, $setID )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $cssID = intval( $cssID );
        //-----------------------------------------
        // Fetch template data
        //-----------------------------------------
        
        $css = $this->DB->buildAndFetch( array( 'select' => '*',
                                                'from'   => 'skin_css',
                                                'where'  => 'css_id=' . $cssID ) );
        if ( ! $css['css_id'] )
        {
            throw new Exception( 'NO_SUCH_CSS' );
        }
        
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Update data
        //-----------------------------------------
        $css['_actualSetID']      = $css['css_set_id'];
        $css['_belongsSetID']     = $setID;
        $css['_isMaster']          = ( $css['css_master_key'] ) ? 1 : 0;
        //$css['_css_content']      = $css['css_content'];
        //-----------------------------------------
        // Update content for editing
        //-----------------------------------------
        
        /* Commented out for bug #18432 */
//        $css['css_content'] = IPSText::textToForm( $css['css_content'] );
        //-----------------------------------------
        // Return it
        //-----------------------------------------
        return $css;
    }
    /**
     * Save template bit after edit
     *
     * @access    public
     * @param    int            CSS ID
     * @param    int            CSS content
     * @param    string        CSS group
     * @param    string        CSS position
     * @param    string        CSS attributes (media="Screen")
     * @param    string        APP key to tie CSS to
     * @param    int            If APP key, hide when APP key doesn't match viewing app
     * @param    string        Modules to tie CSS to
     * @return    int            New CSS id
     * <code>
     * Exception Codes:
     * CSS_EXISTS:        CSS by that name already exists
     * NO_SUCH_SET:        Set ID doesn't match anything in the DB
     * </code>
     */
    public function saveCSSFromAdd( $setID, $css_content, $css_group, $css_position=0, $css_attributes='', $css_app='', $css_app_hide=0, $css_modules='' )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $setID        = intval( $setID );
        $css_group    = str_replace( '.css', '', IPSText::alphanumericalClean( $css_group ) );
        $css_position = intval( $css_position );
        
        /* Make sure skingen is always on top */
        if ( $css_group != 'ipb_skingen' )
        {
            $css_position = ( $css_position >= 999 ) ? 998 : $css_position;
        }
        
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Got it?
        //-----------------------------------------
        if ( ! $skinSetData['set_id'] )
        {
            throw new Exception( 'NO_SUCH_SET' );
        }
        //-----------------------------------------
        // Make sure we don't have one already
        //-----------------------------------------
        $css = $this->DB->buildAndFetch( array( 'select' => '*',
                                                'from'   => 'skin_css',
                                                'where'  => 'css_set_id=' . $setID . ' AND css_group='' . $css_group . ''' ) );
        if ( $css['css_id'] )
        {
            throw new Exception( 'CSS_EXISTS' );
        }
        //-----------------------------------------
        // Fix up content
        //-----------------------------------------
        $css_content = IPSText::formToText( $css_content );
        //-----------------------------------------
        // Save CSS?
        //-----------------------------------------
        $this->DB->insert( 'skin_css', array(    'css_set_id'     => $setID,
                                                 'css_content'    => $css_content,
                                                 'css_group'      => $css_group,
                                                 'css_position'   => $css_position,
                                                 'css_attributes' => $css_attributes,
                                                 'css_app'          => $css_app,
                                                 'css_app_hide'      => $css_app_hide,
                                                 'css_modules'      => str_replace( ' ', '', $css_modules ),
                                                 'css_added_to'   => $setID,
                                                 'css_updated'    => time() ) );
        $css_id_new = $this->DB->getInsertId();
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildCSS( $setID );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $css_id_new;
    }
    /**
     * Save template bit after edit
     *
     * @access    public
     * @param    int            CSS ID
     * @param    int            CSS Set ID
     * @param    string        CSS content
     * @param    string        CSS name (css_group)
     * @param    int            CSS position
     * @param    string        CSS attributes (media="Screen")
     * @param    string        APP key to tie CSS to
     * @param    int            If APP key, hide when APP key doesn't match viewing app
     * @param    string        Modules to tie CSS to
     * @return    int         New CSS id
     * <code>
     * Exception Codes:
     * NO_SUCH_CSS:        Could not locate CSS
     * CSS_EXISTS:         Could not rename CSS file
     * </code>
     */
    public function saveCSSFromEdit( $cssID, $setID, $css_content, $css_group, $css_position=0, $css_attributes='', $css_app='', $css_app_hide=0, $css_modules='' )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $cssID        = intval( $cssID );
        $css_group    = str_replace( '.css', '', IPSText::alphanumericalClean( $css_group ) );
        $css_position = intval( $css_position );
        $_goClean     = 0;
        
        /* Make sure skingen is always on top */
        if ( $css_group != 'ipb_skingen' )
        {
            $css_position = ( $css_position >= 999 ) ? 998 : $css_position;
        }
        
        //-----------------------------------------
        // Fetch template data
        //-----------------------------------------
        $css = $this->DB->buildAndFetch( array( 'select' => '*',
                                                'from'   => 'skin_css',
                                                'where'  => 'css_id=' . $cssID ) );
        if ( ! $css['css_id'] )
        {
            throw new Exception( 'NO_SUCH_CSS' );
        }
        //-----------------------------------------
        // Group?
        //-----------------------------------------
        if ( $css['css_added_to'] == $setID )
        {
            if ( $css['css_group'] != $css_group )
            {
                $cssTest = $this->DB->buildAndFetch( array( 'select' => '*',
                                                            'from'   => 'skin_css',
                                                            'where'  => 'css_set_id=' . $setID . ' AND css_group='' . $css_group . ''' ) );
                if ( $cssTest['css_id'] )
                {
                    throw new Exception( "CSS_EXISTS" );
                }
                $_goClean = 1;
            }
        }
        else
        {
            $css_group = $css['css_group'];
        }
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Fix up content
        //-----------------------------------------
        $css_content = IPSText::formToText( $css_content );
        $css_attributes = IPSText::formToText( $css_attributes );
        //-----------------------------------------
        // Same CSS?
        //-----------------------------------------
        if ( $setID == $css['css_set_id'] )
        {
            $this->DB->update( 'skin_css', array( 'css_content'      => $css_content,
                                                  'css_group'        => $css_group,
                                                  'css_position'     => $css_position,
                                                  'css_attributes'     => $css_attributes,
                                                  'css_app'            => $css_app,
                                                  'css_app_hide'    => $css_app_hide,
                                                  'css_modules'        => str_replace( ' ', '', $css_modules ),
                                                  'css_updated'      => time() ), 'css_id=' . $cssID );
            $css_id_new = $cssID;
        }
        else
        {
            $this->DB->insert( 'skin_css', array(     'css_set_id'       => $setID,
                                                    'css_content'      => $css_content,
                                                    'css_group'        => $css_group,
                                                    'css_position'     => $css_position   ? $css_position   : $css['css_position'],
                                                    'css_attributes'=> $css_attributes ? $css_attributes : $css['css_attributes'],
                                                    'css_app'        => $css_app        ? $css_app        : $css['css_app'],
                                                    'css_app_hide'    => $css_app_hide   ? $css_app_hide   : $css['css_app_hide'],
                                                    'css_modules'   => str_replace( ' ', '', $css_modules ),
                                                    'css_added_to'     => $css['css_added_to'],
                                                    'css_updated'      => time() ) );
            $css_id_new = $this->DB->getInsertId();
        }
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildCSS( $setID );
        //-----------------------------------------
        // Remove dead-uns?
        //-----------------------------------------
        if ( $_goClean == 1 )
        {
            $this->removeDeadCSSCaches( $setID );
        }
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $css_id_new;
    }
    /**
     * Reverts / Removes CSS
     *
     * @access    public
     * @param    int            CSS ID
     * @param    int            Template Set ID
     * @param    int            From a delete request
     * @return    array         Data from "next in line" CSS (parent, or root)
     * <code>
     * Exception Codes:
     * NO_SUCH_CSS:            Could not locate css id#
     * CANNOT_REMOVE:         Cannot revert / remove the css
     * </code>
     */
    public function revertCSS( $cssID, $setID, $fromDelete=0 )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $cssID   = intval( $cssID );
        $setData = $this->fetchSkinData( $setID );
        //-----------------------------------------
        // Fetch CSS data
        //-----------------------------------------
        $css = $this->DB->buildAndFetch( array( 'select' => '*',
                                                'from'   => 'skin_css',
                                                'where'  => 'css_id=' . $cssID ) );
        if ( ! $css['css_id'] )
        {
            throw new Exception( 'NO_SUCH_CSS' );
        }
        /* Inherited from master */
        if ( $css['css_added_to'] == 0 )
        {
            /* Did we want to delete? */
            if ( $fromDelete )
            {
                /* Inherited from master css */
                if ( $css['css_set_id'] > 0 )
                {
                    /* Clear any existing CSS if it's been modified from set 0 */
                    $this->DB->delete( 'skin_css', 'css_id=' . $cssID );
                }
            
                /* Add one as 'removed' */
                $this->DB->insert( 'skin_css', array( 'css_set_id'   => $setID,
                                                      'css_updated'  => time(),
                                                      'css_group'    => $css['css_group'],
                                                      'css_added_to' => $setID,
                                                      'css_removed'  => 1 ) );
            }
            else
            {
                /* We're reverting - just make sure it's not a master CSS file */
                if ( $css['css_set_id'] > 0 )
                {
                    $this->DB->delete( 'skin_css', 'css_id=' . $cssID );
                }
            }
        }
        /* Modified or added to this skin set or a parent
           Remove and revert do the same thing here... */
        else
        {
            /* Fix up child array */
            array_unshift( $setData['_childTree'], $setID );
            
            /* This is CSS unique to this skin set, so remove it from this set and all children */
            $this->DB->delete( 'skin_css', "css_group='" . $css['css_group'] . "' AND css_set_id IN(" . implode( ',', $setData['_childTree'] ) . ")" );
            # Remove dead caches
            $this->removeDeadCSSCaches( $setID );
        }
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildCSS( $setID );
        
        //-----------------------------------------
        // Grab the next template bit inline...
        //-----------------------------------------
        $cssData = $this->fetchCSS( $setID, TRUE );
            
        //-----------------------------------------
        // Reeee-turn
        //-----------------------------------------
        return $cssData;
    }
    
    /**
     * Fetch Templates from the tree
     *
     * @access    public
     * @param    mixed        Skin key or    Skin set ID
     * @param    string        Type of data to return: 'allTemplates' will return the data [template_group][template_name], 'allNoContent' the same as 'allTemplates' minus the actual template content, 'groupNames'; [template_group] or groupTemplates, just that groups templates [template_name], groupTemplatesNoContent is the same as groupTemplates but template_content is removed
     * @param    string        Which group to use
     * @return    array         Array of data depending on the params
     * <code>
     * Usage:
     * # To return all skin 'groups' (eg, skin_global, skin_topics, etc)
     * $groups = $skinFunctions->fetchTemplates( 1, 'groupNames' );
     * # To return all templates within group 'skin_global'
     * $templates = $skinFunctions->fetchTemplates( 1, 'groupTemplates', 'skin_global' );
     * # To return all templates in all groups
     * $templates = $skinFunctions->fetchTemplates( 1, 'allTemplates');
     * # To return all master templates for the mobile skin
     * $templates = $skinFunctions->fetchTemplates( 'mobile', 'allTemplates' );
     * </code>
     */
    public function fetchTemplates( $setID, $type='allTemplates', $group='')
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $templates     = array();
        $where           = '';
    
        /* Did we pass a master key? - all root skins are 0  */
        if ( ! is_numeric( $setID ) )
        {
            $skinSetData = array( 'set_id'          => 0,
                                  'set_master_key'  => $setID,
                                  'set_key'            => $setID,
                                  '_isMaster'        => 1,
                                  '_parentTree'     => array(),
                                  '_childTree'      => array(),
                                  '_userAgents'     => array(),
                                  '_cssGroupsArray' => array() );
        }
        else
        {        
            $skinSetData                = $this->fetchSkinData( $setID );
            $skinSetData['_isMaster']    = $skinSetData['_isMaster'] ? $skinSetData['_isMaster'] : 0;
        }
        
        /* Did we get a skin set? */
        if ( ! isset( $skinSetData['_parentTree'] ) OR ! is_array( $skinSetData['_parentTree'] ) )
        {
            return array();
        }
        
        //-----------------------------------------
        // Push root ID onto the END of the parent array
        //-----------------------------------------
        array_push( $skinSetData['_parentTree'], 0 );
        //-----------------------------------------
        // Push the current skin set ID onto the beginnging
        //-----------------------------------------
        
        if ( is_numeric( $setID ) )
        {
            array_unshift( $skinSetData['_parentTree'], $setID );
        }
        
        /* We want to capture only this set's master bits */
        $where = ' AND ( ( template_set_id > 0 AND ( template_master_key='' OR template_master_key IS NULL ) ) OR ( template_set_id=0 AND template_master_key='' . $skinSetData['set_master_key'] . '' ) )';
        
        /* First off, load 'root' skin as we tend to develop this the most and there is always a small chance other master skins won't
           contain all the template bits. We'll just load the group / name and leave the content blank to prevent parse errors, etc */
        if ( $setID != 'root' )
        {
            if ( ( $type == 'groupTemplates' OR $type == 'groupTemplatesNoContent' ) and $group != '' )
            {
                 $_w = " AND template_group='{$group}'";
            }
            
            $this->DB->build( array( 'select'    => '*',
                                     'from'        => 'skin_templates',
                                     'where'    => "template_master_key='root'" . $_w,
                                     'order'    => 'template_group' ) );
                                     
            $this->DB->execute();
        
            while( $r = $this->DB->fetch() )
            {
                $r['template_content'] = '<!--no data in this master skin-->';
                
                if ( $type == 'groupNames' )
                {
                    $templates[ $r['template_group'] ] = $r;
                }
                else if ( $type == 'groupTemplates' OR $type == 'groupTemplatesNoContent' )
                {
                    $r['_templateSize'] = IPSLib::sizeFormat( IPSLib::strlenToBytes( strlen( $r['template_content'] ) ) );
    
                    if ( $type == 'groupTemplatesNoContent' )
                    {
                        unset( $r['template_data'] );
                        unset( $r['template_content'] );
                    }
                    
                    $templates[ strtolower($r['template_name']) ] = $r;
                }
                else
                {
                    if ( $type == 'allNoContent' )
                    {
                        unset( $r['template_content'] );
                    }
                    
                    $templates[ $r['template_group'] ][ strtolower($r['template_name']) ] = $r;
                }
            }
        }
        
        //-----------------------------------------
        // Ok, what to return?
        //-----------------------------------------
        if ( $type == 'groupNames' )
        {
            # Just return group titles
            $this->DB->build( array( 'select'    => 'template_group, template_set_id, template_id, template_name, template_data,' .
                                                        $this->DB->buildInstring( "," . implode( ",", $skinSetData['_parentTree'] ) . ",",
                                                        $this->DB->buildConcat( array( array( ',', 'string' ), array( 'template_set_id' ), array( ',', 'string' ) ) ) ) . ' as theorder',
                                           'from'    => 'skin_templates',
                                           'where'    => "template_set_id IN (" . implode( ",", $skinSetData['_parentTree'] ) ." )" . $where,
                                           'order'    => 'template_group, theorder DESC'
                                )        );
            $newq = $this->DB->execute();
        }
        else if ( ( $type == 'groupTemplates' OR $type == 'groupTemplatesNoContent' ) and $group != '' )
        {
            # Return group template bits
            $this->DB->build( array( 'select'    => '*,' .
                                                       $this->DB->buildInstring( "," . implode( ",", $skinSetData['_parentTree'] ) . ",",
                                                       $this->DB->buildConcat( array( array( ',', 'string' ), array( 'template_set_id' ), array( ',', 'string' ) ) ) ) . ' as theorder',
                                            'from'    => 'skin_templates',
                                            'where'    => "template_set_id IN (" . implode( ",", $skinSetData['_parentTree'] ) . ") AND template_group='{$group}'" . $where,
                                            'order'    => 'template_name, theorder DESC'
                                )        );
            $newq = $this->DB->execute();
        }
        else
        {
            # Return all...
            $this->DB->build( array( 'select'    => '*,' .
                                                       $this->DB->buildInstring( "," . implode( ",", $skinSetData['_parentTree'] ) . ",",
                                                       $this->DB->buildConcat( array( array( ',', 'string' ), array( 'template_set_id' ), array( ',', 'string' ) ) ) ) . ' as theorder',
                                            'from'    => 'skin_templates',
                                            'where'    => "template_set_id IN (" . implode( ",", $skinSetData['_parentTree'] ) .")" . $where,
                                            'order'    => 'template_group, template_name, theorder DESC'
                                )        );
            $newq = $this->DB->execute();
        }
        //-----------------------------------------
        // Get all results
        //-----------------------------------------
        while ( $r = $this->DB->fetch($newq) )
        {
            if ( isset( $r['template_name'] ) )
            {
                if ( substr( $r['template_name'], 0, 2 ) == '__' )
                {
                    continue;
                }
            }
            //-----------------------------------------
            // Build counts
            //-----------------------------------------
            $this->_templateCount[ $r['template_set_id'] ] = isset($this->_templateCount[ $r['template_set_id'] ]) ? $this->_templateCount[ $r['template_set_id'] ] : array();
            $this->_templateCount[ $r['template_set_id'] ][ $r['template_group'] ] = isset($this->_templateCount[ $r['template_set_id'] ][ $r['template_group'] ]) ? $this->_templateCount[ $r['template_set_id'] ][ $r['template_group'] ] : array( 'count' => 0 );
            $this->_templateCount[ $r['template_set_id'] ][ $r['template_group'] ]['count']++;
            if ( $type == 'groupNames' )
            {
                $templates[ $r['template_group'] ] = $r;
            }
            else if ( $type == 'groupTemplates' OR $type == 'groupTemplatesNoContent' )
            {
                $r['_templateSize'] = IPSLib::sizeFormat( IPSLib::strlenToBytes( strlen( $r['template_content'] ) ) );
                if ( $type == 'groupTemplatesNoContent' )
                {
                    unset( $r['template_data'] );
                    unset( $r['template_content'] );
                }
                
                $templates[ strtolower($r['template_name']) ] = $r;
            }
            else
            {
                if ( $type == 'allNoContent' )
                {
                    unset( $r['template_content'] );
                }
                
                $templates[ $r['template_group'] ][ strtolower($r['template_name']) ] = $r;
            }
        }
        ksort($templates);
        
        foreach( $templates as $k => $v )
        {
            if( is_array($v) )
            {
                ksort($templates[ $k ]);
            }
        }
        return $templates;
    }
    /**
     * Fetch template bit for editing
     *
     * @access    public
     * @param    int            Template ID
     * @param    int            Template Set ID
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * NO_SUCH_TEMPLATE:        Could not locate template id#
     * </code>
     */
    public function fetchTemplateBitForEdit( $templateID, $setID )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $templateID = intval( $templateID );
        //-----------------------------------------
        // Fetch template data
        //-----------------------------------------
        $template = $this->DB->buildAndFetch( array( 'select' => '*',
                                                     'from'   => 'skin_templates',
                                                     'where'  => 'template_id=' . $templateID ) );
        if ( ! $template['template_id'] )
        {
            throw new Exception( 'NO_SUCH_TEMPLATE' );
        }
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Update data
        //-----------------------------------------
        $template['_actualSetID']      = $template['template_set'];
        $template['_belongsSetID']     = $setID;
        $template['_template_content'] = $template['template_content'];
        $template['_template_data']    = $template['template_data'];
        $template['_isMaster']           = ( $template['template_master_key'] ) ? 1 : 0;
        //-----------------------------------------
        // Update content for editing
        //-----------------------------------------
        $template['template_content'] = IPSText::textToForm( $template['template_content'] );
        //-----------------------------------------
        // Update function data for editing
        //-----------------------------------------
        $template['template_data'] = IPSText::textToForm( $template['template_data'] );
        //-----------------------------------------
        // Return it
        //-----------------------------------------
        return $template;
    }
    /**
     * Reverts a template bit
     *
     * @access    public
     * @param    int            Template ID
     * @param    int            Template Set ID
     * @return    array         Data from "next in line" template bit (parent, or root)
     * <code>
     * Exception Codes:
     * NO_SUCH_TEMPLATE:    Could not locate template id#
     * CANNOT_REMOVE:         Cannot revert / remove the template bit
     * </code>
     */
    public function revertTemplateBit( $templateID, $setID )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $templateID = intval( $templateID );
        $_goClean   = 0;
        //-----------------------------------------
        // Fetch template data
        //-----------------------------------------
        $template = $this->DB->buildAndFetch( array( 'select' => '*',
                                                     'from'   => 'skin_templates',
                                                     'where'  => 'template_id=' . $templateID ) );
        if ( ! $template['template_id'] )
        {
            throw new Exception( 'NO_SUCH_TEMPLATE' );
        }
        //-----------------------------------------
        // Is this a master skin bit?
        //-----------------------------------------
        if ( $template['template_set_id'] == 0 AND $template['template_added_to'] == 0 )
        {
            throw new Exception( "CANNOT_REMOVE");
        }
        //-----------------------------------------
        // Remove it...
        //-----------------------------------------
        if ( $template['template_added_to'] == $setID  )
        {
            /* Does it exist in set ID 0? */
            $test = $this->DB->buildAndFetch( array( 'select' => 'template_id',
                                                      'from'   => 'skin_templates',
                                                      'where'  => "template_set_id=0 AND template_user_added=0 AND template_name='" . $template['template_name'] . "' AND template_group='" . $template['template_group'] . "'" ) );
            
            /* Is from master skin? */
            if ( $test['template_id'] )
            {
                $this->DB->delete( 'skin_templates', 'template_id=' . $templateID );
            }
            else
            {
                # Remove it from ALL template sets
                $this->DB->delete( 'skin_templates', "template_name='" . $template['template_name'] . "' AND template_group='" . $template['template_group'] . "'" );
            }
            
            $_goClean = 1;
        }
        else
        {
            $this->DB->delete( 'skin_templates', 'template_id=' . $templateID );
        }
        //-----------------------------------------
        // Grab the next template bit inline...
        //-----------------------------------------
        $templates = $this->fetchTemplates( $setID, 'groupTemplates', $template['template_group'] );
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildPHPTemplates( $setID, $template['template_group'] );
        //-----------------------------------------
        // Clean up?
        //-----------------------------------------
        if ( $_goClean )
        {
            $this->removeDeadPHPCaches( $setID );
        }
        //-----------------------------------------
        // Reeee-turn
        //-----------------------------------------
        
        if ( $templates[ strtolower( $template['template_name'] ) ]['template_id'] )
        {
            return $this->fetchTemplateBitForEdit( $templates[ strtolower( $template['template_name'] ) ]['template_id'], $setID );
        }
        else
        {
            return array();
        }
    }
    /**
     * Save template bit from 'add' form
     *
     * @access    public
     * @param    int            Template Set ID
     * @param    string        Template content
     * @param    string        Template function data
     * @param    string        Template group
     * @param    string        Template name
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * TEMPLATE_EXISTS:        Template bit name already exists
     * SYNTAX_INCORRECT:    The php syntax is invalid
     * </code>
     */
    public function saveTemplateBitFromAdd( $setID, $template_content, $template_data, $template_group, $template_name )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $template_group   = IPSText::alphanumericalClean( $template_group );
        $template_name    = IPSText::alphanumericalClean( $template_name );
        $template_content = str_replace( '\"', '\"', $template_content );
        
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Does this template bit already exist?
        //-----------------------------------------
        $template = $this->DB->buildAndFetch( array( 
                                                    'select' => '*',
                                                    'from'   => 'skin_templates',
                                                    'where'  => "template_name='" . $template_name . "' AND template_group='" . $template_group . "' AND template_added_to={$setID}" ) );
        if ( $template['template_id'] )
        {
            throw new Exception( 'TEMPLATE_EXISTS' );
        }
        //-----------------------------------------
        // Fix up content
        //-----------------------------------------
        $template_content = IPSText::formToText( $template_content );
        //-----------------------------------------
        // Fix up data
        //-----------------------------------------
        $template_data = IPSText::formToText( $template_data );
        //-----------------------------------------
        // Test it..
        //-----------------------------------------
        if ( $this->testTemplateBitSyntax( $template_name, $template_data, $template_content ) !== TRUE )
        {
            throw new Exception( "SYNTAX_INCORRECT" );
        }
        //-----------------------------------------
        // Same template set?
        //-----------------------------------------
        
        /* @todo - 'root' is almost certainly what we want but we *should* write a method that traverses up the tree
         * from this set ID and looks for the master key of the root set. Especially important if we add any future
         * output engines.
         */
        $this->DB->insert( 'skin_templates', array( 'template_set_id'      => 0,
                                                    'template_group'       => $template_group,
                                                    'template_content'     => $template_content,
                                                    'template_name'        => $template_name,
                                                    'template_data'        => $template_data,
                                                    'template_added_to'    => $setID,
                                                    'template_user_edited' => 1,
                                                    'template_user_added'  => 1,
                                                    'template_removable'   => 1,
                                                    'template_updated'     => time(),
                                                    'template_master_key'  => 'root' ) );
        $template_id_new = $this->DB->getInsertId();
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildPHPTemplates( $setID, $template_group );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $template_id_new;
    }
    /**
     * Save template bit after edit
     *
     * @access    public
     * @param    int            Template ID
     * @param    int            Template Set ID
     * @param    string        Template content
     * @param    string        Template function data
     * @return    array         ..of data
     * <code>
     * Exception Codes:
     * NO_SUCH_TEMPLATE:        Could not locate template id#
     * SYNTAX_INCORRECT:        The template bit syntax is incorrect and would cause a parse error if saved
     * </code>
     */
    public function saveTemplateBitFromEdit( $templateID, $setID, $template_content, $template_data )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $templateID       = intval( $templateID );
        $template_content = str_replace( '\"', '\"', $template_content );
        
        //-----------------------------------------
        // Fetch template data
        //-----------------------------------------
        $template = $this->DB->buildAndFetch( array( 'select' => '*',
                                                     'from'   => 'skin_templates',
                                                     'where'  => 'template_id=' . $templateID ) );
        if ( ! $template['template_id'] )
        {
            throw new Exception( 'NO_SUCH_TEMPLATE' );
        }
        //-----------------------------------------
        // Get skin data
        //-----------------------------------------
        $skinSetData = $this->registry->output->allSkins[ $setID ];
        //-----------------------------------------
        // Fix up content
        //-----------------------------------------
        $template_content = IPSText::formToText( $template_content );
        //-----------------------------------------
        // Fix up data
        //-----------------------------------------
        $template_data = IPSText::formToText( $template_data );
        //-----------------------------------------
        // Test it..
        //-----------------------------------------
        if ( $this->testTemplateBitSyntax( $template['template_name'], $template_data, $template_content ) !== TRUE )
        {
            throw new Exception( "SYNTAX_INCORRECT" );
        }
        //-----------------------------------------
        // Same template set?
        //-----------------------------------------
        if ( $setID == $template['template_set_id'] )
        {
            $this->DB->update( 'skin_templates', array( 'template_data'        => $template_data,
                                                        'template_content'     => $template_content,
                                                        'template_user_edited' => 1,
                                                        'template_updated'     => time() ), 'template_id=' . $templateID );
            $template_id_new = $templateID;
        }
        else
        {
            $this->DB->insert( 'skin_templates', array( 'template_set_id'      => $setID,
                                                        'template_group'       => $template['template_group'],
                                                        'template_content'     => $template_content,
                                                        'template_name'        => $template['template_name'],
                                                        'template_data'        => $template_data,
                                                        'template_added_to'    => $template['template_added_to'],
                                                        'template_user_added'  => $template['template_user_added'],
                                                        'template_user_edited' => 1,
                                                        'template_removable'   => 1,
                                                        'template_updated'     => time() ) );
            $template_id_new = $this->DB->getInsertId();
            # Update count
            $this->_templateCount[ $setID ][ $template['template_group'] ]['count']++;
        }
        
        /* If this is a user-added template bit and we're editing it in the same skin, update master */
        if ( $template['template_id'] && $template['template_user_added'] && $template['template_added_to'] == $setID )
        {
            $this->DB->update( 'skin_templates', array( 'template_content' => $template_content ), "template_set_id=0 AND template_group='" . $template['template_group'] . "' AND template_name='" . $template['template_name'] . "'" );
        }
        //-----------------------------------------
        // Recache
        //-----------------------------------------
        $this->rebuildPHPTemplates( $setID, $template['template_group'] );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return $template_id_new;
    }
    
    /**
     * renames a template title
     * @param int $templateId
     * @param string $newName
     */
    public function renameTemplateName( $templateId, $newName )
    {
        $template  = $this->fetchTemplateBitForEdit( $templateId, 0 );
        $skinSet   = $this->fetchSkinData( $template['template_set_id'] );
        $okToSave  = true;
        $masterKey = 'root';
        
        $update   = array( 'template_name' => $newName );
        
        if ( ( $template['template_set_id'] == $template['template_added_to'] ) )
        {
            $masterTemplates = $this->fetchTemplates( $masterKey, 'allNoContent', $template['template_group'] );
            
            if ( isset( $masterTemplates[ $template['template_group'] ][ $newName ] ) )
            {
                /* Added by webdav, now attempting to rename */
                $update['template_added_to']   = 0;
                $update['template_master_key'] = $masterKey;
            }
        }
        
        if ( ! $template['template_id'] OR $template['_isMaster'] )
        {
            $okToSave = false;
        }
        
        if ( $okToSave )
        {
            $this->DB->update( 'skin_templates', $update, 'template_name='' . $this->DB->addSlashes( $template['template_name'] ) . '' AND template_group='' . $this->DB->addSlashes( $template['template_group'] ) . ''' );
        }
        
        /* rebuild */
        $this->rebuildPHPTemplates( $template['template_set_id'], $template['template_group'] );
    }
    
    /**
     * renames a css title
     * @param int $cssId
     * @param string $newName
     */
    public function renameCssName( $cssId, $newName )
    {
        $template = $this->fetchCssForEdit( $cssId, 0 );
        $okToSave = true;
        $update   = array( 'css_group' => $newName );
        
        if ( ( $template['css_set_id'] == $template['css_added_to'] ) )
        {
            $masterCss = $this->fetchCSS( 'root', false );
            
            if ( isset( $masterCss[ $newName ] ) )
            {
                /* Added by webdav, now attempting to rename */
                $update['css_added_to'] = 0;
            }
        }
        
        if ( ! $template['css_id'] OR $template['_isMaster'] )
        {
            $okToSave = false;
        }
        
        if ( $okToSave )
        {
            $this->DB->update( 'skin_css', $update, 'css_group='' . $this->DB->addSlashes( $template['css_group'] ) . ''' );
        }
        
        /* rebuild */
        $this->rebuildCSS( $template['css_set_id'] );
    }    
    /**
     * Fetch image directories from the file system
     *
     * @access    public
     * @return    array     simple array of filenames
     */
    public function fetchImageDirectories()
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $dirs = array();
        
        try
        {
            foreach( new DirectoryIterator( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_images' ) as $file )
            {
                if ( ! $file->isDot() AND $file->isDir() )
                {
                    $_name = $file->getFileName();
            
                    /* Annoyingly, isDot doesn't match .svn, etc */
                    if ( substr( $_name, 0, 1 ) == '.' )
                    {
                        continue;
                    }
            
                    $dirs[] = $_name;
                }
            }
        } catch ( Exception $e ) {}
        return $dirs;
    }
    /**
     * Fetch emoticon directories from the file system
     *
     * @access    public
     * @return    array     simple array of filenames
     */
    public function fetchEmoticonDirectories()
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $dirs = array();
        
        try
        {
            foreach( new DirectoryIterator( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_emoticons' ) as $file )
            {
                if ( ! $file->isDot() AND $file->isDir() )
                {
                    $_name = $file->getFileName();
            
                    /* Annoyingly, isDot doesn't match .svn, etc */
                    if ( substr( $_name, 0, 1 ) == '.' )
                    {
                        continue;
                    }
            
                    $dirs[] = $_name;
                }
            }
        } catch ( Exception $e ) {}
        return $dirs;
    }
    /**
     * Returns the Image directory
     *
     * @access    public
     * @param    string    Dir name
     * @return    mixed    False, or directory path
     */
    public function fetchImageDirectoryPath( $dir )
    {
        if ( $this->checkImageDirectoryExists( $dir ) === TRUE )
        {
            return DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_images/' . $dir;
        }
        else
        {
            return FALSE;
        }
    }
    /**
     * Check Image directory exists
     *
     * @access    public
     * @param    string    Dir name
     * @return    boolean
     */
    public function checkImageDirectoryExists( $dir )
    {
        if ( ! is_dir( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_images/' . $dir ) )
        {
            return FALSE;
        }
        else
        {
            return TRUE;
        }
    }
    /**
     * Creates a new image directory
     *
     * @access    public
     * @param    string    Dir name
     * @return    boolean
     */
    public function createNewImageDirectory( $dir )
    {
        if ( @mkdir( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_images/' . $dir ) )
        {
            @chmod( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_images/' . $dir, IPS_FOLDER_PERMISSION );
            return TRUE;
        }
    }
    /**
     * Check Emoticon directory exists
     *
     * @access    public
     * @param    string    Dir name
     * @return    boolean
     */
    public function checkEmoticonDirectoryExists( $dir )
    {
        if ( ! is_dir( DOC_IPS_ROOT_PATH . PUBLIC_DIRECTORY . '/style_emoticons/' . $dir ) )
        {
            return FALSE;
        }
        else
        {
            return TRUE;
        }
    }
    /**
     * Fetch output formats
     *
     * @access    public
     * @return    array         array( 'key' => array( $confdata.... ) )
     */
    public function fetchOutputFormats()
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $formats = array();
        $path    = IPS_ROOT_PATH . 'sources/classes/output/formats';
        
        try
        {
            foreach( new DirectoryIterator( $path ) as $file )
            {
                if ( ! $file->isDot() AND $file->isDir() )
                {
                    $_name = $file->getFileName();
            
                    /* Annoyingly, isDot doesn't match .svn, etc */
                    if ( substr( $_name, 0, 1 ) == '.' )
                    {
                        continue;
                    }
                    
                    if( ! is_file( $path . '/' . $_name . '/conf.php' ) )
                    {
                        continue;
                    }
                    
                    $config = array();
                    require( $path . '/' . $_name . '/conf.php' );/*noLibHook*/
                    
                    $formats[ $_name ] = $config;
                }
            }
        } catch ( Exception $e ) {}
        return $formats;
    }
    /**
     * Fetch all template hooks
     *
     * @access    public
     * @param    array         [Array of skin groups to search in. Leave blank to search in all]
     * @return    array         Array of skin groups and their hooks
     */
    public function fetchSkinHooks( $groups=array() )
    {
        /* INIT */
        $myHooks = array();
        
        /* Fetch groups? */
        if ( ! count( $groups ) )
        {
            $groups = array_keys( $this->fetchTemplates( 'root', 'groupNames' ) );
        }
        
        /* Got a hooks cache? */
        if ( ! $this->cache->exists('hooks') )
        {
            $this->cache->rebuildCache('hooks', 'global' );
        }
        
        /* Loop through the cache */
        $hooksCache = $this->cache->getCache( 'hooks' );
        
        if( is_array( $hooksCache['templateHooks'] ) AND count( $hooksCache['templateHooks'] ) )
        {
            foreach( $hooksCache['templateHooks'] as $skinGroup => $hooks )
            {
                /* Check to see if the group is loaded */
                if( ! in_array( $skinGroup, $groups ) )
                {
                    continue;
                }
                foreach( $hooks as $tplHook )
                {
                    /* Check for hook file */
                    if( is_file( IPS_HOOKS_PATH . $tplHook['filename'] ) )
                    {
                        $tplHook['_commentTag'] = $tplHook['type'] . '.' . $skinGroup . '.' . $tplHook['skinFunction'] . '.' . $tplHook['id'] . '.' . $tplHook['position'];
                        $myHooks[ $skinGroup ][] = $tplHook;
                    }
                }
            }
        }
        
        return $myHooks;
    }
    
    /**
     * Flushes and reloads skin set data
     *
     * @access    public
     * @return  void
     */
    public function flushSkinData()
    {
        try
        {
            $skinSetData = $this->registry->output->reloadSkinData();
        }
        catch( Exception $error )
        {
            # We're here 'cos this was called during setup and
            # registry->getClass('output') isn't set up yet, so we
            # just carry on and let the DB get it
            
        }
    }
    
    /**
     * Fetch skin set data
     *
     * @access    public
     * @param    int        Skin set ID
     * @param    bool    Force load from DB
     * @return    array
     */
    public function fetchSkinData( $setID, $forceLoadDB=false )
    {
        //-----------------------------------------
        // Skin ID #0?
        //-----------------------------------------
        if ( $setID == 0 )
        {
            /* This is a master skin, so lets fake it */
            $skinSetData['set_id']            = 0;
            $skinSetData['set_key']            = is_numeric( $setID ) ? 'root' : $setID;
            $skinSetData['set_master_key']  = is_numeric( $setID ) ? 'root' : $setID;
            $skinSetData['_isMaster']        = 1;
            $skinSetData['_parentTree']     = array();
            $skinSetData['_childTree']      = array();
            $skinSetData['_userAgents']     = array();
            $skinSetData['_cssGroupsArray'] = array();
            return $skinSetData;
        }
        //-----------------------------------------
        // Try and get the skin from the cache
        //-----------------------------------------
        try
        {
            $skinSetData = $this->registry->output->allSkins[ $setID ];
        }
        catch( Exception $error )
        {
            # We're here 'cos this was called during setup and
            # registry->getClass('output') isn't set up yet, so we
            # just carry on and let the DB get it
        }
        $skinSetData['_parentTree']     = $skinSetData['_parentTree'] ? $skinSetData['_parentTree'] : array();
        $skinSetData['_childTree']      = $skinSetData['_childTree'] ? $skinSetData['_childTree'] : array();
        $skinSetData['_userAgents']     = $skinSetData['_userAgents'] ? $skinSetData['_userAgents'] : array();
        $skinSetData['_cssGroupsArray'] = $skinSetData['_cssGroupsArray'] ? $skinSetData['_cssGroupsArray'] : array();
        
        if ( is_array( $this->remapData ) AND count( $this->remapData ) )
        {
            $skinSetData['_isMaster'] = ( $this->remapData['templates'][ $setID ] ) ? 1 : 0;
        }
        
        //-----------------------------------------
        // Got nothing?
        //-----------------------------------------
        if ( $forceLoadDB OR ( $setID > 0 AND ! $skinSetData['set_id'] ) )
        {
            $skinSetData = $this->DB->buildAndFetch( array( 'select' => '*',
                                                            'from'   => 'skin_collections',
                                                            'where'  => 'set_id=' . $setID ) );
            $skinSetData['_parentTree']     = unserialize( $skinSetData['set_parent_array'] );
            $skinSetData['_childTree']      = unserialize( $skinSetData['set_child_array'] );
            $skinSetData['_userAgents']     = unserialize( $skinSetData['set_locked_uagent'] );
            $skinSetData['_cssGroupsArray'] = unserialize( $skinSetData['set_css_groups'] );
            
            if ( is_array( $this->remapData ) AND count( $this->remapData ) )
            {
                $skinSetData['_isMaster'] = ( $this->remapData['templates'][ $setID ] ) ? 1 : 0;
            }
            
            if ( is_array( $skinSetData['_cssGroupsArray'] ) )
            {
                ksort( $skinSetData['_cssGroupsArray'], SORT_NUMERIC );
            }
            else
            {
                $skinSetData['_cssGroupsArray'] = array();
            }
        }
        
        /* Fail safe */
        $skinSetData['set_master_key'] = ( $skinSetData['set_master_key'] ) ? $skinSetData['set_master_key'] : 'root';
        return $skinSetData;
    }
    
    /**
     * Removes customizations in a skin set
     *
     * @access    public
     * @param    int            Skin set ID
     * @param    array         Items to revert ( 'templates' => TRUE/FALSE, 'css' => TRUE/FALSE, 'replacements' => TRUE/FALSE )
     * @return    boolean
     */
    public function removeCustomizations( $setId, $which=array() )
    {
        /* INIT */
        $setId  = intval( $setId );
        
        /* Templates */
        if ( !empty( $which['templates'] ) )
        {
            $this->DB->delete( 'skin_templates', 'template_set_id=' . $setId );
        }
        
        /* CSS */
        if ( !empty( $which['css'] ) )
        {
            $this->DB->delete( 'skin_css', 'css_set_id=' . $setId );
        }
        
        /* Replacements */
        if ( !empty( $which['replacements'] ) )
        {
            $this->DB->delete( 'skin_replacements', 'replacement_set_id=' . $setId );
        }
        
        /* Recache */
        if ( method_exists( $this, 'rebuildReplacementsCache' ) )
        {
            $this->rebuildReplacementsCache( $setId );
            $this->rebuildCSS( $setId );
            $this->rebuildPHPTemplates( $setId );
        }
        
        return TRUE;
    }
    
    
    /**
     * Fetch number of customizations in a skin set
     *
     * @access    public
     * @param    int            Skin set ID
     * @return    array         ( 'templates' => x, 'css' => x, 'replacements' => x )
     */
    public function fetchCustomizationCount( $setId )
    {
        /* INIT */
        $return = array( 'templates' => 0, 'css' => 0, 'replacements' => 0 );
        $setId  = intval( $setId );
        
        /* Templates.. */
        $c = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as count',
                                              'from'   => 'skin_templates',
                                              'where'  => 'template_set_id=' . $setId ) );
                                            
        $return['templates'] = intval( $c['count'] );
        
        /* CSS */
        $c = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as count',
                                              'from'   => 'skin_css',
                                              'where'  => 'css_set_id=' . $setId ) );
                                            
        $return['css'] = intval( $c['count'] );
        
        /* Replacements */
        $c = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as count',
                                              'from'   => 'skin_replacements',
                                              'where'  => 'replacement_set_id=' . $setId ) );
                                            
        $return['replacements'] = intval( $c['count'] );
        
        return $return;
    }
    
    /**
     * Test template bit syntax
     *
     * @access    public
     * @param    string        Template name
     * @param    string        Template function data
     * @param    string        Template content
     * @return    boolean        [ TRUE == OK ]
     */
    public function testTemplateBitSyntax( $name, $data, $content )
    {
        //-----------------------------------------
        // INIT
        //-----------------------------------------
        $return = '';
        $this->_resetMessageHandle();
        //-----------------------------------------
        // Test...
        //-----------------------------------------
        $eval = $this->registry->templateEngine->convertHtmlToPhp( 'test__' . $name, $data, $content );
        
        ob_start();
        eval( $eval );
        $return = ob_get_contents();
        ob_end_clean();
        //-----------------------------------------
        // More data...
        //-----------------------------------------
        $this->_addMessage( $return );
        //-----------------------------------------
        // Return
        //-----------------------------------------
        return ($return) ? FALSE : TRUE;
    }
    
    /**
     * Minify text
     *
     * @access    public
     * @param    string        Text to minify
     * @param    string        Type to minify (css/html). Default is css
     * @return    string        Minified text
     */
    
    public function minify( $text, $type='css' )
    {
        $type = strtolower( $type );
        
        /* What to do? */
        if ( $type == 'css' )
        {
            require_once( IPS_PUBLIC_PATH . 'min/lib/Minify/CSS/Compressor.php' );/*noLibHook*/
            
            $text = Minify_CSS_Compressor::process( $text );
        }
        else if ( $type == 'html' )
        {
            require_once( IPS_PUBLIC_PATH . 'min/lib/Minify/HTML.php' );/*noLibHook*/
            
            $text = Minify_HTML::minify( $text, array( 'xhtml' => 1 ) );
        }
        
        return $text;
    }
    
    /**
     * Clean out templates DB
     * Goes through and removes any 'dead' templates
     *
     * @access    public
     * @return    array        ( 'cached' => x, 'templates' => x ) Number of template bits removed
     */
    public function cleanDbCss()
    {
        /* INIT */
        $sets     = array_keys( $this->registry->output->allSkins );
        $affected = 0;
        $cached   = 0;
        
        /* Remove from cache: sets */
        $this->DB->delete( 'skin_cache', 'cache_type='css' AND cache_set_id NOT IN ( 0,' . implode( ',', $sets ) . ')' );
        
        $cached += intval( $this->DB->getAffectedRows() );
        
        /* Clean out any template bits no longer assigned to a valid skin set */
        $this->DB->delete( 'skin_css', 'css_set_id NOT IN ( 0,' . implode( ',', $sets ) . ')' );
        
        $affected += intval( $this->DB->getAffectedRows() );
        
        return array( 'cached' => intval( $cached ), 'templates' => intval( $affected ) );
    }    
    
    
    /**
     * Clean out templates DB
     * Goes through and removes any 'dead' templates
     *
     * @access    public
     * @return    array        ( 'cached' => x, 'templates' => x ) Number of template bits removed
     */
    public function cleanDbTemplates()
    {
        /* INIT */
        $sets     = array_keys( $this->registry->output->allSkins );
        $groups   = $this->fetchTemplates( 0, 'groupNames' );
        $affected = 0;
        $cached   = 0;
        
        /* Remove from cache: sets */
        $this->DB->delete( 'skin_cache', 'cache_type='phptemplate' AND cache_set_id NOT IN ( 0,' . implode( ',', $sets ) . ')' );
        
        $cached += intval( $this->DB->getAffectedRows() );
        
        /* Remove from cache: non-existant groups */
        $this->DB->delete( 'skin_cache', "cache_type='phptemplate' AND cache_value_1 NOT IN ('" . implode( "','", array_keys( $groups ) ) . "')" );
        
        $cached += intval( $this->DB->getAffectedRows() );
        
        /* Clean out any template bits no longer assigned to a valid skin set */
        $this->DB->delete( 'skin_templates', 'template_set_id NOT IN ( 0,' . implode( ',', $sets ) . ')' );
        
        $affected += intval( $this->DB->getAffectedRows() );
        
        /* Loop through groups */
        foreach( $groups as $_group => $_data )
        {
            $templates = $this->fetchTemplates( 0, 'groupTemplatesNoContent', $_group );
            
            /* Clean out any template bits no longer part of this group */
            $this->DB->delete( 'skin_templates', "template_group='" . $_group . "' AND template_user_added=0 AND " . $this->DB->buildLower('template_name') . " NOT IN ('" . implode( "','", array_keys( $templates ) ) . "')" );
            $affected += intval( $this->DB->getAffectedRows() );
        }
        
        return array( 'cached' => intval( $cached ), 'templates' => intval( $affected ) );
    }
    
    /**
     * Rebuilds the mobile user agents from the skin set data
     */
    public function rebuildMobileSkinUserAgentsFromSetDataXml()
    {
        /* Init */
        $mobileSkinSet = $this->fetchSkinData( $this->fetchSetIdByKey( 'mobile', true ), true );
        $xmlData       = array();
        
        /* Grab xml */
        require_once( IPS_KERNEL_PATH . 'classXML.php' );/*noLibHook*/
        $xml    = new classXML( 'UTF-8' );
        
        /* Skin Set Data */
        $xml->load( IPS_ROOT_PATH . 'setup/xml/skins/setsData.xml' );
        foreach( $xml->fetchElements( 'set' ) as $xmlelement )
        {
            $data = $xml->fetchElementsFromRecord( $xmlelement );
            if ( $data['set_key'] == 'mobile' )
            {
                $xmlData = $data;
                break;
            }
        }
        
        /* Update */
        if ( $xmlData['set_key'] && IPSLib::isSerialized( $xmlData['set_locked_uagent'] ) && IPSLib::isSerialized( $mobileSkinSet['set_locked_uagent'] ) )
        {
            $new = unserialize( $xmlData['set_locked_uagent'] );
            $old = unserialize( $mobileSkinSet['set_locked_uagent'] );
            
            /* Merge them */
            foreach( $new['groups'] as $group )
            {
                if ( ! in_array( $group, $old['groups'] ) )
                {
                    $old['groups'][] = $group;
                }
            }
            
            foreach( $new['uagents'] as $agent => $version )
            {
                if ( ! in_array( $agent, $new['uagents'] ) )
                {
                    $old['uagents'][ $agent ] = $version;
                }
            }
        }    
        
        if ( is_array( $old ) && count( $old ) )
        {
            $this->DB->update( 'skin_collections', array( 'set_locked_uagent' => serialize( $old ) ), "set_key='mobile'" );
        }
    }
}