Seditio Source
Root |
./othercms/ips_4.3.4/system/Theme/Theme.php
<?php
/**
 * @brief        Skin Set
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        16 Apr 2013
 */

namespace IPS;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Skin set
 */
class _Theme extends \IPS\Node\Model
{
   
/**
     * @brief    [ActiveRecord] Multiton Store
     */
   
protected static $multitons;
   
   
/**
     * @brief    [ActiveRecord] Database Table
     */
   
public static $databaseTable = 'core_themes';
   
   
/**
     * @brief    [ActiveRecord] Database Prefix
     */
   
public static $databasePrefix = 'set_';
   
   
/**
     * @brief    [ActiveRecord] ID Database Column
     */
   
public static $databaseColumnId = 'id';
   
   
/**
     * @brief    [ActiveRecord] Database ID Fields
     */
   
protected static $databaseIdFields = array('set_id', 'set_key');
   
   
/**
     * @brief    [ActiveRecord] Multiton Map
     */
   
protected static $multitonMap    = array();
   
   
/**
     * @brief    [Node] Parent Node ID Database Column
     */
   
public static $databaseColumnParent = 'parent_id';
   
   
/**
     * @brief    [Node] Order Database Column
     */
   
public static $databaseColumnOrder = 'order';
   
   
/**
     * @brief    [Node] Node Title
     */
   
public static $nodeTitle = 'menu__core_customization_themes';
   
   
/**
     * @brief    [Node] Show forms modally?
     */
   
public static $modalForms = TRUE;

   
/**
     * @brief    [Node] Title prefix.  If specified, will look for a language key with "{$key}_title" as the key
     */
   
public static $titleLangPrefix = 'core_theme_set_title_';
   
   
/**
     * @brief    IN_DEV "theme"
     */
   
protected static $inDevTheme = NULL;
   
   
/**
     * @brief    Setup "theme"
     */
   
protected static $setupSkin = NULL;
   
   
/**
     * @brief    Member's "theme"
     */
   
public static $memberTheme = NULL;
   
   
/**
     * @brief    [SkinSets] Store theme set parent/id relationship for parent/child recursion
     */
   
public static $themeSetRelationships = array();
   
   
/**
     * @brief    Have fetched all?
     */
   
protected static $gotAll = FALSE;
   
   
/**
     * @brief    [SkinSets] Stores the default theme set id
     */
   
public static $defaultFrontendThemeSet = 0;
   
   
/**
     * @brief    [SkinSets] Stores the default ACP theme set id
     */
   
public static $defaultAcpThemeSet = 0;
   
   
/**
     * @brief    [SkinSets] Templates already loaded and evald via getTemplate()
     */
   
public static $calledTemplates = array();
   
   
/**
     * @brief    [SkinSets] Some CSS files are built from a directory to save on http requests. They are saved as {$location}_{$folder}.css (so front_responsive.css for example)
     */
   
protected static $buildGrouping = array(
           
'css'  => array(
               
'core' => array(
                   
'global'=> array( 'framework', 'responsive' ),
                   
'front' => array( 'custom' ),
                   
'admin' => array( 'core', 'responsive' )
                    )
                )
            );
   
   
/**
     * @brief    Return type for getRawTemplates/getRawCss: Return all
     */
   
const RETURN_ALL = 1;
   
   
/**
     * @brief    Return type for getRawTemplates/getRawCss: Return groups and names as a tree
     */
   
const RETURN_BIT_NAMES = 2;
   
   
/**
     * @brief    Return type for getRawTemplates/getRawCss: Return groups and names as a tree with array of data without content
     */
   
const RETURN_ALL_NO_CONTENT = 4;
   
   
/**
     * @brief    Return type for getRawTemplates/getRawCss: Returns bit names as a flat array
     */
   
const RETURN_ARRAY_BIT_NAMES = 8;
   
   
/**
     * @brief    Return type for getRawTemplates/getRawCss: Uses DB if not IN_DEV, otherwise uses disk .phtml look up
     */
   
const RETURN_NATIVE = 16;

   
/**
     * @brief    Type for templates
     */
   
const TEMPLATES = 1;
   
   
/**
     * @brief    Type for CSS
     */
   
const CSS = 2;
   
   
/**
     * @brief    Type for Images
     */
   
const IMAGES = 4;
   
   
/**
     * @brief Bit option for theme settings
     */
   
const THEME_KEY_VALUE_PAIRS = 1;
   
   
/**
     * @brief Bit option for theme settings
     */
   
const THEME_ID_KEY = 2;
   
   
/**
     * Get currently logged in member's theme
     *
     * @return    \IPS\Theme
     */
   
public static function i()
    {
        if ( \
IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'setup' )
        {
            if ( static::
$setupSkin === NULL )
            {
                static::
$setupSkin = new \IPS\Theme\Setup\Theme;
            }
            return static::
$setupSkin;
        }
        else if ( \
IPS\RECOVERY_MODE )
        {
            if ( static::
$memberTheme === NULL )
            {
                foreach( static::
themes() as $theme )
                {
                    if ( !
$theme->hasCustomizations() )
                    {
                        static::
$memberTheme = $theme;
                        break;
                    }
                }
            }

           
/* Still here because all themes are customized? */
           
if ( static::$memberTheme === NULL )
            {
               
$newTheme = new \IPS\Theme;
               
$newTheme->permissions = \IPS\Member::loggedIn()->member_group_id;
               
$newTheme->save();
               
$newTheme->installThemeSettings();
               
$newTheme->copyResourcesFromSet();
               
                \
IPS\Lang::saveCustom( 'core', "core_theme_set_title_" . $newTheme->id, "IPS Default" );
               
                static::
$memberTheme = $newTheme;
            }

            return static::
$memberTheme;
        }
        else if ( \
IPS\Theme::designersModeEnabled() )
        {
            if ( \
IPS\IN_DEV )
            {
                die(
"Please disable IN_DEV while in Designer's Mode");
            }
            if ( static::
$memberTheme === NULL )
            {
                static::
themes();
       
                static::
$memberTheme = new \IPS\Theme\Advanced\Theme;

               
/* Add in the default theme properties (_data array, etc) */
               
foreach( static::$multitons[ \IPS\Theme\Advanced\Theme::$currentThemeId ] as $k => $v )
                {
                    static::
$memberTheme->$k = $v;
                }
            }

            return static::
$memberTheme;
        }
        else if ( \
IPS\IN_DEV )
        {
            if ( static::
$inDevTheme === NULL )
            {
                static::
$inDevTheme = new \IPS\Theme\Dev\Theme;
       
                static::
themes();
       
               
/* Add in the default theme properties (_data array, etc) */
               
$default = ( isset( static::$multitons[ \IPS\DEFAULT_THEME_ID ] ) ) ? static::$multitons[ \IPS\DEFAULT_THEME_ID ] : reset( static::$multitons );

                foreach(
$default as $k => $v )
                {
                    static::
$inDevTheme->$k = $v;
                }
            }
           
            return static::
$inDevTheme;
        }
        else
        {
            if ( static::
$memberTheme === NULL )
            {
                static::
themes();

               
$column    = 'skin';

                if( \
IPS\Dispatcher::hasInstance() )
                {
                   
$column    = ( \IPS\Dispatcher::i()->controllerLocation == 'admin' ) ? 'acp_skin' : 'skin';
                }
               
               
$setId = NULL;
                if ( \
IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
                {
                   
$setId = \IPS\Session\Front::i()->getTheme();
                    if (
$setId )
                    {
                        if( ! \
IPS\Request::i()->isAjax() )
                        {
                           
/* Not an ajax call, so reset theme_id */
                           
$setId = NULL;
                            \
IPS\Session\Front::i()->setTheme(0);
                        }
                        else
                        {
                            try
                            {
                                if ( static::
load( $setId )->canAccess() !== true )
                                {
                                   
$setId = NULL;
                                }
                            } catch ( \
OutOfRangeException $ex ){}
                        }

                    }
                }
                if ( !
$setId and \IPS\Member::loggedIn()->$column and array_key_exists( \IPS\Member::loggedIn()->$column, static::themes() ) )
                {
                   
$setId = \IPS\Member::loggedIn()->$column;
                       
                    if ( static::
load( $setId )->canAccess() !== true )
                    {
                       
$setId = ( $column == 'skin' ? static::defaultTheme() : static::defaultAcpTheme() );
                       
                       
/* Restore default theme for member */
                       
\IPS\Member::loggedIn()->$column = $setId;

                        if( \
IPS\Member::loggedIn()->member_id )
                        {
                            \
IPS\Member::loggedIn()->save();
                        }
                    }
                }
                else if ( !
$setId )
                {
                   
$setId = ( $column == 'skin' ? static::defaultTheme() : static::defaultAcpTheme() );
                }
               
                if ( isset( \
IPS\Request::i()->cookie['vseThemeId'] ) AND \IPS\Member::loggedIn()->isAdmin() AND \IPS\Member::loggedIn()->members_bitoptions['bw_using_skin_gen'] )
                {
                   
$setId = intval( \IPS\Request::i()->cookie['vseThemeId'] );
                   
                    if ( ! empty(
$setId ) )
                    {
                       
$ok = false;
                       
                        try
                        {
                           
$theme = static::load( $setId );
                           
                            if (
$theme->by_skin_gen )
                            {
                               
$ok = true;
                            }
                        }
                        catch( \
OutOfRangeException $ex )
                        {
                           
$ok = false;
                        }    
                       
                        if (
$ok !== true )
                        {
                           
/* Update the current member */
                           
\IPS\Member::loggedIn()->members_bitoptions['bw_using_skin_gen'] = 0;
                            \
IPS\Member::loggedIn()->save();
                           
                            \
IPS\Request::i()->setCookie( 'vseThemeId', 0 );
                       
                           
$setId = static::defaultTheme();
                        }
                    }
                    else
                    {
                       
$setId = static::defaultTheme();
                    }
                }
               
                static::
$memberTheme = static::load( $setId );
            }
           
            return static::
$memberTheme;
        }
    }
   
   
/**
     * Themes
     *
     * @return    array
     */
   
public static function themes()
    {
        if ( !static::
$gotAll )
        {
            static::
$gotAll = true;
            static::
$themeSetRelationships = array();
           
            if ( isset( \
IPS\Data\Store::i()->themes ) )
            {
               
$rows = \IPS\Data\Store::i()->themes;
            }
            else
            {
               
$rows = iterator_to_array( \IPS\Db::i()->select( '*', 'core_themes', NULL, 'set_order' )->setKeyField('set_id') );
                \
IPS\Data\Store::i()->themes = $rows;
            }
           
            foreach(
$rows as $id => $theme )
            {
                if (
$theme['set_is_default'] )
                {
                    static::
$defaultFrontendThemeSet = $theme['set_id'];
                }
                if ( isset(
$theme['set_is_acp_default'] ) and $theme['set_is_acp_default'] )
                {
                    static::
$defaultAcpThemeSet = $theme['set_id'];
                }
               
                static::
$themeSetRelationships[ $theme['set_parent_id'] ][ $theme['set_id'] ] = $theme;
               
                static::
$multitons[ $theme['set_id'] ] = static::constructFromData( $theme );
            }
        }
        return static::
$multitons;
    }

   
/**
     * Returns only the visible themes for the member
     *
     * @return array|bool
     */
   
public static function getThemesWithAccessPermission()
    {
       
$visibleThemes = array();

        foreach ( static::
themes() AS $themeId => $theme )
        {
            if (
$theme->canAccess() )
            {
               
$visibleThemes[$theme->id] = $theme;
            }
        }

        return
$visibleThemes;
    }
   
   
/**
     * Fetch the master theme object
     *
     * @return    \IPS\Theme
     */
   
public static function master()
    {
        static::
themes();
       
$default = static::$multitons[ static::$defaultFrontendThemeSet ];
        return static::
constructFromData( array_merge( static::$themeSetRelationships[ $default->parent_id ][ $default->id ], array( 'set_id' => 0 ) ) );
    }

   
/**
     * Is designer's mode enabled?
     *
     * @return boolean
     */
   
public static function designersModeEnabled()
    {
        return (boolean) \
IPS\Settings::i()->theme_designers_mode;
    }

   
/**
     * Default Frontend Skin Set ID
     *
     * @return    int
     */
   
public static function defaultTheme()
    {
        if ( !static::
$gotAll )
        {
            static::
themes();
        }
   
        return static::
$defaultFrontendThemeSet;
    }
   
   
/**
     * Default ACP Skin Set ID
     *
     * @return    int
     */
   
public static function defaultAcpTheme()
    {
        if ( !static::
$gotAll )
        {
            static::
themes();
        }
   
        return static::
$defaultAcpThemeSet ?: static::$defaultFrontendThemeSet;
    }

   
/**
     * Switches the currently initialized theme during execution
     *
     * @param   int     $themeId        Id of the theme to switch to
     * @param    boolean    $persistent        Allow to persist through ajax calls
     * @return  boolean
     * @note    This will not check to ensure the member has permission to view the theme
     */
   
public static function switchTheme( $themeId, $persistent=TRUE )
    {
        static::
themes();

        try
        {
           
$class = get_class( static::$memberTheme );
            static::
$memberTheme = $class::load( $themeId );
           
           
/* Store the ID in sessions so ajax loads correct theme */
           
if ( $persistent )
            {
                \
IPS\Session\Front::i()->setTheme( $themeId );
            }
           
           
/* Flush loaded CSS */
           
\IPS\Output::i()->cssFiles = array();

            \
IPS\Dispatcher\Front::baseCss();

           
/* App CSS */
           
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( \IPS\Dispatcher\Front::i()->application->directory . '.css', \IPS\Dispatcher\Front::i()->application->directory, \IPS\Dispatcher\Front::i()->controllerLocation ) );
            if ( \
IPS\Theme::i()->settings['responsive'] )
            {
                \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( \IPS\Dispatcher\Front::i()->application->directory . '_responsive.css', \IPS\Dispatcher\Front::i()->application->directory, \IPS\Dispatcher\Front::i()->controllerLocation ) );
            }
        }
        catch( \
OutOfRangeException $e )
        {
            return
FALSE;
        }
    }
   
   
/**
     * @brief Prevent CSS from being loaded more than once
     */
   
protected static $usedCss = array();
   
   
/**
     * Get CSS
     * This method is used to return the built CSS stored in the file objects system
     *
     * @param    string        $file        Filename
     * @param    string|null    $app        Application
     * @param    string|null    $location    Location (e.g. 'admin', 'front')
     * @return    array        URLs to CSS files
     */
   
public function css( $file, $app=NULL, $location=NULL )
    {
       
$app      = $app      ?: \IPS\Request::i()->app;
       
$location = $location ?: \IPS\Dispatcher::i()->controllerLocation;
       
$paths    = explode( '/', $file );
       
$name     = array_pop( $paths );
       
$path     = ( count( $paths ) ) ? implode( '/', $paths ) : '.';
       
$cacheKey = $file . ',' . $app . ',' . $location . ',' . $this->_id;
       
        if ( isset( static::
$usedCss[ $cacheKey ] ) )
        {
            return static::
$usedCss[ $cacheKey ];
        }
       
        if (
$location === 'interface' )
        {
            static::
$usedCss[ $cacheKey ] = array( rtrim( \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ), '/' ) . "/applications/{$app}/interface/{$file}" );
            return static::
$usedCss[ $cacheKey ];
        }

       
$key = static::makeBuiltTemplateLookupHash( $app, $location, $path . '/' . $name );

        if (
in_array( $key, array_keys( $this->css_map ) ) )
        {
            if (
$this->css_map[ $key ] !== null )
            {
                static::
$usedCss[ $cacheKey ] = array( \IPS\File::get( 'core_Theme', $this->css_map[ $key ] )->url );
            }
            else
            {
                static::
$usedCss[ $cacheKey ] = array();
            }
           
            return static::
$usedCss[ $cacheKey ];
        }
        else
        {
           
/* We're setting up, do nothing to avoid compilation requests when tables are incomplete */
           
if ( ! isset( \IPS\Settings::i()->setup_in_progress ) OR \IPS\Settings::i()->setup_in_progress )
            {
               
/* Do not store a cache here as this will break later when we want CSS */
               
return array();
            }
           
           
/* Map doesn't exist, try and create it */
           
if ( $this->compileCss( $app, $location, $path, $name ) === NULL )
            {
               
/* Do not store a cache here as this will break later when we want CSS */
               
return array();
            }
           
           
/* Still not here? Then add a key but as null to prevent it from attempting to rebuild on every single page
             * load thus hitting the DB multiple times */
           
$cssMap = $this->css_map;
            if ( !
in_array( $key, array_keys( $this->css_map ) ) )
            {
               
$cssMap[ $key ] = null;
               
               
$this->css_map = $cssMap;
               
$this->save();
            }
            else
            {
                static::
$usedCss[ $cacheKey ] = array( \IPS\File::get( 'core_Theme', $this->css_map[ $key ] )->url );
                return static::
$usedCss[ $cacheKey ];
            }
        }
       
        return array();
    }

   
/**
     * Get Theme Resource (image, font, theme-specific JS, etc)
     *
     * @param    string        $path        Path to resource
     * @param    string|null    $app        Application key
     * @param    string|null    $location    Location
     * @return    \IPS\Http\Url|NULL        URL to resource
     */
   
public function resource( $path, $app=NULL, $location=NULL, $noProtocol=FALSE )
    {
       
$app      = $app      ?: \IPS\Request::i()->app;
       
$location = $location ?: \IPS\Dispatcher::i()->controllerLocation;
       
$paths    = explode( '/', $path );
       
$name     = array_pop( $paths );
       
$path     = ( count( $paths ) ) ? ( '/' . implode( '/', $paths ) . '/' ) : '/';
       
$key      = static::makeBuiltTemplateLookupHash($app, $location, $path) . '_' .$name;

        if (
$location === 'interface' )
        {
            return \
IPS\Http\Url::internal( "applications/{$app}/interface{$path}{$name}", 'interface', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE );
        }
               
        if (
in_array( $key, array_keys( $this->resource_map ) ) )
        {
            if (
$this->resource_map[ $key ] === NULL )
            {
                return
NULL;
            }
            else
            {
               
$url = \IPS\File::get( 'core_Theme', $this->resource_map[ $key ] )->url;

                if(
$noProtocol and $url instanceof \IPS\Http\Url )
                {
                   
$url = $url->setScheme(NULL);
                }

                return
$url;
            }
        }
       
       
/* Still here? Map doesn't exist, try and create it */
       
$resourceMap = $this->resource_map;
       
        try
        {
           
/* We're setting up, do nothing to avoid compilation requests when tables are incomplete */
           
if ( ! isset( \IPS\Settings::i()->setup_in_progress ) OR \IPS\Settings::i()->setup_in_progress )
            {
                return
NULL;
            }
           
           
$flagKey  = 'resource_compiling_' . $this->_id . '_' . md5( $key );
            if ( static::
checkLock( $flagKey ) )
            {
                return
NULL;
            }
   
            static::
lock( $flagKey );
       
           
$resource = \IPS\Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? AND resource_app=? AND resource_location=? AND resource_path=? AND resource_name=?', $this->id, $app, $location, $path, $name ) )->first();
       
           
$resourceMap[ $key ] = (string) \IPS\File::create( 'core_Theme', $key, $resource['resource_data'], 'set_resources_' . $this->id, FALSE, NULL, FALSE );
           
            \
IPS\Db::i()->update( 'core_theme_resources', array( 'resource_added' => time(), 'resource_filename' => $resourceMap[ $key ] ), array( 'resource_id=?', $resource['resource_id'] ) );
           
           
/* Save map */
           
$this->resource_map = $resourceMap;
           
$this->save();
           
            static::
unlock( $flagKey );

           
$url = \IPS\File::get( 'core_Theme', $resourceMap[ $key ] )->url;

            if(
$noProtocol and $url instanceof \IPS\Http\Url )
            {
               
$url = $url->setScheme(NULL);
            }

            return
$url;
        }
        catch( \
UnderflowException $e )
        {
           
/* Doesn't exist, add null entry to map to prevent it from being rebuilt on each page load */
           
$resourceMap[ $key ] = null;
           
           
/* Save map */
           
$this->resource_map = $resourceMap;
           
$this->save();
           
            return
NULL;
        }
    }
   
   
/**
     * Get a template
     *
     * @param    string    $group                Template Group
     * @param    string    $app                Application key (NULL for current application)
     * @param    string    $location            Template Location (NULL for current template location)
     * @return    \IPS\Output\Template
     * @throws    \UnexpectedValueException
     */
   
public function getTemplate( $group, $app=NULL, $location=NULL )
    {
       
/* Do we have an application? */
       
if( $app === NULL )
        {
           
$app = \IPS\Dispatcher::i()->application->directory;
        }
           
       
/* How about a template location? */
       
if( $location === NULL )
        {
           
$location = \IPS\Dispatcher::i()->controllerLocation;
        }

       
$key = \strtolower( 'template_' . $this->id . '_' . static::makeBuiltTemplateLookupHash( $app, $location, $group ) . '_' . static::cleanGroupName( $group ) );
       
$cachedObject = NULL;
       
       
/* We cannot use isset( static::$calledTemplates[ $key ] ) here because it fails with NULL while in_array does not */
       
if ( !in_array( $key, array_keys( static::$calledTemplates ) ) )
        {
           
/* First, are we on the front end and using a disk cache? */
           
if ( static::isUsingTemplateDiskCache() and $location != 'admin' )
            {
               
$cache = new \IPS\Theme\Cache\Template( $app, $location, $group );

                if (
$cache->exists() )
                {
                   
$cachedObject = $cache->get();
                }
                else
                {
                   
/* If it exists in datastore, use that to write the value */
                   
if ( isset( \IPS\Data\Store::i()->$key ) )
                    {
                        try
                        {
                           
$cache->set( str_replace( 'namespace IPS\Theme;', 'namespace IPS\Theme\Cache;', \IPS\Data\Store::i()->$key ) );
                        }
                        catch( \
RuntimeException $e )
                        {
                           
/* Should we log, ignore or collect failures to notify admin? */
                       
}
                    }
                    else
                    {
                       
/* We'll compile it now, but let datastore serve the template this time round */
                       
$this->compileTemplates( $app, $location, $group );
                    }
                }
            }
           
           
/* If we don't have a compiled template, do that now */
           
if ( ! $cachedObject and !isset( \IPS\Data\Store::i()->$key ) )
            {
               
/* It can take a few seconds for templates to finish compiling if initiated elsewhere, so let's try a few times sleeping 1 second between attempts
                   to give the compilation time to finish */
               
$attempts = 0;
                while(
$attempts < 6 )
                {
                    if (
$attempts === 5 )
                    {
                       
/* Rebuild in progress */
                       
\IPS\Log::log( "Template store key: {$key} rebuilding and requested again ({$app}, {$location}, {$group})", "template_store_building" );
   
                       
/* Since we can't do anything else, this ends up just being an uncaught exception - show the error page right away to avoid the unnecessary logging */
                       
\IPS\IPS::genericExceptionPage();
                    }
                   
                   
$built = $this->compileTemplates( $app, $location, $group );
                   
                    if (
$built === NULL )
                    {
                       
$attempts++;
                       
sleep(1);
                    }
                    else
                    {
                        break;
                    }
                }
               
               
/* Still no key? */
               
if ( ! isset( \IPS\Data\Store::i()->$key ) )
                {
                    \
IPS\Log::log( "Template store key: {$key} missing ({$app}, {$location}, {$group})", "template_store_missing" );
   
                    throw new \
ErrorException( 'template_store_missing' );
                }
            }

           
/* Load compiled template */
           
if ( $cachedObject )
            {
               
/* Init */
               
static::$calledTemplates[ $key ] = new \IPS\Theme\SandboxedTemplate( $cachedObject );
            }
            else
            {
               
$compiledGroup = \IPS\Data\Store::i()->$key;
               
                if ( \
IPS\DEBUG_TEMPLATES )
                {
                    static::
runDebugTemplate( $key, $compiledGroup );
                }
                else
                {
                    try
                    {
                        if ( @eval(
$compiledGroup ) === FALSE )
                        {
                            throw new \
IPS\Theme\TemplateException( 'Invalid Template', 1000, NULL, array( 'group' => $group, 'app' => $app, 'location' => $location ), $this );
                        }
                    }
                    catch ( \
ParseError $e )
                    {
                        throw new \
UnexpectedValueException;
                    }
                }
               
               
/* Hooks */
               
$class = "\\IPS\\Theme\\" . static::overloadHooks( 'class_' . $app . '_' . $location . '_' . $group );

               
/* Init */
               
static::$calledTemplates[ $key ] = new \IPS\Theme\SandboxedTemplate( new $class( $app, $location, $group ) );
            }
        }

        return static::
$calledTemplates[ $key ];
    }
   
   
/**
     * Eval hooks into life
     *
     * @param    string    $class     Class name
     * @param    string    $namespace    Namespace for the templates
     * @return    Modified classname
     */
   
public static function overloadHooks( $class, $namespace="IPS\\Theme" )
    {
        if ( isset( \
IPS\IPS::$hooks[ "\\IPS\\Theme\\{$class}" ] ) AND \IPS\RECOVERY_MODE === FALSE )
        {
            foreach ( \
IPS\IPS::$hooks[ "\\IPS\\Theme\\{$class}" ] as $id => $data )
            {
                if (
file_exists( \IPS\ROOT_PATH . '/' . $data['file'] ) )
                {
                    if (
class_exists( "{$namespace}\\" . $data['class'], FALSE ) )
                    {
                       
$class = $data['class'];
                        continue;
                    }

                   
$contents = "namespace {$namespace};\n\n" . str_replace( '_HOOK_CLASS_', $class, file_get_contents( \IPS\ROOT_PATH . '/' . $data['file'] ) );

                    try
                    {
                        if( eval(
$contents ) !== FALSE )
                        {
                           
$class = $data['class'];
                        }
                    }
                    catch ( \
ParseError $e ) { }
                }
            }
        }
       
        return
$class;
    }
   
   
/*! Active Record */
   
    /**
     * Construct ActiveRecord from database row
     *
     * @param    array    $data                            Row from database table
     * @param    bool    $updateMultitonStoreIfExists    Replace current object in multiton store if it already exists there?
     * @return    static
     */
   
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
    {
       
$obj = parent::constructFromData( $data, $updateMultitonStoreIfExists );

       
/* Extra set up */
       
$logo = json_decode( $obj->logo_data, true );
       
       
$obj->_data['logo'] = array( 'front' => null, 'sharer' => null, 'favicon' => null );
       
        if (
is_array( $logo ) )
        {
            foreach( array(
'front', 'sharer', 'favicon' ) as $type )
            {
                if ( isset(
$logo[ $type ] ) )
                {
                   
$obj->_data['logo'][ $type ] = $logo[ $type ];
                }
            }
        }

        if (
$settings = json_decode( $obj->_data['template_settings'], true ) )
        {
           
$obj->_data['settings'] = $settings;
        }
        else
        {
           
/* No settings here */
           
$obj->_data['settings'] = array();
        }
       
        if ( !
is_array( $obj->_data['resource_map'] ) )
        {
            if (
$imgMap = json_decode( $obj->_data['resource_map'], true ) )
            {
               
$obj->_data['resource_map'] = $imgMap;
            }
            else
            {
               
$obj->_data['resource_map'] = array();
            }
        }
       
        if ( !
is_array( $obj->_data['css_map'] ) )
        {
            if (
$cssMap = json_decode( $obj->_data['css_map'], true ) )
            {
               
$obj->_data['css_map'] = $cssMap;
            }
            else
            {
               
$obj->_data['css_map'] = array();
            }
        }

        return
$obj;
    }
   
   
/**
     * Save resource map
     *
     * @param    $value        array    Value to save
     */
   
public function set_resource_map( $value )
    {
        if (
is_array( $value ) )
        {
           
$this->_data['resource_map'] = json_encode( $value );
        }
    }
   
   
/**
     * Save CSS map
     *
     * @param    $value        array    Value to save
     */
   
public function set_css_map( $value )
    {
        if (
is_array( $value ) )
        {
           
$this->_data['css_map'] = json_encode( $value );
        }
    }
   
   
/**
     * Save Changed Columns
     *
     * @return    void
     */
   
function save()
    {
        if ( !
$this->editor_skin )
        {
           
$this->editor_skin = 'ips';
        }
       
       
/* Re-calculate customised flag */
       
$this->customized = $this->isCustomized();
       
       
parent::save();
        unset( \
IPS\Data\Store::i()->themes );
       
       
/* Reset map arrays */
       
if ( !isset( $this->_data['resource_map'] ) OR  ! is_array( $this->_data['resource_map'] ) )
        {
            if ( isset(
$this->_data['resource_map'] ) AND $imgMap = json_decode( $this->_data['resource_map'], true ) )
            {
               
$this->_data['resource_map'] = $imgMap;
            }
            else
            {
               
$this->_data['resource_map'] = array();
            }
        }
       
        if ( !isset(
$this->_data['css_map'] ) OR ! is_array( $this->_data['css_map'] ) )
        {
            if ( isset(
$this->_data['css_map'] ) AND $cssMap = json_decode( $this->_data['css_map'], true ) )
            {
               
$this->_data['css_map'] = $cssMap;
            }
            else
            {
               
$this->_data['css_map'] = array();
            }
        }
    }
   
   
/*! Node */

    /**
     * Get sharer logo
     *
     * @return    \IPS\Http\Url|string
     */
   
public function get_logo_sharer()
    {        
       
$url = $this->logoImage( 'sharer' );

        if(
$url !== '' AND !$url->data[ \IPS\Http\Url::COMPONENT_SCHEME ] )
        {
           
$baseUrl = \IPS\Http\Url::createFromString( \IPS\Settings::i()->base_url );

            return
$url->setScheme( $baseUrl->data[ \IPS\Http\Url::COMPONENT_SCHEME ] );
        }
        else
        {
            return
$url;
        }
    }

   
/**
     * Get header logo
     *
     * @return    \IPS\Http\Url|string
     */
   
public function get_logo_front()
    {        
        return
$this->logoImage( 'front' );
    }

   
/**
     * Get favicon logo
     *
     * @return    \IPS\Http\Url|string
     */
   
public function get_logo_favicon()
    {        
        return
$this->logoImage( 'favicon' );
    }

   
/**
     * Return logo image
     *
     * @param    string    $type    Type of logo image
     * @return    \IPS\Http\Url|string
     */
   
protected function logoImage( $type )
    {
        if( isset( \
IPS\Theme::i()->logo[ $type ]['url'] ) )
        {
            try
            {
                return \
IPS\File::get( 'core_Theme', \IPS\Theme::i()->logo[ $type ]['url'] )->url;
            }
            catch( \
Exception $e )
            {
                return
'';
            }    
        }

        return
'';
    }

   
/**
     * [Node] Get Description
     *
     * @return    string|null
     */
   
protected function get__description()
    {        
        return \
IPS\Theme::i()->getTemplate( 'customization', 'core' )->themeDescription( $this );
    }

   
/**
     * [Node] Return the custom badge for each row
     *
     * @return    NULL|array        Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
     */
   
protected function get__badge()
    {
       
/* Is there an update to show? */
       
$badge    = NULL;

        if ( static::
designersModeEnabled() )
        {
            if (
$this->is_default or $this->is_acp_default )
            {
                if (
$this->is_default and $this->is_acp_default )
                {
                   
$message = 'theme_is_default_with_id';
                }
                elseif (
$this->is_default )
                {
                   
$message = 'theme_is_front_default_with_id';
                }
                else
                {
                   
$message = 'theme_is_acp_default_with_id';
                }
               
               
$badge    = array(
                   
0    => 'positive ipsPos_right',
                   
1    => \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => array( $this->id ) ) )
                );
            }
            else
            {
               
$badge = array(
                   
0 => 'style7 ipsPos_right',
                   
1 => \IPS\Member::loggedIn()->language()->addToStack('theme_with_id', FALSE, array( 'sprintf' => array( $this->id ) ) )
                );
            }
        }
        else
        {
            if (
$this->is_default or $this->is_acp_default )
            {
                if (
$this->is_default and $this->is_acp_default )
                {
                   
$message = 'default_no_parenthesis';
                }
                elseif (
$this->is_default )
                {
                   
$message = 'default_front_no_parenthesis';
                }
                else
                {
                   
$message = 'default_acp_no_parenthesis';
                }

               
$badge    = array(
                   
0    => 'positive ipsPos_right',
                   
1    => $message
               
);
            }

            if (
$this->update_data )
            {
               
$data    = json_decode( $this->update_data, TRUE );

                if( !empty(
$data['longversion']) AND $data['longversion'] > $this->long_version )
                {
                   
$released    = NULL;

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

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

        return
$badge;
    }
   
   
/**
     * [Node] Get Icon for tree
     *
     * @note    Return the class for the icon (e.g. 'fa-globe')
     * @return    string|null
     */
   
protected function get__icon()
    {
        return (
$this->by_skin_gen ) ? 'magic' : '';
    }

   
/**
     * [Node] Clone the theme set
     *
     * @return    void
     */
   
public function __clone()
    {
        if(
$this->skipCloneDuplication === TRUE )
        {
            return;
        }
       
       
$title      = \IPS\Member::loggedIn()->language()->get( static::$titleLangPrefix . $this->_id );
       
$originalId = $this->get__id();
       
       
/* Unset custom properties */
       
foreach( array( 'settings', 'resource_map', 'css_map', 'logo', 'name_translated', 'title' ) as $f )
        {
            unset(
$this->_data[ $f ] );
        }
       
$this->is_default = FALSE;
       
$this->is_acp_default = FALSE;
       
       
parent::__clone();
       
       
/* Dynamically produce insert list so we don't have to update each time the table changes */
       
$templateTable  = \IPS\Db::i()->getTableDefinition( 'core_theme_templates', TRUE );
       
$cssTable       = \IPS\Db::i()->getTableDefinition( 'core_theme_css', TRUE );
       
$templateFields = array_keys( $templateTable['columns'] );
       
$cssFields      = array_keys( $cssTable['columns'] );
       
       
array_walk( $templateFields, function( &$name, $i, $setId )
        {
            switch(
$name )
            {
                case
'template_id':
                   
$name = 'null';
                    break;
                case
'template_set_id':
                   
$name = $setId;
                    break;
            }
        },
$this->id );
       
       
array_walk( $cssFields, function( &$name, $i, $setId )
        {
            switch(
$name )
            {
                case
'css_id':
                   
$name = 'null';
                    break;
                case
'css_set_id':
                   
$name = $setId;
                    break;
            }
        },
$this->id );
       
       
/* Insert new language bit */
       
\IPS\Lang::saveCustom( 'core', "core_theme_set_title_" . $this->id, sprintf( \IPS\Member::loggedIn()->language()->get( 'theme_clone_copy_of' ), $title ) );
       
       
/* Copy across any template bits */
       
\IPS\Db::i()->insert( 'core_theme_templates', \IPS\Db::i()->select( implode(',', $templateFields ), 'core_theme_templates', array( 'template_set_id=?', $originalId ) ) );
       
       
/* Copy across any CSS bits */
       
\IPS\Db::i()->insert( 'core_theme_css', \IPS\Db::i()->select( implode(',', $cssFields ), 'core_theme_css', array( 'css_set_id=?', $originalId ) ) );
       
       
/* Copy across any settings */
       
$settingFields = array();

        foreach( \
IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $originalId ) ) as $row )
        {
           
$settingFields[ $row['sc_id'] ] = $row;
        }

       
$settingValues = iterator_to_array( \IPS\Db::i()->select( 'sv_id, sv_value', 'core_theme_settings_values', array( \IPS\Db::i()->in( 'sv_id', array_keys( $settingFields ) ) ) )->setKeyField('sv_id')->setValueField('sv_value') );

        foreach(
$settingFields as $id => $row )
        {
           
$origId = $row['sc_id'];
            unset(
$row['sc_id'] );

           
$row['sc_set_id'] = $this->id;

           
$newId = \IPS\Db::i()->insert( 'core_theme_settings_fields', $row );

            \
IPS\Db::i()->insert( 'core_theme_settings_values', array(
               
'sv_id'    => $newId,
               
'sv_value' => ( array_key_exists( $origId, $settingValues ) ) ? $settingValues[ $origId ] : $row['sc_default']
            ) );
        }

       
/* Copy any language keys */
       
foreach( \IPS\Db::i()->select( '*', 'core_sys_lang_words', array( 'word_theme=?', $originalId ) ) AS $word )
        {
           
$word['word_theme'] = $this->id;
            unset(
$word['word_id'] );
            \
IPS\Db::i()->insert( 'core_sys_lang_words', $word );
        }

       
/* Copy across resources */
       
$this->copyResourcesFromSet( $originalId );

       
/* Make sure data objects are loaded correctly */
       
static::$gotAll = false;

       
/* Save css/img maps */
       
\IPS\Theme::load( $this->id )->saveSet();
       
       
/* Copy logos */
       
$this->logo_data = NULL;
       
$this->copyLogosFromSet( $originalId );
       
        \
IPS\Session::i()->log( 'acplogs__themeset_created', array( sprintf( \IPS\Member::loggedIn()->language()->get( 'theme_clone_copy_of' ), \IPS\Member::loggedIn()->language()->get( 'core_theme_set_title_' . $originalId ) ) => FALSE ) );
    }

   
/**
     * [Node] Does the currently logged in user have permission to delete this node?
     *
     * @return    bool
     */
   
public function canDelete()
    {
        if ( \
IPS\IN_DEV AND $this->_id == \IPS\DEFAULT_THEME_ID )
        {
            return
FALSE;
        }

        if(
$this->is_default OR $this->is_acp_default )
        {
            return
FALSE;
        }

        foreach(
$this->children( NULL ) as $childTheme )
        {
            if(
$childTheme->is_default OR $childTheme->is_acp_default )
            {
                return
FALSE;
            }
        }

        return
parent::canDelete();
    }
   
   
/**
     * [Node] Delete the theme set
     *
     * @return    void
     */
   
public function delete()
    {
        if ( \
IPS\IN_DEV AND $this->_id == \IPS\DEFAULT_THEME_ID )
        {
            \
IPS\Output::i()->error( 'theme_error_not_available_in_dev', '2S140/1', 403, '' );
        }
       
        if (
$this->is_default or $this->is_acp_default )
        {
            \
IPS\Output::i()->error( 'core_theme_cannot_delete_default_theme', '2S162/1', 403, '' );
        }
       
       
/* Clear out existing built bits */
       
\IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $this->_id );
        \
IPS\File::getClass('core_Theme')->deleteContainer( 'set_resources_' . $this->_id );
       
       
$templates = $this->getRawTemplates();
           
        foreach(
$templates as $app => $v )
        {
            foreach(
$templates[ $app ] as $location => $groups )
            {
                foreach(
$templates[ $app ][ $location ] as $group => $bits )
                {
                    foreach(
$templates[ $app ][ $location ][ $group ] as $name => $data )
                    {
                       
/* Store it */
                       
$key = \strtolower( 'template_' . $this->_id . '_' .static::makeBuiltTemplateLookupHash( $app, $location, $group ) . '_' . static::cleanGroupName( $group ) );
                           
                        unset( \
IPS\Data\Store::i()->$key );
                    }
                }
            }
        }
       
        \
IPS\Db::i()->delete( 'core_theme_resources', array( 'resource_set_id=?', $this->_id ) );
        \
IPS\Db::i()->delete( 'core_theme_css', array( 'css_set_id=?', $this->_id ) );
        \
IPS\Db::i()->delete( 'core_theme_templates', array( 'template_set_id=?', $this->_id ) );
        \
IPS\Db::i()->delete( 'core_sys_lang_words', array( 'word_theme=?', $this->_id ) );

       
/* Delete theme settings */
       
$settingFields = array();
        foreach( \
IPS\Db::i()->select( '*', 'core_theme_settings_fields', array( 'sc_set_id=?', $this->_id ) ) as $row )
        {
           
$settingFields[ $row['sc_id'] ] = $row;
        }

        if (
count( $settingFields ) )
        {
            \
IPS\Db::i()->delete( 'core_theme_settings_values', array( \IPS\Db::i()->in( 'sv_id', array_keys( $settingFields ) ) ) );
        }

       
/** reset member skin  */
       
\IPS\Db::i()->update( 'core_members', array( 'skin' => 0 ), array('skin=?', $this->_id ) );

        \
IPS\Db::i()->delete( 'core_theme_settings_fields', array( 'sc_set_id=?', $this->_id ) );
   
       
parent::delete();
        unset( \
IPS\Data\Store::i()->themes );
    }
   
   
/**
     * [Node] Add/Edit Form
     *
     * @param    \IPS\Helpers\Form    $form    The form
     * @return    void
     */
   
public function form( &$form )
    {
       
/* Check permission */
       
\IPS\Dispatcher::i()->checkAcpPermission( 'theme_sets_manage' );
       
        \
IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'customization/themes.css', 'core', 'admin' ) );
        \
IPS\Output::i()->jsFiles  = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_customization.js', 'core', 'admin' ) );

       
/* General */
       
if ( $this->id )
        {
            \
IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('core_theme_editing_set', FALSE, array( 'sprintf' => array( $this->_title ) ) );
           
           
$form->addTab( 'theme_set_tab__general' );
        }
        else
        {
            \
IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('theme_set_add_button');
           
           
$form->hiddenValues['theme_type'] = \IPS\Request::i()->type;
           
$form->addTab( ( \IPS\Request::i()->type === 'vse' ) ? 'theme_set_tab__new_vse_set' : 'theme_set_tab__new_custom_set' );
        }
       
       
$form->add( new \IPS\Helpers\Form\Translatable( 'core_theme_set_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_theme_set_title_{$this->id}" : NULL ) ) ) );

       
$class = get_called_class();

       
$form->add( new \IPS\Helpers\Form\Node( 'core_theme_parent_id', intval( $this->parent_id ), FALSE, array(
           
'class'          => '\IPS\Theme',
           
'subnodes'        => FALSE,
           
'zeroVal'         => 'core_theme_parent_id_none',
           
'permissionCheck' => function( $node ) use ( $class )
            {
                if( isset(
$class::$subnodeClass ) AND $class::$subnodeClass AND $node instanceof $class::$subnodeClass )
                {
                    return
FALSE;
                }

                return !isset( \
IPS\Request::i()->id ) or ( $node->id != \IPS\Request::i()->id and !$node->isChildOf( $node::load( \IPS\Request::i()->id ) ) );
            } ) ) );
       
       
$id = $this->id;
       
$form->add( new \IPS\Helpers\Form\YesNo( 'core_theme_set_is_default' , $this->is_default, false, array( 'togglesOff'  => array('core_theme_set_permissions') ), function( $val ) use ( $id )
        {
           
$where = array( array( 'set_is_default=1' ) );
            if (
$id )
            {
               
$where[] = array('set_id<>?', $id );
            }
           
            if ( !
$val and !\IPS\Db::i()->select( 'COUNT(*)', 'core_themes', $where )->first() )
            {
                throw new \
DomainException('core_theme_set_is_default_error');
            }
        } ) );
       
       
$form->add( new \IPS\Helpers\Form\Select(
               
'core_theme_set_permissions',
                (
$this->id ) ? ( $this->permissions === '*' ? '*' : explode( ",", $this->permissions ) ) : '*',
               
FALSE,
                array(
'options' => \IPS\Member\Group::groups(), 'multiple' => TRUE, 'parse' => 'normal', 'unlimited' => '*', 'unlimitedLang' => 'all' ),
               
NULL,
               
NULL,
               
NULL,
               
'core_theme_set_permissions'
       
) );
       
       
$form->add( new \IPS\Helpers\Form\YesNo( 'core_theme_set_is_acp_default' , $this->is_acp_default, false, array(), function( $val ) use ( $id )
        {
           
$where = array( array( 'set_is_acp_default=1' ) );
            if (
$id )
            {
               
$where[] = array('set_id<>?', $id );
            }
           
            if ( !
$val and !\IPS\Db::i()->select( 'COUNT(*)', 'core_themes', $where )->first() )
            {
                throw new \
DomainException('core_theme_set_is_default_error');
            }
        } ) );
               
        if ( \
IPS\IN_DEV OR \IPS\Theme::designersModeEnabled() )
        {
           
$form->add( new \IPS\Helpers\Form\Text( 'theme_template_export_author_name', $this->author_name, false ) );
           
$form->add( new \IPS\Helpers\Form\Text( 'theme_template_export_author_url' , $this->author_url, false ) );
           
$form->add( new \IPS\Helpers\Form\Text( 'theme_update_check' , $this->update_check, false ) );
               
           
$form->add( new \IPS\Helpers\Form\Text( 'theme_template_export_version'        , $this->version ? $this->version : '1.0'    , true, array( 'placeholder' => '1.0.0' ) ) );
           
$form->add( new \IPS\Helpers\Form\Number( 'theme_template_export_long_version' , $this->long_version ? $this->long_version : 10000, true ) );
        }
       
       
/* Logo */
       
$form->addTab( 'theme_set_tab__logo' );
       
       
/* SITE LOGO */
       
$form->addHeader( 'core_theme_set_logo_manage' );
       
       
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_set_logo', ( isset( $this->logo['front']['url'] ) ? \IPS\File::get( 'core_Theme', $this->logo['front']['url'] ) : NULL ), FALSE, array( 'image' => true, 'storageExtension' => 'core_Theme' ), NULL, NULL, NULL, 'core_theme_set_logo' ) );

       
/* SHARER LOGO */
       
$form->addHeader( 'core_theme_set_sharer_logo_manage' );
           
       
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_set_sharer_logo', ( isset( $this->logo['sharer']['url'] ) ? \IPS\File::get( 'core_Theme', $this->logo['sharer']['url'] ) : NULL ), FALSE, array( 'image' => true, 'storageExtension' => 'core_Theme' ), function( $value )
        {
            if( !
$value )
            {
                return
TRUE;
            }
           
           
$image = \IPS\Image::create( $value->contents() );

            if(
$image->width < 200 or $image->height < 200 )
            {
                throw new \
DomainException( 'core_theme_set_sharer_logo_too_small' );
            }
        },
NULL, NULL, 'core_theme_set_sharer_logo' ) );
           
       
/* FAVICO LOGO */
       
$form->addHeader( 'core_theme_set_favico_logo_manage' );
       
       
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_set_favico_logo', ( isset( $this->logo['favicon']['url'] ) ? \IPS\File::get( 'core_Theme', $this->logo['favicon']['url'] ) : NULL ), FALSE, array( 'allowedFileTypes' => array('ico'), 'storageExtension' => 'core_Theme' ), NULL, NULL, NULL, 'core_theme_set_favico_logo' ) );
       
       
/* EDITOR */
       
$form->addTab('core_theme_set_editor_header');
        if ( \
IPS\IN_DEV )
        {
           
$form->addMessage( \IPS\Member::loggedIn()->language()->get('core_theme_set_editor_dev'), 'ipsMessage ipsMessage_warning' );
        }
       
$form->addMessage( \IPS\Member::loggedIn()->language()->addToStack( 'core_theme_set_editor_blurb', FALSE, array( 'sprintf' => array( \IPS\Helpers\Form\Editor::ckeditorVersion() ) ) ) );
        if ( !\
IPS\NO_WRITES and class_exists( 'ZipArchive', FALSE ) )
        {
           
$form->add( new \IPS\Helpers\Form\Radio( 'core_theme_set_editor', 'existing', FALSE, array(
               
'options' => array( 'existing' => 'core_theme_set_editor_existing_sel', 'new' => 'core_theme_set_editor_new_sel' ),
               
'toggles' => array( 'existing' => array( 'core_theme_set_editor_existing' ), 'new' => array( 'core_theme_set_editor_new' ) )
            ) ) );
        }
        else
        {
            \
IPS\Member::loggedIn()->language()->words['core_theme_set_editor_existing_desc'] = \IPS\Member::loggedIn()->language()->get('core_theme_set_editor_existing_instr');
        }
       
$skins = array();
        if (
is_dir( \IPS\ROOT_PATH . '/applications/core/interface/ckeditor/ckeditor/skins' ) )
        {
            foreach ( new \
DirectoryIterator( \IPS\ROOT_PATH . '/applications/core/interface/ckeditor/ckeditor/skins' ) as $f )
            {
                if ( !
$f->isDot() and $f->isDir() )
                {
                   
$_name                = (string) $f;

                    if( \
IPS\Member::loggedIn()->language()->checkKeyExists( 'ckeditor_theme_' . $_name ) )
                    {
                       
$skins[ $_name ]    = \IPS\Member::loggedIn()->language()->addToStack( 'ckeditor_theme_' . $_name );
                    }
                    else
                    {
                       
$skins[ $_name ]    = $_name;
                    }
                }
            }
        }
       
$form->add( new \IPS\Helpers\Form\Select( 'core_theme_set_editor_existing', $this->id ? $this->editor_skin : 'ips', FALSE, array( 'options' => $skins ), NULL, NULL, NULL, 'core_theme_set_editor_existing' ) );
       
        if ( !\
IPS\NO_WRITES and class_exists( 'ZipArchive', FALSE ) )
        {
            if ( !
is_writable( \IPS\ROOT_PATH . '/applications/core/interface/ckeditor/ckeditor/skins' ) )
            {
               
$form->add( new \IPS\Helpers\Form\Custom( 'core_theme_set_editor_new', NULL, TRUE, array(
                   
'getHtml' => function()
                    {
                        return \
IPS\Theme::i()->getTemplate( 'global' )->message( 'editor_skin_nowrite', 'error' );
                    },
                   
'getValue' => function()
                    {
                        return
NULL;
                    }
                ),
NULL, NULL, NULL, 'core_theme_set_editor_new' ) );
            }
            else
            {
               
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_set_editor_new', NULL, FALSE, array( 'allowedFileTypes' => array( 'zip' ), 'temporary' => TRUE ), NULL, NULL, NULL, 'core_theme_set_editor_new' ) );
            }
        }
       
       
/* THEME SETTINGS */
       
if ( $this->id )
        {
            if ( static::
designersModeEnabled() )
            {
                \
IPS\Theme\Advanced\Theme::loadLanguage( $this->id );
            }
           
           
$customTabs = array();
            foreach( \
IPS\Db::i()->select( 'sc.*, sv.sv_value', array( 'core_theme_settings_fields', 'sc' ), array( 'sc.sc_set_id=? AND app_enabled=1', $this->id ), 'sc_order' )
                ->
join( array( 'core_theme_settings_values', 'sv' ), 'sv.sv_id=sc.sc_id' )
                ->
join( array( 'core_applications', 'a' ), 'sc.sc_app=a.app_directory' )
            as
$data ) {
               
$customTabs[ $data['sc_tab_key'] ][] = $data;
            }
           
           
ksort( $customTabs );
           
            foreach(
$customTabs as $tabKey => $data )
            {
               
$form->addTab( 'theme_custom_tab_' . $tabKey );
               
                if ( \
IPS\IN_DEV AND $this->_id === \IPS\DEFAULT_THEME_ID )
                {
                   
$form->addHeader( 'core_theme_default_dev_reminder' );
                }
               
                foreach(
$data as $row )
                {
                    if (
$field = $this->getCustomSettingField( $row ) )
                    {
                       
$form->add( $field );
                    }
                }
            }
           
           
$form->canSaveAndReload = true;
        }
    }
   
   
/**
     * Custom Settings
     *
     * @param    array    $row    Row from core_theme_settings_fields
     * @return    \IPS\Helpers\Form\FormAbstract
     */
   
public function getCustomSettingField( $row )
    {
        if (
$row['sc_condition'] )
        {
            if ( !@eval(
$row['sc_condition'] ) )
            {
                return
NULL;
            }
        }

       
$row['sc_type'] = ( empty( $row['sc_type'] ) ) ? 'Text' : $row['sc_type'];

       
/* What's the value */
       
$value = ( isset( $row['sv_value'] ) ) ? $row['sv_value'] : $row['sc_default'];
        if(
$row['sc_multiple'] and in_array( $row['sc_type'], array( 'Select', 'Radio' ) ) )
        {
           
$value = explode( ",", $value );
        }
       
        if (
$row['sc_type'] === 'other' )
        {
           
$theme = $this;
           
$field = eval( $row['sc_content'] );
        }
        else
        {                    
           
$class = '\IPS\Helpers\Form\\' . $row['sc_type'];

           
$options = array();
            switch (
$row['sc_type'] )
            {
                case
'Select':
                   
$options['multiple'] = $row['sc_multiple'];
                   
// No break
               
case 'Radio':
                   
$content = json_decode( $row['sc_content'], true );
                   
$values  = array();
                   
                    foreach(
$content as $data )
                    {
                        if ( isset(
$data['key'] ) )
                        {
                           
$values[ $data['key'] ] = $data['value'];
                        }
                        else
                        {
                           
$values[] = $data;
                        }
                    }
                   
                   
$options['options'] = $values;
                break;

                case
'Editor':
                   
$options['app']            = 'core';
                   
$options['key']            = 'Admin';
                   
$options['autoSaveKey']    = 'theme_custom_autosave_' . $row['sc_id'];
                   
$options['attachIds']   = array( $this->id, $row['sc_id'] );
                break;

                case
'TextArea':
                   
$options['rows'] = 8;
                break;
                case
'Upload':
                   
$options['storageExtension'] = 'core_Theme';
                   
                    if (
$value )
                    {
                        try
                        {
                           
$value = \IPS\File::get( $options['storageExtension'], $value );
                        }
                        catch ( \
OutOfRangeException $e )
                        {
                           
$value = NULL;
                        }
                    }
                break;
            }

           
$suffix = ( $row['sc_type'] == 'Color' and isset( $row['sv_value'] ) and ( mb_strtolower( $row['sc_default'] ) != mb_strtolower( $row['sv_value'] ) ) ) ? \IPS\Theme::i()->getTemplate( 'customization', 'core', 'admin' )->themeSettingRevert( $this->id, $row ) : NULL;
           
           
$field = new $class( "core_theme_setting_title_{$row['sc_id']}", $value, false, $options, NULL, NULL, $suffix, 'theme_setting_' . $row['sc_key'] );
        }
       
       
$desc = $row['sc_title'] . '_desc';
       
$field->description = \IPS\Member::loggedIn()->language()->addToStack( $desc, FALSE, array( 'returnBlank' => TRUE, 'returnInto' => \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->rowDesc( $desc, $field ) ) );

        if ( \
IPS\IN_DEV OR \IPS\Theme::designersModeEnabled() )
        {
           
$field->label = \IPS\Theme::i()->getTemplate( 'customization', 'core', 'admin' )->themeSettingLabelWithKey( $row );
        }
        else
        {
           
$field->label = \IPS\Member::loggedIn()->language()->addToStack( $row['sc_title'] );
        }
       
        return
$field;
    }
   
   
   
/**
     * [Node] Get buttons to display in tree
     * Example code explains return value
     *
     * @param    string    $url        Base URL
     * @param    bool    $subnode    Is this a subnode?
     * @return    array
     */
   
public function getButtons( $url, $subnode=FALSE )
    {
       
$parentButtons = array();
       
$buttons       = array();

        foreach(
parent::getButtons( $url, $subnode ) as $button )
        {
           
$parentButtons[ $button['title'] ] = $button;
        }

        unset(
$parentButtons['edit']['data']['ipsDialog'], $parentButtons['edit']['data']['ipsDialog-title'] );
       
        if (
$this->by_skin_gen AND \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'customization', 'theme_easy_editor' ) )
        {
           
$buttons[] = array(
                   
'icon'       => 'magic',
                   
'title'    => 'core_theme_launch_vse_tooltip',
                   
'link'       => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=launchvse&id={$this->_id}" ),
                   
'target'   => '_blank'
           
);
           
           
$buttons['edit'] = $parentButtons['edit'];
        }
       
       
/* Add in our buttons */
       
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'customization', 'theme_templates_manage' ) )
        {
           
$buttons['edit_templates'] = array(
               
'icon'    => 'code',
               
'title'    => 'theme_set_manage_templates_css',
               
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=templates&id={$this->_id}" )
            );
        }
       
        if ( !
$this->by_skin_gen )
        {
           
$buttons['edit'] = $parentButtons['edit'];
        }

       
$buttons['resources'] = array(
           
'icon'    => 'file-image-o',
           
'title'    => 'theme_set_manage_resources',
           
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=resources&set_id={$this->_id}" ),
           
'data'    => array()
        );

       
$buttons['copy'] = $parentButtons['copy'];
       
        if ( \
IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'customization', 'theme_download_upload' ) )
        {
           
$buttons['upload'] = array(
                   
'icon'    => 'upload',
                   
'title'    => 'theme_set_import',
                   
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=importForm&id={$this->_id}" ),
                   
'data'    => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('theme_set_import_title', FALSE, array( 'sprintf' => array( $this->_title ) ) ) )
            );
           
           
$buttons['download'] = array(
                   
'icon'    => 'download',
                   
'title'    => 'theme_set_export',
                   
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=exportForm&id={$this->_id}" . ( ( \IPS\IN_DEV or \IPS\Theme::designersModeEnabled() ) ? '' : '&form_submitted=1' ) )
            );
        }
               
        if ( \
IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'members', 'member_edit' ) )
        {
           
$buttons['member_theme_set'] = array(
                   
'icon'    => 'user',
                   
'title'    => 'theme_set_members',
                   
'link'    => $url->setQueryString( array( 'do' => 'setMembers', 'id' => $this->is_default ? 0 : $this->_id ) ),
                   
'data'     => array( 'ipsDialog' => '', 'ipsDialog-title' => $this->_title )
            );
        }
       
        if (
$this->by_skin_gen )
        {
           
$buttons['convert_to_full'] = array(
                   
'icon'    => 'exchange',
                   
'title'    => 'theme_set_convert_vsecustom_link',
                   
'link'    => $url->setQueryString( array( 'do' => 'convertToCustom', 'id' => $this->_id ) ),
                   
'data'     => array( 'confirm' => '', 'confirmMessage' => \IPS\Member::loggedIn()->language()->addToStack('theme_set_convert_vsecustom') )
            );
        }
       
        if (
$this->canDelete() )
        {
           
$buttons['delete'] = $parentButtons['delete'];
        }
       
        if ( \
IPS\IN_DEV OR \IPS\Theme::designersModeEnabled() )
        {
           
$buttons['theme_settings'] = array(
               
'icon'    => 'cog',
               
'title'    => 'theme_set_custom_setting',
               
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themesettings&set_id={$this->_id}" )
            );
           
            if (
$this->is_default OR \IPS\Theme::designersModeEnabled() )
            {
               
$buttons['dev_import'] = array(
                       
'icon'    => 'cogs',
                       
'title'    => 'theme_set_import_master',
                       
'link'    => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=devImport&id={$this->_id}" )
                );
            }
           
           
/*$buttons[] = array(
                    'icon'    => 'cogs',
                    'title'    => 'theme_set_build',
                    'link'    => \IPS\Http\Url::internal( "{$url}&do=build&id={$this->_id}" )
            );*/
       
}

        if ( \
IPS\Theme::designersModeEnabled() )
        {
            foreach( array(
'delete', 'copy', 'convert_to_full', 'upload', 'resources', 'edit_templates' ) as $btn )
            {
                if ( isset(
$buttons[ $btn ] ) )
                {
                    unset(
$buttons[ $btn ] );
                }
            }
        }
       
        return
$buttons;
    }
   
   
/**
     * [Node] Save Add/Edit Form
     *
     * @param    array    $values    Values from the form
     * @return    void
     */
   
public function saveForm( $values )
    {
       
$creating = FALSE;

       
/* Create if necessary */
       
if ( ! $this->id )
        {
           
$creating = TRUE;
           
$this->parent_array = '[]';
           
$this->child_array  = '[]';
           
$this->parent_id    = ( $values['core_theme_parent_id'] instanceof \IPS\Theme ) ? $values['core_theme_parent_id']->id : 0;
           
$this->by_skin_gen  = ( \IPS\Request::i()->theme_type === 'vse' ) ? 1 : 0;
           
$this->long_version = \IPS\Application::load( 'core' )->long_version;
           
$this->save();
           
           
/* Copy across resources */
           
$this->copyResourcesFromSet( $this->parent_id );
        }
       
        if ( isset(
$values['core_theme_set_new_import'] ) )
        {
           
/* Move it to a temporary location */
           
$tempFile = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
           
move_uploaded_file( $values['core_theme_set_new_import'], $tempFile );
           
           
/* Store values */
           
$key = 'core_theme_import_' . md5_file( $tempFile );
            \
IPS\Data\Store::i()->$key = array( 'apps'          => 'all',
                                               
'resources'        => true,
                                               
'html'            => true,
                                               
'css'            => true );
           
           
/* Initate a redirector */
           
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=customization&controller=themes&do=import' )->setQueryString( array( 'file' => $tempFile, 'key' => md5_file( $tempFile ), 'id' => $this->id ) ) );
        }
        else
        {
           
/* Name */
           
\IPS\Lang::saveCustom( 'core', "core_theme_set_title_{$this->id}", $values['core_theme_set_title'] );
           
           
/* CKEditor Skin */
           
if ( isset( $values['core_theme_set_editor'] ) and $values['core_theme_set_editor'] === 'new' and $values['core_theme_set_editor_new'] )
            {
               
/* Get the theme name */
               
$zip = zip_open( $values['core_theme_set_editor_new'] );
               
$name = zip_entry_name( zip_read( $zip ) );
               
$values['core_theme_set_editor_existing'] = mb_substr( $name, 0, mb_strpos( $name, '/' ) );
               
zip_close( $zip );
               
               
/* Extract it */
               
$zip = new \ZipArchive;
               
$zip->open( $values['core_theme_set_editor_new'] );
               
$zip->extractTo( \IPS\ROOT_PATH . '/applications/core/interface/ckeditor/ckeditor/skins' );
               
$zip->close();
               
               
/* Delete the temp file */
               
unlink( $values['core_theme_set_editor_new'] );
               
               
/* IPS Cloud Resync */
               
\IPS\IPS::resyncIPSCloud('Added CKEditor theme');
            }
           
           
$dataChanged = false;
           
$save        = array();

           
/* SITE LOGO */
           
if ( $values['core_theme_set_logo'] )
            {
               
$url   = (string) $values['core_theme_set_logo'];
                try
                {
                   
$image = \IPS\Image::create( \IPS\File::get( 'core_Theme', $url )->contents() );
                   
$this->_data['logo']['front'] = array( 'url' => $url, 'width' => $image->width, 'height' => $image->height );
                }
                catch ( \
RuntimeException $e )
                {
                   
$this->_data['logo']['front'] = NULL;
                }
               
               
$dataChanged = true;
            }
            else
            {
               
$dataChanged = true;
               
$this->_data['logo']['front'] = NULL;
            }
           
           
/* SHARER LOGO */
           
if ( $values['core_theme_set_sharer_logo'] )
            {
               
$url   = (string) $values['core_theme_set_sharer_logo'];
                try
                {
                   
$image = \IPS\Image::create( \IPS\File::get( 'core_Theme', $url )->contents() );
                   
$this->_data['logo']['sharer'] = array( 'url' => $url, 'width' => $image->width, 'height' => $image->height );
                }
                catch ( \
RuntimeException $e )
                {
                   
$this->_data['logo']['sharer'] = NULL;
                }
           
               
$dataChanged = true;
            }
            else
            {
               
$dataChanged = true;
               
$this->_data['logo']['sharer'] = NULL;
            }
           
           
/* FAVICON LOGO */
           
if ( $values['core_theme_set_favico_logo'] )
            {
               
$url   = (string) $values['core_theme_set_favico_logo'];
                   
               
$this->_data['logo']['favicon'] = array( 'url' => $url );
                   
               
$dataChanged = true;
            }
            else
            {
               
$dataChanged = true;
               
$this->_data['logo']['favicon'] = NULL;
            }
           
            if (
$values['core_theme_set_is_default'] )
            {
                \
IPS\Db::i()->update( 'core_themes', array( 'set_is_default' => 0 ), array( 'set_id<>?', $this->id ) );
               
$dataChanged = true;
            }
            if (
$values['core_theme_set_is_acp_default'] )
            {
                \
IPS\Db::i()->update( 'core_themes', array( 'set_is_acp_default' => 0 ), array( 'set_id<>?', $this->id ) );
               
$dataChanged = true;
            }
           
            if (
$dataChanged OR ( \IPS\IN_DEV OR \IPS\Theme::designersModeEnabled() ) )
            {
                if ( \
IPS\IN_DEV OR \IPS\Theme::designersModeEnabled() )
                {
                   
$save['set_author_name']  = $values['theme_template_export_author_name'];
                   
$save['set_author_url']   = $values['theme_template_export_author_url'];
                   
$save['set_update_check'] = $values['theme_update_check'];
                   
$save['set_version']      = $values['theme_template_export_version'];
                   
$save['set_long_version'] = $values['theme_template_export_long_version'];
                }
               
$this->save();
               
$this->saveSet( $save );
            }
           
           
$this->editor_skin = $values['core_theme_set_editor_existing'];
        }

       
$changedSettings = false;

        if (
$creating === FALSE )
        {
           
$themeSettings = $this->getThemeSettings( static::THEME_ID_KEY );

           
$save = array();
           
$json = array();
            foreach(
$themeSettings as $settingId => $row )
            {
                if (
array_key_exists( "core_theme_setting_title_{$row['sc_id']}", $values ) )
                {
                   
$field       = $this->getCustomSettingField( $row, TRUE );
                   
$stringValue = $field::stringValue( $values["core_theme_setting_title_{$row['sc_id']}"] );
                   
                    if (
get_class( $field ) == 'IPS\Helpers\Form\Editor' )
                    {
                        \
IPS\File::claimAttachments( 'theme_custom_autosave_' . $row['sc_id'], $this->id, $row['sc_id'] );
                    }
                   
                    if (
$values["core_theme_setting_title_{$row['sc_id']}"] and get_class( $field ) == 'IPS\Helpers\Form\Upload' )
                    {
                        if (
is_array( $values["core_theme_setting_title_{$row['sc_id']}"] ) )
                        {
                           
$items = array();
                            foreach(
$values["core_theme_setting_title_{$row['sc_id']}"] as $obj )
                            {
                               
$items[] = (string) $obj;
                            }
                           
$save[ $row['sc_id'] ] = implode( ',', $items );
                           
$json[ $row['sc_key'] ] = implode( ',', array_map( function( $v )
                            {
                                return
'<fileStore.core_Theme>/' .  $v;
                            },
$items ) );
                        }
                        else
                        {
                           
$save[ $row['sc_id'] ] = (string) $values["core_theme_setting_title_{$row['sc_id']}"];
                           
$json[ $row['sc_key'] ] = '<fileStore.core_Theme>/' . $save[ $row['sc_id'] ];
                        }
                    }
                    else
                    {
                       
$save[ $row['sc_id'] ]  = $stringValue;
                       
$json[ $row['sc_key'] ] = $save[ $row['sc_id'] ];
                    }
                }
            }

            if (
count( $save ) )
            {
                foreach(
$save as $id => $value )
                {
                    if (
$themeSettings[ $id ]['_value'] != $value )
                    {
                       
$changedSettings = true;
                    }

                    \
IPS\Db::i()->delete( 'core_theme_settings_values', array( 'sv_id=?', $id ) );
                    \
IPS\Db::i()->insert( 'core_theme_settings_values', array( 'sv_id' => $id, 'sv_value' => (string) $value ) );

                    if ( \
IPS\IN_DEV AND $this->id === \IPS\DEFAULT_THEME_ID )
                    {
                        \
IPS\Db::i()->update( 'core_theme_settings_fields', array( 'sc_default' => (string) $value ), array( 'sc_set_id=? and sc_id=?', \IPS\DEFAULT_THEME_ID, $id ) );
                    }
                }

                \
IPS\Db::i()->update( 'core_themes', array( 'set_template_settings' => json_encode( $json ) ), array( 'set_id=?', $this->id ) );

                if ( \
IPS\IN_DEV AND $this->id === \IPS\DEFAULT_THEME_ID )
                {
                    \
IPS\Theme\Dev\Theme::writeThemeSettingsToDisk();
                }
            }
        }

       
/* Designers mode needs specialist attention */
       
if ( $creating and \IPS\Theme::designersModeEnabled() AND \IPS\IN_DEV === FALSE )
        {
            \
IPS\Theme\Advanced\Theme::$buildingFiles = true;
            \
IPS\Theme\Advanced\Theme::$currentThemeId = $this->id;

            foreach( \
IPS\Application::applications() as $app )
            {
                \
IPS\Theme\Advanced\Theme::exportTemplates( $app->directory );
                \
IPS\Theme\Advanced\Theme::exportCss( $app->directory );
                \
IPS\Theme\Advanced\Theme::exportResources( $app->directory );
            }
        }

       
/* Clear out compiled CSS so CSS theme plugins are up to date and rebuild parent/child trees */
       
$themeSetToBuild = static::load( $this->id );
       
        if (
$changedSettings )
        {
            static::
themeSettingsHaveChanged( $this->id );
        }
       
       
$this->is_default  = $values['core_theme_set_is_default'];
       
$this->is_acp_default  = $values['core_theme_set_is_acp_default'];
       
$this->permissions = ( $values['core_theme_set_permissions'] === '*' ) ? '*' : implode( ',', $values['core_theme_set_permissions'] );
       
$this->parent_id   = ( $values['core_theme_parent_id'] instanceof \IPS\Theme ) ? $values['core_theme_parent_id']->id : 0;
       
       
$this->save();
       
        if (
$creating === TRUE AND $this->id )
        {
           
$this->installThemeSettings();
           
            if (
$this->parent_id )
            {
               
$this->copyLogosFromSet( $this->parent_id );
            }
        }
    }
   
   
/**
     * Build resource map of "human URL" to File Object URL
     *
     * @param    string|array    $app    App (e.g. core, forum)
     * @return    void
     */
   
public function buildResourceMap( $app=NULL )
    {
       
$flagKey = 'resource_compiling_' . $this->_id . '_' . $app;
        if ( static::
checkLock( $flagKey ) )
        {
            return
NULL;
        }

        static::
lock( $flagKey );
       
       
$resourceMap = $this->resource_map;
   
       
$where = ( $app !== null ) ? array( 'resource_set_id=? and resource_app=?', $this->_id, $app ) : array('resource_set_id=?', $this->_id );
       
$keysSeen = array();

        foreach ( \
IPS\Db::i()->select( '*', 'core_theme_resources', $where ) as $row )
        {
           
$name = static::makeBuiltTemplateLookupHash( $row['resource_app'], $row['resource_location'], $row['resource_path'] ) . '_' . $row['resource_name'];

            if (
$row['resource_filename'] )
            {
               
$keysSeen[]         = $name;
               
$resourceMap[$name] = $row['resource_filename'];
            }
            else
            {
               
/* If there is no filename, then it has yet to be compiled so do not add it to the resource map as it prevents it being compiled later */
               
unset( $resourceMap[$name] );
            }
        }

       
$this->resource_map = $resourceMap;
       
$this->save();
       
        static::
unlock( $flagKey );
    }

   
/**
     * Copy all resources from set $id to this set
     * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
     *
     * @param    int        $id        ID to copy from. 0 is the 'master' resources (same as the default when first installed)
     * @return  void
     */
   
public function copyResourcesFromSet( $id=0 )
    {
        \
IPS\Db::i()->delete( 'core_theme_resources', array( 'resource_set_id=?', $this->_id ) );
       
       
$resourceMap = array();
       
        foreach ( \
IPS\Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=?', $id ) ) as $data )
        {
           
$key = static::makeBuiltTemplateLookupHash($data['resource_app'], $data['resource_location'], $data['resource_path']) . '_' . $data['resource_name'];
           
            if (
$data['resource_data'] )
            {
                try
                {
                   
$fileName = (string) \IPS\File::create( 'core_Theme', $key, $data['resource_data'], 'set_resources_' . $this->_id, FALSE, NULL, FALSE );
               
                    \
IPS\Db::i()->insert( 'core_theme_resources', array(
                           
'resource_set_id'      => $this->_id,
                           
'resource_app'         => $data['resource_app'],
                           
'resource_location'    => $data['resource_location'],
                           
'resource_path'        => $data['resource_path'],
                           
'resource_name'        => $data['resource_name'],
                           
'resource_added'       => time(),
                           
'resource_filename'    => $fileName,
                           
'resource_data'        => $data['resource_data'],
                           
'resource_plugin'       => isset( $data['resource_plugin'] ) ? $data['resource_plugin'] : NULL,
                           
'resource_user_edited' => $data['resource_user_edited']
                    ) );
                   
                   
$resourceMap[ $key ] = $fileName;
                }
                catch( \
Exception $e ) { }
            }
        }
       
       
/* Update theme map */
       
$this->resource_map = $resourceMap;
       
$this->save();
    }
   
   
/**
     * Copy all logos from a set
     *
     * @param    int        $id            ID to copy from
     * @param    boolean    $cloning    Are we cloning?
     * @return  void
     */
   
public function copyLogosFromSet( $id )
    {
        try
        {
           
$original = static::load( $id );
        }
        catch( \
OutOfRangeException $e )
        {
            throw new \
OutOfRangeException("CANNOT_LOAD_THEME");
        }
       
        if (
$original->logo_data === NULL )
        {
            return;
        }
       
       
$currentLogos = json_decode( $this->logo_data, TRUE );
       
$logos        = array();
        foreach (
json_decode( $original->logo_data, TRUE ) as $file => $data )
        {
            if ( isset(
$currentLogos[ $file ] ) )
            {
                continue;
            }
           
            if( isset(
$data['url'] ) and $data['url'] )
            {
                try
                {
                   
/* Create new file */
                   
$original = \IPS\File::get( 'core_Theme', $data['url'] );
                   
$original->contents();
                   
$image = \IPS\Image::create( $original->contents() );
                   
$newImage = \IPS\File::create( 'core_Theme', $original->originalFilename, $original->contents() );

                   
$logos[$file] = array( 'url' => (string) $newImage, 'width' => $image->width, 'height' => $image->height );
                }
                catch ( \
Exception $e ) { }
            }
        }
       
       
$this->logo_data = json_encode( $logos );
       
$this->save();
    }
   
   
/**
     * It installs theme settings. What did you really expect?
     *
     * @return void
     */
   
public function installThemeSettings()
    {
       
/* Make sure theme setting fields are clear */
       
\IPS\Db::i()->delete( 'core_theme_settings_fields', array( 'sc_set_id=?', $this->id ) );
       
        if ( !
$this->parent_id )
        {
            foreach( \
IPS\Application::applications() as $appKey => $data )
            {
               
/* Root skin? Add default theme settings */
               
if ( ! file_exists( \IPS\ROOT_PATH . "/applications/{$appKey}/data/themesettings.json" ) )
                {
                    continue;
                }
               
               
$json = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$appKey}/data/themesettings.json" ), true );
               
               
/* Add */
               
foreach( $json as $key => $data)
                {
                   
$insertId = \IPS\Db::i()->insert( 'core_theme_settings_fields', array(
                       
'sc_set_id'         => $this->id,
                       
'sc_key'         => $data['sc_key'],
                       
'sc_tab_key'     => $data['sc_tab_key'],
                       
'sc_type'         => $data['sc_type'],
                       
'sc_multiple'     => $data['sc_multiple'],
                       
'sc_default'     => $data['sc_default'],
                       
'sc_content'     => $data['sc_content'],
                       
'sc_show_in_vse' => ( isset( $data['sc_show_in_vse'] ) ) ? $data['sc_show_in_vse'] : 0,
                       
'sc_updated'     => time(),
                       
'sc_app'         => $appKey,
                       
'sc_title'         => $data['sc_title'],
                       
'sc_order'         => $data['sc_order'],
                       
'sc_condition'     => $data['sc_condition'],
                    ) );
                   
                    \
IPS\Db::i()->insert( 'core_theme_settings_values', array( 'sv_id' => $insertId, 'sv_value' => (string) $data['sc_default'] ) );
                }
            }
        }
        else
        {
           
$parent = \IPS\Theme::load( $this->parent_id );
           
$themeSettings = $parent->getThemeSettings( static::THEME_ID_KEY );

           
$save = array();
           
            foreach(
$themeSettings as $settingId => $row )
            {
               
$insertId = \IPS\Db::i()->insert( 'core_theme_settings_fields', array(
                   
'sc_set_id'      => $this->id,
                   
'sc_key'         => $row['sc_key'],
                   
'sc_tab_key'     => $row['sc_tab_key'],
                   
'sc_type'        => $row['sc_type'],
                   
'sc_multiple'    => $row['sc_multiple'],
                   
'sc_default'     => $row['sc_default'],
                   
'sc_content'     => $row['sc_content'],
                   
'sc_show_in_vse' => ( isset( $row['sc_show_in_vse'] ) ) ? $row['sc_show_in_vse'] : 0,
                   
'sc_updated'     => time(),
                   
'sc_app'         => $row['sc_app'],
                   
'sc_title'         => $row['sc_title'],
                   
'sc_order'         => $row['sc_order'],
                   
'sc_condition'     => $row['sc_condition'],
                ) );
               
                \
IPS\Db::i()->insert( 'core_theme_settings_values', array( 'sv_id' => $insertId, 'sv_value' => (string) $row['_value'] ) );
            }
        }
       
       
$themeSettings = $this->getThemeSettings();
       
       
$json = array();
        foreach(
$themeSettings as $settingId => $row )
        {
           
$json[ $row['sc_key'] ]    = $row['_value'];
        }
       
       
$this->_data['settings'] = $json;
       
$this->template_settings = json_encode( $json );
       
$this->save();
    }
   
   
/**
     * Return all children
     *
     * @param    $theme    \IPS\Theme item
     * @return    array
     */
     
public function allChildren( &$return=array() )
     {
         foreach(
$this->children() as $child )
         {
             
$return[ $child->id ] = $child;
             
             
$child->allChildren( $return );
         }
         
         return
$return;
     }
     
   
/**
     * Compile CSS ready for non IN_DEV use. This replaces any HTML logic such as {resource="foo.png"} with full URLs
     *
     * @param string|array    $app        CSS app (e.g. core, forum)
     * @param string|array    $location    CSS location (e.g. admin,global,front)
     * @param string|array    $group        CSS group (e.g. custom, framework)
     * @param string        $name        CSS name (e.g. foo.css)
     * @return boolean|null
     */
   
public function compileCss( $app=null, $location=null, $group=null, $name=null )
    {
       
$flagKey = 'css_compiling_' . $this->_id . '_' . md5( $app . ',' . $location . ',' . $name );
        if ( static::
checkLock( $flagKey ) )
        {
            return
NULL;
        }

        static::
lock( $flagKey );
       
       
/* Deconstruct build grouping */
       
if ( $name !== null )
        {
            if ( isset( static::
$buildGrouping['css'][ $app ][ $location ] ) )
            {
                foreach( static::
$buildGrouping['css'][ $app ][ $location ] as $grouped )
                {
                    if (
str_replace( '.css', '', $name ) == $grouped )
                    {
                       
$group = $grouped;
                    }
                }
            }
        }

       
$css    = $this->getRawCss( $app, $location, $group );
       
$cssMap = $this->css_map;
       
        if (
$name === null )
        {
           
/* Clear out existing built bits */
           
\IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $this->_id );
           
           
$cssMap = array();
        }

        foreach(
$css as $app => $v )
        {
            foreach(
$css[ $app ] as $location => $paths )
            {
               
$built = array();
   
                foreach(
$css[ $app ][ $location ] as $path => $data )
                {
                    foreach(
$css[ $app ][ $location ][ $path ] as $cssName => $cssData )
                    {
                        if ( isset( static::
$buildGrouping['css'][ $app ][ $location ] ) AND in_array( $path,  static::$buildGrouping['css'][ $app ][ $location ] ) )
                        {
                            if (
$name === null OR $name == ( $path . '.css' ) )
                            {
                               
$key = static::makeBuiltTemplateLookupHash( $app, $location, $path );
                               
                                if ( isset(
$built[ $key ] ) )
                                {
                                   
$built[ $key ]['css_content'] .= "\n\n" . $cssData['css_content'];
                                }
                                else
                                {
                                   
$cssData['css_name'] = $path . '.css';
                                   
$cssData['css_path'] = '.';
       
                                   
$built[ $key ] = $cssData;
                                }
                            }
                        }
                        else
                        {
                            if (
$name === null OR $name == $cssData['css_name'] )
                            {
                               
$store  = static::makeBuiltTemplateLookupHash( $app, $location, $cssData['css_path'] . '/' . $cssData['css_name'] );
                   
                               
$cssMap[ $store ] = (string) static::writeCss( $cssData );
                            }
                        }
                    }
                }
               
               
/* Write combined css */
               
if ( count( $built ) )
                {
                    foreach(
$built as $id => $cssData )
                    {
                       
$store = static::makeBuiltTemplateLookupHash( $app, $location, $cssData['css_path'] . '/' . $cssData['css_name'] );

                       
$cssMap[ $store ] = (string) static::writeCss( $cssData );
                    }
                }
            }
        }

       
$this->css_map = $cssMap;
       
$this->save();
   
        static::
unlock( $flagKey );

        return
TRUE;
    }
   
   
/**
     * Build Templates ready for non IN_DEV use
     * This fetches all templates in a group, converts HTML logic into ready to eval PHP and stores as a single PHP class per template group
     *
     * @param    string|array    $app        Templates app (e.g. core, forum)
     * @param    string|array    $location    Templates location (e.g. admin,global,front)
     * @param    string|array    $group        Templates group (e.g. forms, members)
     * @return    boolean|null
     */
   
public function compileTemplates( $app=null, $location=null, $group=null )
    {
       
$flagKey = 'template_compiling_' . $this->_id . '_' . md5( $app . ',' . $location . ',' . $group );
        if ( static::
checkLock( $flagKey ) )
        {
            return
NULL;
        }

        static::
lock( $flagKey );

       
$templates = $this->getRawTemplates( $app, $location, $group );

        foreach(
$templates as $app => $v )
        {
            foreach(
$templates[ $app ] as $location => $groups )
            {
                foreach(
$templates[ $app ][ $location ] as $group => $bits )
                {
                   
/* Any template hooks? */
                   
$templateHooks = array();
                    if( isset( \
IPS\IPS::$hooks[ "\\IPS\\Theme\\class_{$app}_{$location}_{$group}" ] ) AND \IPS\RECOVERY_MODE === FALSE )
                    {
                        foreach ( \
IPS\IPS::$hooks[ "\\IPS\\Theme\\class_{$app}_{$location}_{$group}" ] as $k => $data )
                        {
                            if ( !
class_exists( "IPS\\Theme\\" . $data['class'] . "_tmp", FALSE ) )
                            {
                               
/* Like code hooks, we should only attempt to load the files contents if it actually exists */
                               
if ( file_exists( \IPS\ROOT_PATH . '/' . $data['file'] ) )
                                {
                                    try
                                    {
                                        if ( eval(
"namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp', 'array()' ), file_get_contents( \IPS\ROOT_PATH . '/' . $data['file'] ) ) ) !== FALSE )
                                        {
                                           
$class = "IPS\\Theme\\" . $data['class'] . "_tmp";
                                           
$templateHooks = array_merge_recursive( $templateHooks, $class::hookData() );
                                        }
                                    }
                                    catch ( \
ParseError $e ) { }
                                }
                            }
                        }
                    }

                   
/* Build all the functions */
                   
$functions = array();
                    foreach(
$templates[ $app ][ $location ][ $group ] as $name => $data )
                    {
                        if ( isset(
$templateHooks[ $name ] ) )
                        {                            
                           
$data['template_content'] = static::themeHooks( $data['template_content'], $templateHooks[ $name ] );
                        }
                       
                       
$functions[ $name ] = static::compileTemplate( $data['template_content'], $name, $data['template_data'], true, false, $app, $location, $group );
                    }
               
                   
/* Put them in a class */
                   
$template = <<<EOF
namespace _NAMESPACE_;
class class_
{$app}_{$location}_{$group} extends \IPS\Theme\Template
{
    public \$cache_key = '
{$this->cache_key}';

EOF;
                   
                   
$template .= implode( "\n\n", $functions );
       
                   
$template .= <<<EOF
}
EOF;

                   
/* Store it */
                   
$key = \strtolower( 'template_' . $this->_id . '_' .static::makeBuiltTemplateLookupHash( $app, $location, $group ) . '_' . static::cleanGroupName( $group ) );
                    \
IPS\Data\Store::i()->$key = str_replace( 'namespace _NAMESPACE_', 'namespace IPS\Theme', $template );
                   
                    if ( static::
isUsingTemplateDiskCache() and $location != 'admin' )
                    {
                       
/* We need a different namespace to prevent a "class already exists" when a cache loads, but is out of date so datastore is loaded and evalled with the exact same class name */
                       
$cache = new \IPS\Theme\Cache\Template( $app, $location, $group );
                        try
                        {
                           
$cache->set( str_replace( 'namespace _NAMESPACE_', 'namespace IPS\Theme\Cache', $template ) );
                        }
                        catch( \
RuntimeException $e )
                        {
                           
/* Should we log, ignore or collect failures to notify admin? */
                       
}
                    }
                }
            }
        }

        static::
unlock( $flagKey );

        return
TRUE;
    }

   
/**
     * Clean the group name
     *
     * @param   string      $name       The name to clean
     * @return  string
     */
   
public static function cleanGroupName( $name )
    {
        return
str_replace( '-', '_', \IPS\Http\Url\Friendly::seoTitle( $name ) );
    }

   
/**
     * Find templates in a template group
     *
     * @param    string        $group        Template group to search in
     * @param    string|null    $app        Application key
     * @param    string|null    $location    Template location
     * @return    array
     */
   
public static function findTemplatesByGroup( $group, $app=NULL, $location=NULL )
    {
       
/*
            Upon review, designers mode should not be used by developers, and subsequently the areas calling this method shouldn't see new templates only available
            in designers mode before they are imported to the database.
        if ( \IPS\Theme::designersModeEnabled() )
        {
            return \IPS\Theme\Advanced\Theme::findTemplatesByGroup( $group, $app, $location );
        }
        else*/
if ( \IPS\IN_DEV )
        {
            return \
IPS\Theme\Dev\Theme::findTemplatesByGroup( $group, $app, $location );
        }

       
$where    = array( array( 'template_group=?', $group ) );

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

        if(
$location !== NULL )
        {
           
$where[]    = array( 'template_location=?', $location );
        }

       
$results    = array();

        foreach( \
IPS\Db::i()->select( 'template_name', 'core_theme_templates', $where ) as $result )
        {
           
$results[ $result['template_name'] ]    = $result['template_name'];
        }

        return
array_unique( $results );
    }
   
   
/**
     * [Node] Does the currently logged in user have permission to edit permissions for this node?
     *
     * @return    bool
     */
   
public function canManagePermissions()
    {
        return
false;
    }
   
   
/**
     * Delete CSS
     * If the CSS is customized in this theme set, it will just delete the custom CSS giving the appearance of a revert.
     * If the CSS is new in this theme set, then it will be deleted completely.
     *
     * @param    int $itemId    Specific CSS ID to remove
     * @return    array
     * @throws    \UnderflowException
     */
   
public function deleteCssById( $itemId )
    {
       
$children = array( $this->_id );

        foreach(
$this->allChildren() as $id => $child )
        {
           
$children[] = $child->_id;
        }
       
       
$css = \IPS\Db::i()->select( '*', 'core_theme_css', array( 'css_id=?', (int) $itemId ) )->first();
       
        static::
deleteCompiledCss( $css['css_app'], $css['css_location'], $css['css_path'], $css['css_name'], $css['css_set_id'] );
       
       
/* Inherited from master */
       
if ( $css['css_added_to'] == 0 )
        {
           
/* Inherited from master css */
           
if ( $css['css_set_id'] > 0 )
            {
               
/* Clear any existing CSS if it's been modified from set 0 */
               
\IPS\Db::i()->delete( 'core_theme_css', array( 'css_id=?', (int) $itemId ) );
            }
        }
        else
        {
           
/* This is CSS unique to this theme set, so remove it from this set and all children */
           
\IPS\Db::i()->delete( 'core_theme_css', array(
                                           
"css_app=? AND css_location=? AND css_path=? AND css_name=? AND css_set_id IN(" . implode( ',', $children ) . ")",
                                           
$css['css_app'], $css['css_location'], $css['css_path'], $css['css_name'] ) );
        }
       
        if ( ! empty(
$css['css_set_id'] ) )
        {
            static::
setThemeCustomized( $css['css_set_id'] );
        }

       
$csses = $this->getRawCss( array( $css['css_app'] ), array( $css['css_location'] ), array( $css['css_path'] ) );
       
        if ( isset(
$csses[ $css['css_app'] ][ $css['css_location'] ][ $css['css_path'] ][ $css['css_name'] ] ) )
        {
            return
$csses[ $css['css_app'] ][ $css['css_location'] ][ $css['css_path'] ][ $css['css_name'] ];
        }
        else
        {
            return array();
        }
    }
   
   
/**
     * Save CSS
     * Saves a CSS template
     *
     * @param    array    $data    array( 'app' => .., 'location' => .., 'group' => .., 'name' => .., 'set_id' => .., 'item_id' => .., 'position' => .., 'content' => .., 'variables' => .. )
     * @return    int        Template ID
     * @throws    \OutOfBoundsException
     * @throws    \UnderflowException
     */
   
public function saveCss( $data )
    {
       
$css      = array();
       
$children = array( $this->_id );

        foreach(
$this->allChildren() as $child )
        {
           
$children[] = $child->_id;
        }
       
       
/* Save */
       
if ( ! empty( $data['item_id'] ) )
        {
           
$css = \IPS\Db::i()->select( '*', 'core_theme_css', array( 'css_id=?', (int) $data['item_id'] ) )->first();
        }
        else
        {
           
$group  = ( empty( $data['group'] ) ) ? '.' : $data['group'];
           
$master = array();
           
            if (
mb_substr( $data['name'], -4 ) != '.css' )
            {
               
$data['name'] .= '.css';
            }
           
            try
            {
               
$master = \IPS\Db::i()->select( '*', 'core_theme_css', array( 'css_set_id=0 and css_app=? and css_path=? and css_location=? and css_name=?', $data['app'], $group, $data['location'], $data['name'] ) )->first();
            }
            catch( \
UnderflowException $ex ) { }
           
           
           
/* Set up some default data here */
           
$css = array( 'css_app'         => $data['app'],
                         
'css_path'        => $group,
                         
'css_location'    => $data['location'],
                         
'css_name'        => $data['name'],
                         
'css_version'     => \IPS\Theme::load( $data['set_id'] )->long_version,
                         
'css_user_edited' => \IPS\Application::load( $data['app'] )->long_version,
                         
'css_position'    => 0,
                         
'css_content'     => $data['content'],
                         
'css_added_to'    => $data['set_id'],
                         
'css_plugin'      => isset( $master['css_plugin'] ) ? $master['css_plugin'] : NULL );
        }

       
/* Test to make sure CSS isn't broken */
       
try
        {
            static::
makeProcessFunction( static::fixResourceTags( $data['content'], $css['css_location'] ), 'css_' . md5( mt_rand() . time() ), '', FALSE, TRUE );
        }
        catch( \
InvalidArgumentException $e )
        {
            throw new \
InvalidArgumentException( $e->getMessage() );
        }

        if ( ! empty(
$data['item_id'] ) AND $data['set_id'] == $css['css_set_id'] )
        {
            \
IPS\Db::i()->update( 'core_theme_css', array( 'css_attributes'  => ( ! empty( $data['attributes'] ) ) ? $data['attributes'] : '',
                                                           
'css_content'     => $data['content'],
                                                           
'css_user_edited' =>  \IPS\Application::load( $data['app'] )->long_version,
                                                           
'css_updated'     => time() ), array('css_id=?', (int)$data['item_id'] ) );
   
           
$newCssId = $data['item_id'];
        }
        else
        {
           
/* First modification in this css set */
           
$newCssId = \IPS\Db::i()->replace( 'core_theme_css', array( 'css_set_id'      => $data['set_id'],
                                                                       
'css_path'        => $css['css_path'],
                                                                       
'css_location'    => $css['css_location'],
                                                                       
'css_app'         => $css['css_app'],
                                                                       
'css_content'     => $data['content'],
                                                                       
'css_name'        => $css['css_name'],
                                                                       
'css_attributes'  => ( ! empty( $data['attributes'] ) ) ? $data['attributes'] : '',
                                                                       
'css_added_to'    => $css['css_added_to'],
                                                                       
'css_position'    => intval( $css['css_position'] ),
                                                                       
'css_version'     => \IPS\Theme::load( $data['set_id'] )->long_version,
                                                                       
'css_user_edited' => \IPS\Application::load( $css['css_app'] )->long_version,
                                                                       
'css_updated'     => time(),
                                                                       
'css_plugin'      => isset( $css['css_plugin'] ) ? $css['css_plugin'] : NULL ) );
        }
       
        static::
deleteCompiledCss( $css['css_app'], $css['css_location'], $css['css_path'], $css['css_name'], $data['set_id'] );
       
        static::
setThemeCustomized( $data['set_id'] );
       
        return
$newCssId;
    }
   
   
/**
     * Delete a template bit
     * If the template is customized in this theme set, it will just delete the custom template bit giving the appearance of a revert.
     * If the template is new in this theme set, then it will be deleted from templates completely.
     *
     * @param     int        $itemId    Actual template ID to remove
     * @return    array    Template bit row
     * @throws    \UnderflowException
     */
   
public function deleteTemplateById( $itemId )
    {
       
$children = array( $this->_id );

        foreach(
$this->allChildren() as $child )
        {
           
$children[] = $child->_id;
        }
       
       
$template = \IPS\Db::i()->select( '*', 'core_theme_templates', array( 'template_id=?', (int) $itemId ) )->first();
       
       
/* Master template bit? */
       
if ( $template['template_set_id'] == 0 AND $template['template_added_to'] == 0 )
        {
            return
false;
        }
       
        static::
deleteCompiledTemplate( $template['template_app'], $template['template_location'], $template['template_group'], $template['template_set_id'] );
       
       
/* What to do */
       
if ( $template['template_added_to'] == $this->_id  )
        {
           
/* Does it exist in set ID 0? */
           
$select = \IPS\Db::i()->select( 'template_id', 'core_theme_templates', array(
               
"template_set_id=? AND template_user_added=? AND template_name=? AND template_group=? AND template_app=? AND template_location=?",
               
0, 0, $template['template_name'], $template['template_group'], $template['template_app'], $template['template_location']
            ) );

           
/* Is from master theme? */
           
if ( count( $select ) )
            {
                \
IPS\Db::i()->delete( 'core_theme_templates', array( 'template_id=?', (int) $itemId ) );
            }
            else
            {
               
# Remove it from ALL template sets
               
\IPS\Db::i()->delete( 'core_theme_templates', array(
                                                             
"template_name=? AND template_group=? AND template_app=? AND template_location=?",
                                                               
$template['template_name'], $template['template_group'], $template['template_app'], $template['template_location']
                                                              ) );
            }
        }
        else
        {
            \
IPS\Db::i()->delete( 'core_theme_templates', array( 'template_id=?', (int) $itemId ) );
        }
       
        if ( ! empty(
$template['template_set_id'] ) )
        {
            static::
setThemeCustomized( $template['template_set_id'] );
        }
       
       
$templates = $this->getRawTemplates( array( $template['template_app'] ), array( $template['template_location'] ), array( $template['template_group'] ) );
       
        if ( isset(
$templates[ $template['template_app'] ][ $template['template_location'] ][ $template['template_group'] ][ $template['template_name'] ] ) )
        {
            return
$templates[ $template['template_app'] ][ $template['template_location'] ][ $template['template_group'] ][ $template['template_name'] ];
        }
        else
        {
            return array();
        }
    }
   
   
/**
     * User can access this theme?
     *
     * @return    bool
     */
   
public function canAccess()
    {
        if (
$this->is_default )
        {
            return
true;
        }

        return (bool) (
$this->permissions === '*' OR \IPS\Member::loggedIn()->inGroup( explode( ',', $this->permissions ) ) );
    }
   
   
/**
     * Save Template
     * Saves a HTML template
     *
     * @param    array    $data    array( 'app' => .., 'location' => .., 'group' => .., 'name' => .., 'set_id' => .., 'item_id' => ..,  'content' => .., 'variables' => .. )
     * @return    int        Template ID
     * @throws    \OutOfBoundsException
     * @throws    \UnderflowException
     */
   
public function saveTemplate( $data )
    {
       
$template = array();
       
       
/* Save */
       
if ( $data['item_id'] )
        {
           
$template = \IPS\Db::i()->select( '*', 'core_theme_templates', array( 'template_id=?', (int) $data['item_id'] ) )->first();
        }
        else
        {
           
/* Set up some default data here */
           
$template = array( 'template_app'        => $data['app'],
                               
'template_group'      => $data['group'],
                               
'template_location'   => $data['location'],
                               
'template_name'         => $data['name'],
                               
'template_added_to'   => $data['set_id'],
                               
'template_user_added' => 1 );
        }
       
       
/* Test to make sure CSS isn't broken */
       
try
        {
            static::
makeProcessFunction( $data['content'], 'template_' . md5( mt_rand() . time() ), $data['variables'], TRUE, FALSE );
        }
       
/* PHP 5 */
       
catch( \InvalidArgumentException $e )
        {
            throw new \
InvalidArgumentException( 'core_theme_template_parse_error' );
        }
       
/* PHP 7 */
       
catch( \ParseError $e )
        {
            throw new \
InvalidArgumentException( 'core_theme_template_parse_error' );
        }
       
       
$theme = \IPS\Theme::load( $data['set_id'] );
       
        if (
$data['item_id'] AND $data['set_id'] == $template['template_set_id'] )
        {
           
$save = array(
               
'template_content'        => $data['content'],
               
'template_user_edited'    => \IPS\Application::load( $template['template_app'] )->long_version,
               
'template_version'        => $theme->long_version,
               
'template_updated'        => time()
            );

            if(
$data['variables'] )
            {
               
$save['template_data']    = $data['variables'];
            }

            \
IPS\Db::i()->update( 'core_theme_templates', $save, array( 'template_id=?', (int) $data['item_id'] ) );
       
           
$newTemplateId = $data['item_id'];
        }
        else
        {
           
$newTemplateId = \IPS\Db::i()->insert( 'core_theme_templates', array( 'template_set_id'      => $data['set_id'],
                                                                                 
'template_group'       => $template['template_group'],
                                                                                 
'template_location'    => $template['template_location'],
                                                                                 
'template_app'         => $template['template_app'],
                                                                                 
'template_content'     => $data['content'],
                                                                                 
'template_name'        => $template['template_name'],
                                                                                 
'template_data'        => $data['variables'] ?: $template['template_data'],
                                                                                 
'template_added_to'    => $template['template_added_to'],
                                                                                 
'template_user_added'  => $template['template_user_added'],
                                                                                 
'template_user_edited' => \IPS\Application::load( $template['template_app'] )->long_version,
                                                                                 
'template_removable'   => 1,
                                                                                 
'template_version'     => $theme->long_version,
                                                                                 
'template_updated'     => time() ) );
        }
       
       
/* If this is a user-added template bit and we're editing it in the same theme, update master */
       
if ( $template['template_id'] && $template['template_user_added'] && $template['template_added_to'] == $data['set_id'] )
        {
            \
IPS\Db::i()->update( 'core_theme_templates',
                array(
'template_content' => $data['content'] ),
                array(
"template_set_id=? AND template_group=? AND template_name=? AND template_app=? AND template_location=?", 0, $template['template_group'], $template['template_name'], $template['template_app'], $template['template_location'] )
            );
        }
       
       
/* Make all template disk caches stale */
       
$theme->resetCacheKey();
        foreach(
$theme->children() as $child )
        {
           
$child->resetCacheKey();
        }
       
        static::
deleteCompiledTemplate( $template['template_app'], $template['template_location'], $template['template_group'], $data['set_id'] );
        static::
setThemeCustomized( $data['set_id'] );
       
        return
$newTemplateId;
    }
   
   
/**
     * Returns theme setting DB data with a special array _value which holds the 'true' value fo this setting.
     *
     * @param    $flags        Bit option flags
     * @return    array
     */
   
public function getThemeSettings( $flags=0 )
    {
       
$settings = array();
       
       
$rows = \IPS\Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( 'sc.sc_set_id=?', $this->id ) )
            ->
join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' );
           
        foreach (
$rows as $row )
        {
           
$row['_value'] = ( array_key_exists( 'sv_value', $row ) AND $row['sv_value'] !== NULL ) ? $row['sv_value'] : $row['sc_default'];

            if (
$row['_value'] and $row['sc_type'] === 'Upload' )
            {
                try
                {
                   
$row['_value'] = (string) \IPS\File::get( 'core_Theme', $row['_value'] )->url;
                }
                catch( \
Exception $ex )
                {

                }
            }

            if (
$flags & static::THEME_ID_KEY )
            {
               
$settings[ $row['sc_id'] ] = $row;
            }
            else if (
$flags & static::THEME_KEY_VALUE_PAIRS )
            {
               
$settings[ $row['sc_key'] ] = $row['_value'];
            }
            else
            {
               
$settings[ $row['sc_key'] ] = $row;
            }
        }
       
        return
$settings;
    }
   
   
/**
     * Get raw CSS. Raw means {resource..} tags and uncompiled
     *
     * @param string|array    $app                CSS app (e.g. core, forum)
     * @param string|array    $location            CSS location (e.g. admin,global,front)
     * @param string|array    $path                CSS group (e.g. custom, framework)
     * @param int|constant    $returnType            Determines the content returned
     * @param boolean        $returnThisSetOnly  Returns rows unique to this set only
     * @return array
     */
   
public function getRawCss( $app=array(), $location=array(), $path=array(), $returnType=null, $returnThisSetOnly=false )
    {
       
$returnType = ( $returnType === null )   ? static::RETURN_ALL   : $returnType;
       
$app        = ( is_string( $app )      AND $app != ''      ) ? array( $app )      : $app;
       
$location   = ( is_string( $location ) AND $location != '' ) ? array( $location ) : $location;
       
$path       = ( is_string( $path )     AND $path != ''    )  ? array( $path )     : $path;
       
$where      = array();
       
$css        = array();
   
       
$parents = array( $this->_id );

        try
        {
           
$allParents = array();
            foreach(
$this->parents() as $parent )
            {
               
$allParents[] = $parent->_id;
            }
           
            if (
count( $allParents ) )
            {
                foreach(
array_reverse( $allParents ) as $id )
                {
                   
$parents[] = $id;
                }
            }
        }
        catch( \
OutOfRangeException $e ) { }

       
/* Append master theme set */
       
array_push( $parents, 0 );
       
       
$where[] = "css_set_id IN (" . implode( ',' , $parents ) . ")";
       
        if (
is_array( $app ) AND count( $app ) )
        {
           
$where[] = "css_app IN ('" . implode( "','", $app ) . "')";
        }
   
        if (
is_array( $location ) AND count( $location ) )
        {
           
$where[] = "css_location IN ('" . implode( "','", $location ) . "')";
        }
   
        if (
is_array( $path ) AND count( $path ) )
        {
           
$where[] = "css_path IN ('" . implode( "','", $path ) . "')";
        }
       
       
$select = ( $returnType & static::RETURN_BIT_NAMES ) ? 'css_app, css_location, css_path, css_set_id, css_id, css_name, css_modules, css_attributes, css_removed, css_added_to, css_hidden, css_user_edited' : '*';
   
        foreach(
            \
IPS\Db::i()->select(
               
$select . ', INSTR(\',' . implode( ',' , $parents ) . ',\', CONCAT(\',\',css_set_id,\',\') ) as theorder',
               
'core_theme_css',
               
implode( " AND ", $where ),
               
'css_location, css_path, css_name, theorder desc'
           
)
            as
$row
       
) {
           
/* App installed? */
           
if ( ! \IPS\Application::appIsEnabled( $row['css_app'] ) )
            {
                continue;
            }
           
           
/* This set only? */
           
if ( $returnThisSetOnly === true )
            {
                if (
$row['css_set_id'] != $this->_id )
                {
                    continue;
                }
            }
           
           
/* CSS has been removed up the tree? */
           
if ( ! empty( $row['css_removed'] ) )
            {
                continue;
            }
           
           
/* CSS not to be included */
           
if ( ! empty( $row['css_hidden'] ) )
            {
                continue;
            }

            if (
$row['css_set_id'] === 0 )
            {
               
$row['InheritedValue'] = 'original';
            }
            else if (
$row['css_set_id'] != $this->_id )
            {
               
$row['InheritedValue'] = 'inherit';
            }
            else if (
$row['css_added_to'] != 0 )
            {
               
$row['InheritedValue'] = 'custom';
            }
            else
            {
               
$row['InheritedValue'] = ( $row['css_user_edited'] < \IPS\Application::load( $row['css_app'] )->long_version ) ? 'outofdate' : 'changed';
            }
           
           
/* ensure set ID is correct */
           
$row['css_set_id']  = $this->_id;
           
$row['CssKey']      = str_replace( '.css', '', $row['css_app'] . '_' . $row['css_location'] . '_' . $row['css_path'] . '_' . $row['css_name'] );
           
$row['jsDataKey']   = str_replace( '.', '--', $row['CssKey'] );
           
            if (
$returnType & static::RETURN_ALL_NO_CONTENT )
            {
                unset(
$row['css_content'] );
               
$css[ $row['css_app'] ][ $row['css_location'] ][ $row['css_path'] ][ $row['css_name'] ] = $row;
            }
            else if (
$returnType & static::RETURN_ALL )
            {
               
$css[ $row['css_app'] ][ $row['css_location'] ][ $row['css_path'] ][ $row['css_name'] ] = $row;
            }
            else if (
$returnType & static::RETURN_BIT_NAMES )
            {
               
$css[ $row['css_app'] ][ $row['css_location'] ][ $row['css_path'] ][] = $row['css_name'];
            }
            else if (
$returnType & static::RETURN_ARRAY_BIT_NAMES )
            {
               
$css[] = $row['css_name'];
            }
        }
   
        if (
$returnType & static::RETURN_ARRAY_BIT_NAMES )
        {
           
sort( $css );
            return
$css;
        }
       
       
ksort( $css );
   
       
/* Pretty sure Mark can turn this into a closure */
       
foreach( $css as $k => $v )
        {
           
ksort( $css[ $k ] );
               
            foreach(
$css[ $k ] as $ak => $av )
            {
               
ksort( $css[ $k ][ $ak ] );
                   
                if (
$returnType & static::RETURN_ALL )
                {
                    foreach(
$css[ $k ][ $ak ] as $bk => $bv )
                    {
                       
ksort( $css[ $k ][ $ak ][ $bk ] );
                    }
                }
            }
        }

        return
$css;
    }
   
   
/**
     * Get raw templates. Raw means HTML logic and variables are still in {{format}}
     *
     * @param string|array    $app                Template app (e.g. core, forum)
     * @param string|array    $location            Template location (e.g. admin,global,front)
     * @param string|array    $group                Template group (e.g. login, share)
     * @param int|constant    $returnType            Determines the content returned
     * @param boolean        $returnThisSetOnly  Returns rows unique to this set only
     * @return array
     */
   
public function getRawTemplates( $app=array(), $location=array(), $group=array(), $returnType=null, $returnThisSetOnly=false )
    {
       
$returnType = ( $returnType === null )  ? static::RETURN_ALL   : $returnType;
       
$app        = ( is_string( $app )      AND $app != ''      ) ? array( $app )      : $app;
       
$location   = ( is_string( $location ) AND $location != '' ) ? array( $location ) : $location;
       
$group      = ( is_string( $group )    AND $group != ''    ) ? array( $group )    : $group;
       
$where      = array();
       
$templates  = array();

       
$parents = array( $this->_id );

        try
        {
           
$allParents = array();
            foreach(
$this->parents() as $parent )
            {
               
$allParents[] = $parent->_id;
            }
           
            if (
count( $allParents ) )
            {
                foreach(
array_reverse( $allParents ) as $id )
                {
                   
$parents[] = $id;
                }
            }
        }
        catch( \
OutOfRangeException $e ) { }

       
/* Append master theme set */
       
array_push( $parents, 0 );
       
       
$where[] = "template_set_id IN (" . implode( ',' , $parents ) . ")";
       
        if (
is_array( $app ) AND count( $app ) )
        {
           
$where[] = "template_app IN ('" . implode( "','", $app ) . "')";
        }
       
        if (
is_array( $location ) AND count( $location ) )
        {
           
$where[] = "template_location IN ('" . implode( "','", $location ) . "')";
        }
       
        if (
is_array( $group ) AND count( $group ) )
        {
           
$where[] = "template_group IN ('" . implode( "','", $group ) . "')";
        }
   
       
$select = ( $returnType & static::RETURN_BIT_NAMES ) ? 'template_added_to, template_app, template_location, template_group, template_set_id, template_id, template_name, template_data' : '*';
       
        foreach(
            \
IPS\Db::i()->select(
               
$select . ', INSTR(\',' . implode( ',' , $parents ) . ',\', CONCAT(\',\',template_set_id,\',\') ) as theorder',
               
'core_theme_templates',
               
implode( " AND ", $where ),
               
'template_location, template_group, template_name, theorder desc'
           
)
            as
$row
       
) {
           
/* App installed? */
           
if ( ! \IPS\Application::appIsEnabled( $row['template_app'] ) )
            {
                continue;
            }
           
           
/* This set only? */
           
if ( $returnThisSetOnly === true )
            {
                if (
$row['template_set_id'] != $this->_id )
                {
                    continue;
                }
            }
           
            if (
$row['template_set_id'] === 0 and ! $row['template_user_added'] )
            {
               
$row['InheritedValue'] = 'original';
            }
            else if (
$row['template_user_added'] and in_array( $row['template_added_to'], array_values( $allParents ) ) )
            {
               
$row['InheritedValue'] = ( $row['template_removable'] and $this->_id == $row['template_set_id'] ) ? 'changed' : 'inherit';
            }
            else if (
$row['template_user_added'] and ( $row['template_added_to'] != $this->_id and $row['template_set_id'] != $this->_id ) )
            {
               
$row['InheritedValue'] = 'original';
            }
            else if (
$row['template_user_added'] )
            {
                if (
$row['template_added_to'] == $this->_id )
                {
                   
$row['InheritedValue'] = 'custom';
                }
                else
                {
                   
$row['InheritedValue'] = ( $row['template_user_edited'] < \IPS\Application::load( $row['template_app'] )->long_version ) ? 'outofdate' : 'changed';
                }
            }
            else
            {
               
$row['InheritedValue'] = ( $row['template_user_edited'] < \IPS\Application::load( $row['template_app'] )->long_version ) ? 'outofdate' : 'changed';
            }
           
           
/* ensure set ID is correct */
           
$row['template_set_id'] = $this->_id;
           
$row['TemplateKey']     = $row['template_app'] . '_' . $row['template_location'] . '_' . $row['template_group'] . '_' . $row['template_name'];
           
$row['jsDataKey']       = str_replace( '.', '--', $row['TemplateKey'] );
           
            if (
$returnType & static::RETURN_ALL_NO_CONTENT )
            {
                unset(
$row['template_content'] );
               
$templates[ $row['template_app'] ][ $row['template_location'] ][ $row['template_group'] ][ $row['template_name'] ] = $row;
            }
            else if (
$returnType & static::RETURN_ALL )
            {
               
$templates[ $row['template_app'] ][ $row['template_location'] ][ $row['template_group'] ][ $row['template_name'] ] = $row;
            }
            else if (
$returnType & static::RETURN_BIT_NAMES )
            {
               
$templates[ $row['template_app'] ][ $row['template_location'] ][ $row['template_group'] ][] = $row['template_name'];
            }
            else if (
$returnType & static::RETURN_ARRAY_BIT_NAMES )
            {
               
$templates[] = $row['template_name'];
            }
        }
   
        if (
$returnType & static::RETURN_ARRAY_BIT_NAMES )
        {
           
sort( $templates );
            return
$templates;
        }
       
       
ksort( $templates );

       
/* Pretty sure Mark can turn this into a closure */
       
foreach( $templates as $k => $v )
        {
           
ksort( $templates[ $k ] );
           
            foreach(
$templates[ $k ] as $ak => $av )
            {
               
ksort( $templates[ $k ][ $ak ] );
           
                if (
$returnType & static::RETURN_ALL )
                {
                    foreach(
$templates[ $k ][ $ak ] as $bk => $bv )
                    {
                       
ksort( $templates[ $k ][ $ak ][ $bk ] );
                    }
                }
            }
        }

        return
$templates;
    }
   
   
/**
     * Save the visual skin editor CSS
     *
     * @param    string $vseCss        VSE Generated CSS
     * @param    string $customCss        User added CSS
     * @param    array  $settings        Settings to update
     * @param    array  $values        JSON string of data from the VSE
     * @return    void
     */
   
public function vseSave( $vseCss, $customCss, $settings, $values )
    {
       
$css = $this->getRawCss( 'core', 'front', 'custom', static::RETURN_ALL, true );

       
$vse = array( 'app'      => 'core',
                     
'location' => 'front',
                     
'group'    => 'custom',
                     
'name'     => 'vse.css',
                     
'set_id'   => $this->id,
                     
'content'  => $vseCss
       
);
       
       
$custom = array( 'app'         => 'core',
                         
'location' => 'front',
                         
'group'    => 'custom',
                         
'name'        => 'custom.css',
                         
'set_id'   => $this->id,
                         
'content'  => $customCss
       
);
       
       
/* Do we have vse already? */
       
if ( isset( $css['core']['front']['custom']['vse.css'] ) )
        {
           
$vse['item_id'] = $css['core']['front']['custom']['vse.css']['css_id'];
        }
       
       
/* Do we have custom already? */
       
if ( isset( $css['core']['front']['custom']['custom.css'] ) )
        {
           
$custom['item_id'] = $css['core']['front']['custom']['custom.css']['css_id'];
        }
       
       
$this->saveCss( $vse );
       
$this->saveCss( $custom );
       
       
$this->skin_gen_data = $values;
       
$this->save();
       
       
/* Settings first */
       
$themeSettings      = $this->getThemeSettings( static::THEME_ID_KEY );
       
$themeSettingsByKey = array();
       
        foreach(
$themeSettings as $themeSettingId => $themeSettingData )
        {
           
$themeSettingsByKey[ $themeSettingData['sc_key'] ] = $themeSettingData;
        }
       
        if (
is_array( $settings ) )
        {
            foreach(
$settings as $key => $value )
            {
                if ( \
stristr( $key, 'core_theme_setting_title_' ) )
                {
                   
$keyId = str_replace( 'core_theme_setting_title_', '', $key );
               
                    if ( isset(
$themeSettings[ $keyId ] ) )
                    {
                        \
IPS\Db::i()->delete('core_theme_settings_values', array( 'sv_id=?', $keyId ) );
                        \
IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $keyId, 'sv_value' => (string)$value ) );
                    }
                }
                else
                {
                    if ( isset(
$themeSettingsByKey[ $key ] ) )
                    {
                        \
IPS\Db::i()->delete('core_theme_settings_values', array( 'sv_id=?', $themeSettingsByKey[ $key ]['sc_id'] ) );
                        \
IPS\Db::i()->insert('core_theme_settings_values', array( 'sv_id' => $themeSettingsByKey[ $key ]['sc_id'], 'sv_value' => (string)$value ) );
                    }
                }
            }
           
           
$this->saveSet();
        }
       
        static::
deleteCompiledCss( 'core', 'front', 'custom', null, $this->id );
    }
   
   
/**
     * Save a theme set
     *
     * @param    array $data    Skin set data
     * @return    void
     */
   
public function saveSet( $data=array() )
    {
       
$save    = array();
       
$fields  = array( 'name', 'key', 'parent_id', 'permissions', 'is_default', 'is_acp_default', 'author_name', 'author_url', 'resource_dir', 'emo_dir', 'hide_from_list', 'order', 'version', 'long_version', 'update_check' );

        foreach(
$fields as $k )
        {
            if ( isset(
$data[ 'set_' . $k ] ) )
            {
               
$save[ 'set_' . $k ] = $data[ 'set_' . $k ];
            }
        }
       
        if ( !
$this->_id )
        {
           
$save['set_long_version'] = ( ! empty( $save['set_long_version'] ) ) ? (int) $save['set_long_version'] : \IPS\Application::load( 'core' )->long_version;
        }
        else if ( isset(
$save['set_long_version'] ) )
        {
           
$save['set_long_version'] = intval( $save['set_long_version'] );
        }
       
        foreach( array(
'front', 'sharer', 'favicon' ) as $icon )
        {
            if ( isset(
$data['logo'][ $icon ] ) )
            {
               
$this->_data['logo'][ $icon ] = $data['logo'][ $icon ];
            }
        }
       
        if ( isset(
$data['set_name'] ) )
        {
            \
IPS\Lang::saveCustom( 'core', "core_theme_set_title_{$this->_id}", $data['set_name'] );
        }
       
       
$save['set_logo_data'] = ( isset( $this->_data['logo'] ) ? json_encode( $this->_data['logo'] ) : '{}' );
       
        if ( isset(
$data['set_css_map'] ) )
        {
           
$save['set_css_map'] = json_encode( $data['set_css_map'] );
        }
       
        if ( isset(
$data['set_resource_map'] ) )
        {
           
$save['set_resource_map'] = json_encode( $data['set_resource_map'] );
        }
       
       
$json          = array();
       
$themeSettings = $this->getThemeSettings();
       
        foreach (
$themeSettings as $key => $row )
        {
           
$json[ $row['sc_key'] ] = $row['_value'];
        }
       
       
$save['set_template_settings'] = json_encode( $json );
       
        \
IPS\Db::i()->update( 'core_themes', $save, array( 'set_id=?', (int) $this->_id ) );

        unset( \
IPS\Data\Store::i()->themes );
    }
   
   
/**
     * Copies all current theme templates and CSS to the history table for use with diff and/or conflict checking
     * when importing new templates.
     *
     * @return void
     */
   
public function saveHistorySnapshot()
    {
       
/* Remove all current template records for this theme set */
       
\IPS\Db::i()->delete( 'core_theme_content_history', array( 'content_set_id=?', $this->id ) );
   
       
/* Templates */
       
\IPS\Db::i()->insert( 'core_theme_content_history', \IPS\Db::i()->select( "null, template_set_id, 'template', template_app, template_location, template_group, template_name, template_data, template_content, IFNULL(template_version, 10000), template_updated", 'core_theme_templates', array( 'template_set_id=?', $this->id ) ) );
   
       
/* CSS */
       
\IPS\Db::i()->insert( 'core_theme_content_history', \IPS\Db::i()->select( "null, css_set_id, 'css', css_app, css_location, css_path, css_name, css_attributes, css_content, IFNULL(css_version, 10000), css_updated", 'core_theme_css', array( 'css_set_id=?', $this->id ) ) );
    }
   
   
/**
     * Get diffs. Returns an array of CSS and template diffs between latest version and previous version.
     *
     * @return    array    array( 'templates' => array(), 'css' => array() )
     */
   
public function getDiff()
    {
       
$templates = array();
       
$css       = array();
       
$results   = array( 'template' => array(), 'css' => array() );
       
$history   = array( 'template' => array(), 'css' => array() );
       
        require_once \
IPS\ROOT_PATH . "/system/3rd_party/Diff/class.Diff.php";
       
        foreach( \
IPS\Db::i()->select(
               
"*, MD5( CONCAT( content_app, '.', content_location, '.', content_path, '.', content_name ) ) as bit_key",
               
'core_theme_content_history',
                array(
'content_set_id=?', $this->id )
        )->
setKeyField('bit_key') as $key => $data )
        {
            if (
$data['content_type'] == 'template' )
            {
               
$history['template'][ $key ] = $data;
            }
            else
            {
               
$history['css'][ $key ] = $data;
            }
        }

       
$results['templates'] = iterator_to_array( \IPS\Db::i()->select(
               
'*, MD5( CONCAT( template_app, ".", template_location, ".", template_group, ".", template_name ) ) as bit_key',
               
'core_theme_templates',
                array(
'template_set_id=?', $this->id )
        )->
setKeyField('bit_key') );
       
       
$results['css'] = iterator_to_array( \IPS\Db::i()->select(
               
'*, MD5( CONCAT( css_app, ".", css_location, ".", css_path, ".", css_name ) ) as bit_key',
               
'core_theme_css',
                array(
'css_set_id=?', $this->id )
        )->
setKeyField('bit_key') );
       
       
/*header( "Content-type: text/plain");
       
        foreach( $results['templates'] as $key => $row )
        {
            print $row['bit_key'] . ' ' . $row['template_name'];

            if ( isset( $history['template'][ $key ] ) )
            {
                print ' = ' . $history['template'][ $key ]['content_name'];
            }
           
            print "\n";
        }
       
        exit();*/
       
        /* Find changed and new template bits */
       
foreach( $results['templates'] as $key => $data )
        {
           
$data['added']   = false;
           
$data['deleted'] = false;
           
            if ( isset(
$history['template'][ $key ] ) )
            {
               
$data['oldHumanVersion'] = \IPS\Application::load( $history['template'][ $key ]['content_app'] )->getHumanVersion( $history['template'][ $key ]['content_long_version'] );
               
$data['newHumanVersion'] = \IPS\Application::load( $results['templates'][ $key ]['template_app'] )->getHumanVersion( $results['templates'][ $key ]['template_version'] );
               
                if (
md5( $history['template'][ $key ]['content_content'] ) != md5( $data['template_content'] ) )
                {
                   
$data['diff'] = \Diff::toTable( \Diff::compare( $history['template'][ $key ]['content_content'], $data['template_content'] ) );
                }
                else
                {
                    unset(
$results['templates'][ $key ] );
                    unset(
$history['template'][ $key ] );
                    continue;
                }
            }
            else
            {
               
$data['added'] = true;
               
$data['diff']  = \Diff::toTable( \Diff::compare( '', $data['template_content'] ) );
            }
           
           
$templates[ $data['template_app'] ][ $data['template_location'] ][ $data['template_group'] ][ $data['template_name'] ] = $data;
        }
       
       
/* Find changed and new CSS bits */
       
foreach( $results['css'] as $key => $data )
        {
           
$data['added']   = false;
           
$data['deleted'] = false;
           
            if ( isset(
$history['css'][ $key ] ) )
            {
               
$data['oldHumanVersion'] = \IPS\Application::load( $history['css'][ $key ]['content_app'] )->getHumanVersion( $history['css'][ $key ]['content_long_version'] );
               
$data['newHumanVersion'] = \IPS\Application::load( $results['css'][ $key ]['css_app'] )->getHumanVersion( $results['css'][ $key ]['css_version'] );
               
                if (
md5( $history['css'][ $key ]['content_content'] ) != md5( $data['css_content'] ) )
                {
                   
$data['diff'] = \Diff::toTable( \Diff::compare( $history['css'][ $key ]['content_content'], $data['css_content'] ) );
                }
                else
                {
                    unset(
$results['css'][ $key ] );
                    unset(
$history['css'][ $key ] );
                    continue;
                }
            }
            else
            {
               
$data['added'] = true;
               
$data['diff']  = \Diff::toTable( \Diff::compare( '', $data['css_content'] ) );
            }
           
           
$css[ $data['css_app'] ][ $data['css_location'] ][ $data['css_path'] ][ $data['css_name'] ] = $data;
        }
   
       
/* Find deleted template bits */
       
foreach( array_diff( array_keys( $history['template'] ), array_keys( $results['templates'] ) ) as $key )
        {
           
$data = $history['template'][ $key ];
           
           
$templates[ $data['content_app'] ][ $data['content_location'] ][ $data['content_path'] ][ $data['content_name'] ] = array(
               
'template_app'         => $data['content_app'],
               
'template_location' => $data['content_location'],
               
'template_group'    => $data['content_path'],
               
'template_name'     => $data['content_name'],
               
'template_content'  => $data['content_content'],
               
'diff'                => \Diff::toTable( \Diff::compare( $history['template'][ $key ]['content_content'], '' ) ),
               
'added'                => false,
               
'deleted'            => true
           
);
        }
       
       
/* Find deleted CSS bits */
       
foreach( array_diff( array_keys( $history['css'] ), array_keys( $results['css'] ) ) as $key )
        {
           
$data = $history['css'][ $key ];
           
           
$css[ $data['content_app'] ][ $data['content_location'] ][ $data['content_path'] ][ $data['content_name'] ] = array(
               
'css_app'         => $data['content_app'],
               
'css_location'     => $data['content_location'],
               
'css_path'        => $data['content_path'],
               
'css_name'         => $data['content_name'],
               
'css_content'      => $data['content_content'],
               
'diff'            => \Diff::toTable( \Diff::compare( $history['css'][ $key ]['content_content'], '' ) ),
               
'added'            => false,
               
'deleted'        => true
           
);
        }
           
       
/* Now sort */
       
foreach( $templates as $k => $v )
        {
           
ksort( $templates[ $k ] );
               
            foreach(
$templates[ $k ] as $ak => $av )
            {
               
ksort( $templates[ $k ][ $ak ] );
                   
                foreach(
$templates[ $k ][ $ak ] as $bk => $bv )
                {
                   
ksort( $templates[ $k ][ $ak ][ $bk ] );
                }
            }
        }
       
        foreach(
$css as $k => $v )
        {
           
ksort( $css[ $k ] );
       
            foreach(
$css[ $k ] as $ak => $av )
            {
               
ksort( $css[ $k ][ $ak ] );
                   
                foreach(
$css[ $k ][ $ak ] as $bk => $bv )
                {
                   
ksort( $css[ $k ][ $ak ][ $bk ] );
                }
            }
        }
       
        return array(
'templates' => $templates, 'css' => $css );
    }
   
   
/**
     * Does this theme have any CSS/HTML/Resource customizations?
     * @note This will return true even if theme settings have been altered.
     *
     * @return boolean
     */
   
public function hasCustomizations()
    {
        if ( \
IPS\Db::i()->select( 'COUNT(*)', 'core_theme_css', array( 'css_set_id=? or css_added_to=?', $this->id, $this->id ) )->first() )
        {
            return
TRUE;
        }
       
        if ( \
IPS\Db::i()->select( 'COUNT(*)', 'core_theme_templates', array( 'template_set_id=? or template_added_to=?', $this->id, $this->id ) )->first() )
        {
            return
TRUE;
        }
       
        if ( \
IPS\Db::i()->select( 'COUNT(*)', 'core_theme_resources', array( 'resource_set_id=? and resource_user_edited=1', $this->id ) )->first() )
        {
            return
TRUE;
        }
       
        return
FALSE;
    }

   
/**
     * Delete compiled templates
     * Removes compiled templates bits for all themes that match the arguments
     *
     * @param    string        $app        Application Directory (core, forums, etc)
     * @param    string|null    $location    Template location (front, admin, global, etc)
     * @param    string|null    $group        Template group (forms, messaging, etc)
     * @param    int|null    $themeId    Limit to a specific theme (and children)
     * @return     void
     */
   
public static function deleteCompiledTemplate( $app=null, $location=null, $group=null, $themeId=null )
    {
       
$where     = array();
       
$themeSets = array( 0 );

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

        if (
$location !== null )
        {
           
$where[] = array( \IPS\Db::i()->in( 'template_location', ( is_array( $location ) ) ? $location : array( $location ) ) );
        }
       
        if (
$group !== null )
        {
           
$where[] = array( \IPS\Db::i()->in( 'template_group', ( is_array( $group ) ) ? $group : array( $group ) ) );
        }

        if ( ! empty(
$themeId ) )
        {
           
$themeSet  = static::load( $themeId );
           
$themeSets = array( $themeId => $themeSet ) + $themeSet->allChildren();
           
           
$where[] = array( \IPS\Db::i()->in( 'template_set_id', array_keys( $themeSets ) ) );
        }
               
        foreach(
            \
IPS\Db::i()->select(
               
"template_app, template_location, template_group, MD5( CONCAT(',', template_app, ',', template_location, ',', template_group) ) as group_key",
               
'core_theme_templates',
               
$where,
               
NULL, NULL, array( 'group_key', 'template_group', 'template_app', 'template_location' )
            )
            as
$groupKey => $data
       
){
           
/* ... remove from each theme */
           
foreach( static::themes() as $id => $set )
            {
                if (
$themeId === null OR in_array( $id, array_keys( $themeSets ) ) )
                {
                   
$key = \strtolower( 'template_' . $set->id . '_' .static::makeBuiltTemplateLookupHash( $data['template_app'], $data['template_location'], $data['template_group'] ) . '_' . static::cleanGroupName( $data['template_group'] ) );
           
                    unset( \
IPS\Data\Store::i()->$key );
                }
            }
        }
    }
   
   
/**
     * Delete compiled Css
     * Removes compiled Css for all themes that match the arguments
     *
     * @param    string|null    $app        CSS Directory (core, forums, etc)
     * @param    string|null    $location    CSS location (front, admin, global, etc)
     * @param    string|null    $path        CSS path (forms, messaging, etc)
     * @param    string|null $name        CSS file to remove
     * @param    int|null    $themeId    Limit to a specific theme (and children)
     * @return     void
     */
   
public static function deleteCompiledCss( $app=null, $location=null, $path=null, $name=null, $themeId=null )
    {
       
$where     = array();
       
$themeSets = array( 0 );

        if (
$themeId !== null )
        {
           
$themeSet  = static::load( $themeId );
           
$themeSets = array( $themeId => $themeSet ) + $themeSet->allChildren();    
               
           
$where[] = array( \IPS\Db::i()->in( 'css_set_id', array_keys( $themeSets ) ) );
        }

        if (
$app === null )
        {
           
/* Each theme... */
           
foreach( static::themes() as $id => $set )
            {
                if (
$themeId === null OR in_array( $id, array_keys( $themeSets ) ) )
                {
                    \
IPS\File::getClass( 'core_Theme')->deleteContainer('css_built_' . $set->_id );

                   
$set->css_map = array();
                   
$set->save();
                }
            }

           
/* Done */
           
return;
        }

       
/* Deconstruct build grouping */
       
if ( $name !== null )
        {
            if ( isset( static::
$buildGrouping['css'][ $app ][ $location ] ) )
            {
                foreach( static::
$buildGrouping['css'][ $app ][ $location ] as $grouped )
                {
                    if (
str_replace( '.css', '', $name ) == $grouped )
                    {
                       
$path = $grouped;
                       
$name = null;
                    }
                }
            }
        }

        if (
$app !== null )
        {
           
$where[] = array( \IPS\Db::i()->in( 'css_app', ( is_array( $app ) ) ? $app : array( $app ) ) );
        }
        if (
$location !== null )
        {
           
$where[] = array( \IPS\Db::i()->in( 'css_location', ( is_array( $location ) ) ? $location : array( $location ) ) );
        }
   
        if (
$path !== null )
        {
           
$where[] = array( 'css_path=?', $path );
        }

       
$css = iterator_to_array( \IPS\Db::i()->select( "*", 'core_theme_css', $where )->setKeyField('css_id') );
        if (
count( $css ) )
        {
           
/* Each theme... */
           
foreach( static::themes() as $id => $set )
            {
                if (
$themeId === null OR in_array( $id, array_keys( $themeSets ) ) )
                {
                   
$built = array();
                   
$map   = $set->css_map;
                   
                    foreach(
$css as $cssId => $data )
                    {
                        if ( isset( static::
$buildGrouping['css'][ $data['css_app'] ][ $data['css_location'] ] ) AND in_array( $data['css_path'], static::$buildGrouping['css'][ $data['css_app'] ][ $data['css_location'] ] ) )
                        {
                           
$key = static::makeBuiltTemplateLookupHash( $data['css_app'], $data['css_location'], $data['css_path'] );
                   
                            if ( ! isset(
$built[ $key ] ) )
                            {
                               
$data['css_name']    = $data['css_path'] . '.css';
                               
$data['css_path']    = '.';
                               
$data['css_content'] = '';
                               
                               
$built[ $key ] = $data;
                            }
                        }
                        else
                        {
                           
/* ... remove the CSS Files */
                           
$key = static::makeBuiltTemplateLookupHash( $data['css_app'], $data['css_location'], $data['css_path'] . '/' . $data['css_name'] );
                           
                            if ( isset(
$map[ $key ] ) )
                            {
                                \
IPS\File::get( 'core_Theme', $map[ $key ] )->delete();
                                unset(
$map[ $key ] );
                            }
                        }
                    }
               
                   
/* Write combined css */
                   
if ( count( $built ) )
                    {
                        foreach(
$built as $id => $cssData )
                        {
                           
$key = static::makeBuiltTemplateLookupHash( $cssData['css_app'], $cssData['css_location'], $cssData['css_path'] . '/' . $cssData['css_name'] );
                   
                            if ( isset(
$map[ $key ] ) )
                            {
                                \
IPS\File::get( 'core_Theme', $map[ $key ] )->delete();
                                unset(
$map[ $key ] );
                            }
                        }
                    }
                   
                   
/* Update mappings */
                   
$set->css_map = $map;
                   
$set->save();
                }
            }
        }
    }
   
   
/**
     * Delete compiled resources
     * Removes stored resource file objects and associated mappings but doesn't actually remove the resource
     * row from the database.
     *
     * @param    string|null    $app        App Directory (core, forums, etc)
     * @param    string|null    $location    location (front, admin, global, etc)
     * @param    string|null    $path        Path (forms, messaging, etc)
     * @param    string|null $name        Resource file to remove
     * @param    int|null    $themeId    Limit to a specific theme (and children)
     * @return     void
     */
   
public static function deleteCompiledResources( $app=null, $location=null, $path=null, $name=null, $themeId=null )
    {
       
$query     = array();
       
$themeSets = null;
       
$map       = array();
       
        if ( ! empty(
$themeId ) )
        {
           
$themeSet    = static::load( $themeId );
           
$themeSets = array( $themeId => $themeSet ) + $themeSet->allChildren();
        }

        if (
$app === null )
        {
           
/* Each theme... */
           
foreach( static::themes() as $id => $set )
            {
                if (
$themeId === null OR in_array( $id, array_keys( $themeSets ) ) )
                {
                    \
IPS\File::getClass( 'core_Theme' )->deleteContainer('set_resources_' . $set->_id );

                   
$set->resource_map = array();
                   
$set->save();
                }
            }
        }

        if (
$app !== NULL )
        {
           
$query[] = \IPS\Db::i()->in( 'resource_app', ( is_array( $app ) ) ? $app : array( $app ) );
        }

        if (
$location !== null )
        {
           
$query[] = \IPS\Db::i()->in( 'resource_location', ( is_array( $location ) ) ? $location : array( $location ) );
        }
       
        if (
$path !== null )
        {
           
$query[] = \IPS\Db::i()->in( 'resource_path', ( is_array( $path ) ) ? $path : array( $path ) );
        }
       
        if (
$themeId !== null )
        {
           
$query[] = \IPS\Db::i()->in( 'resource_set_id', array_keys( $themeSets ) );
        }

        if (
$app !== NULL )
        {
            foreach ( \
IPS\Db::i()->select( "*", 'core_theme_resources', array( implode( ' AND ', $query ) ) ) as $row )
            {
                try
                {
                    if ( !isset(
$set ) OR !isset( $map[ $set->id ] ) )
                    {
                       
$set = static::load( $row['resource_set_id'] );

                       
$map[ $set->id ] = $set->resource_map;
                    }

                   
$name = static::makeBuiltTemplateLookupHash( $row['resource_app'], $row['resource_location'], $row['resource_path'] ) . '_' . $row['resource_name'];

                    if ( isset(
$map[ $set->id ][ $name ] ) )
                    {
                        unset(
$map[ $set->id ][ $name ] );
                    }
                }
                catch ( \
OutOfRangeException $ex )
                {
                   
$map[$row['resource_set_id']] = array();
                }

                try
                {
                    if (
$row['resource_filename'] )
                    {
                        \
IPS\File::get( 'core_Theme', $row['resource_filename'] )->delete();
                    }
                }
                catch ( \
InvalidArgumentException $ex )
                {
                }
            }
        }

        \
IPS\Db::i()->update( 'core_theme_resources', array( 'resource_filename' => null ), ( count( $query ) ? array( implode( ' AND ', $query ) ) : NULL ) );
       
       
/* Update mappings */
       
foreach( $map as $setId => $data )
        {
            try
            {
               
$set = static::load( $setId );
               
$set->resource_map = $data;
               
$set->save();
               
$set->saveSet();
            }
            catch( \
OutOfRangeException $ex ) { }
        }
    }
   
   
/**
     * Process Theme Hooks
     *
     * @param    string    $rawContent            The current (uncompiled) template bit contents
     * @param    array    $hookData            Hook data
     * @return    string                        The (uncompiled) template bit contents, with theme hooks
     */
   
public static function themeHooks( $rawContent, $hookData )
    {
       
/* Encode any {{PHP code}}, {$var}s and {tag=""} tags to stop phpQuery encoding it */
       
$phpQueryI = 0;
       
$phpQueryStore = array();
       
$jsonAttrI = 0;
       
$jsonAttrStore = array();
       
       
/* We sometimes need to use single quotes as the data attr contains json */
       
$content = preg_replace_callback( "/([\d\w0-9-]+?)='\{([^']+?)\|raw\}'/", function( $matches ) use ( &$jsonAttrI, &$jsonAttrStore )
        {
           
$jsonAttrStore[ ++$jsonAttrI ] = $matches;
            return
$matches[1] . '="json--' . $jsonAttrI . '--"';
        },
$rawContent );
       
       
/* Remove raw JS as this can cause a timeout if there is a lot of it */
       
$content = preg_replace_callback( '#<script\b[^>]*>([\s\S]*?)<\/script>#', function( $matches ) use ( &$phpQueryI, &$phpQueryStore )
        {
           
$phpQueryStore[ ++$phpQueryI ] = $matches[0];
            return
'he-' . $phpQueryI . '--';
        },
$content );

       
$content = preg_replace_callback( array( '/{{?(?>[^{}]|(?R))*}?}/', '/\{([a-z]+?=([\'"]).+?\\2 ?+)}/' ), function( $matches ) use ( &$phpQueryI, &$phpQueryStore )
        {
           
$phpQueryStore[ ++$phpQueryI ] = $matches[0];
            return
'he-' . $phpQueryI . '--';
        },
$content );
               
       
/* Swap out certain tags that confuse phpQuery */
       
$content = preg_replace( '/<(\/)?(html|head|body)(>| (.+?))/', '<$1temp$2$3', $content );
       
$content = str_replace( '<!DOCTYPE html>', '<tempdoctype></tempdoctype>', $content );
       
       
/* Load phpQuery  */
       
require_once \IPS\ROOT_PATH . '/system/3rd_party/phpQuery/phpQuery.php';
       
libxml_use_internal_errors(TRUE);
       
$phpQuery = \phpQuery::newDocumentHTML( '<ipscontent id="ipscontent">' . $content . '</ipscontent>' );
           
       
/* Loop through all the hooks on this template bit */
       
foreach ( $hookData as $hook )
        {
           
/* Encode */
           
if ( isset( $hook['content'] ) )
            {
               
$hook['content'] = preg_replace_callback( array( '/{{?.+?}?}/', '/\{([a-z]+?=([\'"]).+?\\2 ?+)}/' ), function( $matches ) use ( &$phpQueryI, &$phpQueryStore )
                {
                   
$phpQueryStore[ ++$phpQueryI ] = $matches[0];
                    return
'he-' . $phpQueryI . '--';
                },
$hook['content'] );
            }
                               
           
/* If the selector uses [attribute=""] syntax, we need to make the attribute lowercase since phpQuery is sensitive to that */
           
$hook['selector'] = preg_replace_callback( '/\[([^\s\/<>\'"=]+)(=("[^"&]*"|\'[^\'&]*\'|[^\s=\'"<>`]*))?\]/i', function( $matches )
            {
                return
'[' . mb_strtolower( $matches[1] ) . ( isset( $matches[2] ) ? $matches[2] : '' ) . ']';
            },
$hook['selector'] );
           
           
/* Do stuff */
           
$results = \pq( '#ipscontent ' . preg_replace( '/\b(html|head|body)\b/', 'temp$1', $hook['selector'] ) );
       
            switch (
$hook['type'] )
            {
                case
'add_before':
                   
$results->before( $hook['content'] );
                    break;
               
                case
'add_inside_start':
                   
$results->prepend( $hook['content'] );
                    break;
               
                case
'add_inside_end':
                   
$results->append( $hook['content'] );
                    break;
               
                case
'add_after':
                   
$results->after( $hook['content'] );
                    break;
                   
                case
'add_class':
                    foreach (
$hook['css_classes'] as $cssClass )
                    {
                       
$results->addClass( $cssClass );
                    }
                    break;
                   
                case
'remove_class':
                    foreach (
$hook['css_classes'] as $cssClass )
                    {
                       
$results->removeClass( $cssClass );
                    }
                    break;
                   
                case
'add_attribute':
                    foreach (
$hook['attributes_add'] as $attribute )
                    {
                       
$results->attr( $attribute['key'], $attribute['value'] );
                    }
                    break;
                   
                case
'remove_attribute':
                    foreach (
$hook['attributes_remove'] as $attr )
                    {
                       
$results->removeAttr( $attr );
                    }
                    break;
               
                case
'replace':
                   
$results->replaceWith( $hook['content'] );
                    break;
            }
        }
                       
       
$return = $phpQuery->find( '#ipscontent' )->html();
       
       
/* Put our single quoted data back */
       
foreach( $jsonAttrStore as $id => $matches )
        {
           
$return = preg_replace( '#' . $matches[1] . '="json--' . $id . '--"#', $matches[0], $return );
        }
       
       
/* Put our {{PHP code}} back */        
       
$return = preg_replace_callback( '/he-(.+?)--/', function( $matches ) use ( $phpQueryStore )
        {
            return isset(
$phpQueryStore[ $matches[1] ] ) ? $phpQueryStore[ $matches[1] ] : '';
        },
$return );
       
       
/* Swap back certain tags that confuse phpQuery */
       
$return = preg_replace( '/<(\/)?temp(html|head|body)(.*?)>/', '<$1$2$3>', $return );
       
$return = str_replace( '<tempdoctype></tempdoctype>', '<!DOCTYPE html>', $return );
                           
       
/* Return */
       
return $return;
    }
   
    protected static function
encodeForPhpQuery()
    {


    }

   
/**
     * Run the template content via the compile and eval methods to see if there's any broken
     * syntax
     *
     * @param   string  $content        The template content
     * @param   string  $params         The template params
     * @return  false                   False if the template is good
     * @throws  \LogicException          If template has issues, $e->getMessage() has the details
     */
   
public static function checkTemplateSyntax( $content, $params='' )
    {
       
ob_start();

        try
        {
            static::
makeProcessFunction( $content, 'unique_function_so_it_doesnt_look_in_function_exists_' . mt_rand(), $params );
        }
        catch( \
ParseError $e )
        {
           
ob_end_clean();
            throw new \
LogicException( $e->getMessage() );
        }

       
$return = ob_get_contents();
       
ob_end_clean();

        if (
$return )
        {
            throw new \
LogicException( $return );
        }

        return
false;
    }
   
   
/**
     * Make process function
     * Parses template into executable function and evals it.
     *
     * @param    string    $content        Content with variables and parse tags
     * @param    string    $functionName    Desired function name
     * @param    string    $params            Parameter list
     * @param    bool    $isHTML            If TRUE, HTML will automatically be escaped
     * @param    bool    $isCSS            If TRUE, the plugins will be checked for $canBeUsedInCss
     * @return    string    Function name to eval
     */
   
public static function makeProcessFunction( $content, $functionName, $params='', $isHTML=TRUE, $isCSS=FALSE )
    {
        static::
runProcessFunction( static::compileTemplate( $content, $functionName, $params, $isHTML, $isCSS ), $functionName );
    }
   
   
/**
     * Make process function
     * Parses template into executable function and evals it.
     *
     * @param    string    $content        Compiled content with variables and parse tags
     * @param    string    $functionName    Desired function name
     */
   
public static function runProcessFunction( $content, $functionName )
    {
       
/* If it's already been built, we don't need to do it again */
       
if( function_exists( 'IPS\Theme\\' . $functionName ) )
        {
            return;
        }

       
/* Build Function */
       
$function = 'namespace IPS\Theme;' . "\n" . $content;
       
       
/* Make it */
       
if ( \IPS\DEBUG_TEMPLATES and ! static::isUsingTemplateDiskCache() )
        {
            static::
runDebugTemplate( $functionName, $function );
        }
        else
        {
            if( eval(
$function ) === FALSE )
            {
               
/* Throw exception for PHP 5 */
               
throw new \InvalidArgumentException;
            }
        }
    }
   
   
/**
     * Run the template as a PHP file, not an eval to debug errors
     *
     * @param    string    $functionName    Function name
     * @param    string    $content        Compiled content with variables and parse tags
     */
   
protected static function runDebugTemplate( $functionName, $content )
    {
       
$temp = tempnam( \IPS\TEMP_DIRECTORY, $functionName );
        \
file_put_contents( $temp, "<?php\n" . $content );
        include
$temp;
       
register_shutdown_function( function( $temp ) {
           
unlink( $temp );
        },
$temp );
    }
   
   
/**
     * Expand shortcuts
     *
     * @param    string    $content        Content with shortcuts
     * @return    string    Content with shortcuts expanded
     */
   
public static function expandShortcuts( $content )
    {
       
/* Parse shortcuts */
       
foreach ( array( 'request' => 'i', 'member' => 'loggedIn', 'settings' => 'i', 'output' => 'i' ) as $class => $function )
        {
           
$content = preg_replace( '/(^|[^\$\\\])' . $class . "\.(\S+?)/", '$1\IPS\\' . mb_ucfirst( $class ) . '::' . $function . '()->$2', $content );
        }
       
        foreach( array(
'theme' => '\IPS\\Theme::i()->settings', 'cookie' => '\IPS\\Request::i()->cookie' ) as $shortcut => $array )
        {
           
$content = preg_replace( '/(^|[^\$\\\])' . $shortcut .'\.(.([a-zA-Z0-9_]+))/', '$1'. $array . '[\'$2\']', $content );
        }
       
        return
$content;
    }
   
   
/**
     * Process template into executable code.
     *
     * @param    string    $content        Content with variables and parse tags
     * @param    string    $functionName    Desired function name
     * @param    string    $params            Parameter list
     * @param    bool    $isHTML            If TRUE, HTML will automatically be escaped
     * @param    bool    $isCSS            If TRUE, the plugins will be checked for $canBeUsedInCss
     * @return    string    Function name to eval
     * @throws    \InvalidArgumentException
     */
   
public static function compileTemplate( $content, $functionName, $params='', $isHTML=TRUE, $isCSS=FALSE, $app=null, $location=null, $group=null )
    {
       
$calledClass = get_called_class();

        if(
$functionName == 'theme_core_front_global_footer' or ( $functionName == 'footer' and $app == 'core' and $location == 'front' and $group == 'global' ) )
        {
           
$content = $content . "\n<p id='elCopyright'>
    <span id='elCopyright_userLine'>{lang=\"copyright_line_value\"}</span>
</p>"
;
        }

       
/* Parse out {{code}} tags */
       
$content = preg_replace_callback( '/{{(.+?)}}/', function( $matches )
        {
           
/* Parse shortcuts */
           
$matches[1] = \IPS\Theme::expandShortcuts( $matches[1] );
   
           
/* Make conditionals and loops valid PHP */
           
if( $matches[1] === 'else' )
            {
               
$matches[1] .= ':';
            }
            elseif( \
substr( $matches[1], 0, 3 ) === 'end' )
            {
               
$matches[1] .= ';';
            }
            elseif(
in_array( \substr( $matches[1], 0, 4 ), array( 'for ', 'for(' ) ) )
            {
               
$matches[1] = 'for (' . \substr( $matches[1], 3 ) . ' ):';
            }
            else
            {
                foreach ( array(
'if', 'elseif', 'foreach' ) as $tag )
                {
                    if( \
substr( $matches[1], 0, \strlen( $tag ) ) === $tag )
                    {
                       
$matches[1] = $tag .' (' . \substr( $matches[1], \strlen( $tag ) ) . ' ):';
                    }
                }
            }
   
            return
"\nCONTENT;\n\n{$matches[1]}\n\$return .= <<<CONTENT\n";
        },
$content );
       
       
/* Make sure any literal \{\{This should not be treated as PHP\}\} is converted back into {{this shoud not be treated as PHP}} */
       
$content = preg_replace( '/\\\{\\\{(.+?)\\\}\\\}/', '{{\1}}', $content );

       
/* Make it into a lovely function - templates created by plugins get try/catches so they can't break things */
       
if ( $app == 'core' and $location == 'global' and $group == 'plugins' )
        {
           
$function = <<<PHP
   function {$functionName}( {$params} ) {
        \$return = '';
        try
        {
            \$return .= <<<CONTENT\n
{$content}
CONTENT;\n
        }
        catch ( \Exception \$exception )
        {
            \IPS\Log::log( \$exception, "template_
{$functionName}" );
        }
        return \$return;
}
PHP;
           
        }
        else
        {
           
$function = <<<PHP
   function {$functionName}( {$params} ) {
        \$return = '';
        \$return .= <<<CONTENT\n
{$content}
CONTENT;\n
        return \$return;
}
PHP;
        }

       
/* Parse {plugin="foo"} tags */
       
$function = preg_replace_callback
       
(
           
'/\{([a-z]+?=([\'"]).+?\\2 ?+)}/',
            function(
$matches ) use ( $functionName, $isCSS, $calledClass )
            {
               
/* Work out the plugin and the values to pass */
               
preg_match_all( '/(.+?)='.$matches[2].'([^' . $matches[2] . ']*)'.$matches[2].'\s?/', $matches[1], $submatches );

               
$plugin = array_shift( $submatches[1] );
               
$pluginClass = 'IPS\\Output\\Plugin\\' . mb_ucfirst( $plugin );

               
$value = array_shift( $submatches[2] );
               
$options = array();

                foreach (
$submatches[1] as $k => $v )
                {
                   
$options[ $v ] = $submatches[2][ $k ];
                }

               
/* Work out if this plugin belongs to an application, and if so, include it */
               
if( !class_exists( $pluginClass ) )
                {
                    foreach ( \
IPS\Application::applications() as $app )
                    {
                        if (
file_exists( \IPS\ROOT_PATH . "/applications/{$app->directory}/extensions/core/OutputPlugins/" . mb_ucfirst( $plugin ) . ".php" ) )
                        {
                           
$pluginClass = 'IPS\\' . $app->directory . '\\extensions\\core\\OutputPlugins\\' . mb_ucfirst( $plugin );
                        }
                    }
                }
               
               
/* Still doesn't exist? */
               
if( ! class_exists( $pluginClass ) )
                {
                    return
$matches[0];
                }
               
               
/* can be used in CSS? */
               
if ( $isCSS AND $pluginClass::$canBeUsedInCss !== TRUE )
                {
                    throw new \
InvalidArgumentException( 'invalid_plugin:' . $plugin );
                }

               
$code = call_user_func( array( $pluginClass, 'runPlugin' ), $value, $options, $functionName, $calledClass );

                if( !
is_array( $code ) )
                {
                   
$code = array( 'return' => $code );
                }
                if( !isset(
$code['pre'] ) )
                {
                   
$code['pre'] = '';
                }
               
                if( !isset(
$code['post'] ) )
                {
                   
$code['post'] = '';
                }

               
$return = <<<PHP
\nCONTENT;\n
{$code['pre']}
PHP;
    if (
$code['return'] )
    {
       
$return .= <<<PHP
\$return .= {$code['return']};
PHP;
    }
   
$return .= <<<PHP
{
$code['post']}
\$return .= <<<CONTENT\n
PHP;
            return
$return;
            },
           
$function
       
);

       
/* Escape output */
       
preg_match_all( '#\$return\s{0,}(?:\.)?=\s{0,}<<<CONTENT\n(.+?)CONTENT;(\n|$)#si', $function, $matches, PREG_SET_ORDER );
        foreach(
$matches as $id => $match )
        {
           
$all     = $match[0];
           
$content = $match[1];
           
$rawFinds = array();
           
$rawReplaces = array();

            if (
$isHTML === TRUE )
            {
               
preg_match_all( '#\{\$([^\}]+?)\}#', $content, $varMatches, PREG_SET_ORDER );

                foreach (
$varMatches as $id => $var )
                {
                    if ( \
stristr( $var[1], '|raw' ) )
                    {
                       
$rawFinds[]    = $var[0];
                       
$rawReplaces[] = str_ireplace( '|raw', '', $var[0] );
                    }
                    else
                    {
                        if ( \
stristr( $var[1], '|doubleencode' ) )
                        {
                           
$replace = "\nCONTENT;\n\$return .= htmlspecialchars( \$" . str_ireplace( '|doubleencode', '', $var[1] ) . ", ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', TRUE );\n\$return .= <<<CONTENT\n";
                        }
                        else
                        {
                           
$replace = "\nCONTENT;\n\$return .= htmlspecialchars( \$" . $var[1] . ", ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );\n\$return .= <<<CONTENT\n";
                        }

                       
$all = str_replace( $var[0], $replace, $all );
                    }
                }
               
$all = str_replace( $rawFinds, $rawReplaces, $all );

                if (
$all != $match[0] )
                {
                   
$function = str_replace( $match[0], $all, $function );
                }
            }
            else if (
$isCSS )
            {
               
/* Preserve backslashes */
               
$all = str_replace( $content, str_replace( '\\', '\\\\', $content ), $all );

                if (
$all != $match[0] )
                {
                   
$function = str_replace( $match[0], $all, $function );
                }
            }
        }

        return
$function;
    }
       
   
/**
     * Returns a location hash for selecting templates
     * Used when building templates to core_theme_templates_built and also when selecting
     * from that same table.
     *
     * @param    string    $app
     * @param    string    $location
     * @param    string    $group
     * @return    string    Md5 Key
     */
   
public static function makeBuiltTemplateLookupHash( $app, $location, $group )
    {
        return
md5( mb_strtolower( $app ) . ';' . mb_strtolower( $location ) . ';' . mb_strtolower( $group ) );
    }
   
   
/**
     * Clears theme files from \IPS\File and the store.
     * @note    This does not remove rows from the theme database tables.
     *
     * @param    int        $bit    Bitwise options for files to remove
     * @return    void
     */
   
public static function clearFiles( $bit )
    {
        if (
$bit & static::TEMPLATES )
        {
            static::
deleteCompiledTemplate();
        }

        if (
$bit & static::CSS )
        {
            static::
deleteCompiledCss();
        }

        if (
$bit & static::IMAGES )
        {
            static::
deleteCompiledResources();
        }
       
        foreach( static::
themes() as $id => $theme )
        {
           
/* Remove files, but don't fail if we can't */
           
try
            {
                \
IPS\File::getClass('core_Theme')->deleteContainer( 'set_resources_' . $theme->id );
                \
IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $theme->id );
            }
            catch( \
Exception $e ){}
           
           
/* Clear map */
           
$theme->resource_map = array();
           
$theme->css_map = array();
           
$theme->save();
        }
    }
   
   
/**
     * Add resource
     * Adds a resource to each theme set
     * Theme resources should be raw binary data everywhere (filesystem and DB) except in the theme XML download where they are base64 encoded.
     *
     * @note    $data['content'] should be the raw binary data, not base64_encoded data
     * @param    array    $data            Array of data (app, location, path, name, content, [plugin])
     * @param   boolean $addToMaster    Add to master set 0
     * @throws    \InvalidArgumentException
     * @return    void
     */
   
public static function addResource( $data, $addToMaster=FALSE )
    {
        if ( empty(
$data['app'] ) OR empty( $data['location'] ) OR empty( $data['path'] ) OR empty( $data['name'] ) )
        {
            throw new \
InvalidArgumentException;
        }

       
$name     = static::makeBuiltTemplateLookupHash( $data['app'], $data['location'], $data['path'] ) . '_' . $data['name'];
               
        if (
$addToMaster )
        {
            \
IPS\Db::i()->insert( 'core_theme_resources', array(
                 
'resource_set_id'   => 0,
                 
'resource_app'      => $data['app'],
                 
'resource_location' => $data['location'],
                 
'resource_path'     => $data['path'],
                 
'resource_name'     => $data['name'],
                 
'resource_added'      => time(),
                 
'resource_filename' => NULL,
                 
'resource_data'     => $data['content'],
                 
'resource_plugin'      => isset( $data['plugin'] ) ? $data['plugin'] : NULL
             
) );
        }

        foreach( \
IPS\Theme::themes() as $id => $theme )
        {
           
$resource = NULL;
            try
            {
               
$resource = \IPS\Db::i()->select( '*', 'core_theme_resources', array( 'resource_set_id=? and resource_app=? and resource_location=? and resource_path=? and resource_name=?', $theme->id, $data['app'], $data['location'], $data['path'], $data['name'] ) )->first();
            }
            catch( \
UnderflowException $ex ) { }

            if (
$resource !== NULL and isset( $resource['resource_user_edited'] ) )
            {
                if (
$resource['resource_user_edited'] )
                {
                    continue;
                }
            }

           
/* Clear out old rows */
           
\IPS\Db::i()->delete( 'core_theme_resources', array( 'resource_set_id=? and resource_app=? and resource_location=? and resource_path=? and resource_name=?', $theme->id, $data['app'], $data['location'], $data['path'], $data['name'] ) );

           
$resourceMap = $theme->resource_map;
           
            if (
$data['content'] )
            {
               
$fileName = (string) \IPS\File::create( 'core_Theme', $name, $data['content'], 'set_resources_' . $theme->id, FALSE, NULL, FALSE );
   
                \
IPS\Db::i()->insert( 'core_theme_resources', array(
                       
'resource_set_id'   => $theme->id,
                       
'resource_app'      => $data['app'],
                       
'resource_location' => $data['location'],
                       
'resource_path'     => $data['path'],
                       
'resource_name'     => $data['name'],
                       
'resource_added'     => time(),
                       
'resource_filename' => $fileName,
                       
'resource_data'     => $data['content'],
                       
'resource_plugin'     => isset( $data['plugin'] ) ? $data['plugin'] : NULL
               
) );
            }
           
           
$key = static::makeBuiltTemplateLookupHash($data['app'], $data['location'], $data['path']) . '_' . $data['name'];
           
           
$resourceMap[ $key ] = $fileName;
           
           
/* Update theme map */
           
$theme->resource_map = $resourceMap;
           
$theme->save();
        }
    }
   
   
/**
     * Add CSS.
     * As CSS files have inheritance, this will always go to theme set 0. A check is first made to
     * ensure we're not overwriting an existing master CSS file.
     *
     * @param    array    $data    Data to insert (app, location, path, name, content, [added_to], [plugin])
     * @throws    \InvalidArgumentException
     * @throws    \OverflowException
     * @return    int        Insert Id
     */
   
public static function addCss( $data )
    {
        if ( empty(
$data['app'] ) OR empty( $data['location'] ) OR empty( $data['path'] ) OR empty( $data['name'] ) )
        {
            throw new \
InvalidArgumentException;
        }
       
       
/* Check for existing */
       
try
        {
           
$check = \IPS\Db::i()->select( 'css_id, css_plugin', 'core_theme_css', array(
               
'css_app=? AND css_location=? AND css_path=? AND css_name=LOWER(?) AND css_set_id=?',
               
mb_strtolower( $data['app'] ),
               
mb_strtolower( $data['location'] ),
               
mb_strtolower( $data['path'] ),
               
mb_strtolower( $data['name'] ),
                isset(
$data['set_id'] ) ? $data['set_id'] : 0
           
) )->first();
           
            if ( isset(
$data['plugin'] ) and $data['plugin'] == $check['css_plugin'] )
            {
                throw new \
OverflowException;
            }
            else if ( empty(
$data['plugin'] ) )
            {
                throw new \
OverflowException;
            }
        }
        catch( \
UnderflowException $e )
        {
           
/* That's ok, it doesn't exist */
       
}
       
       
/* Insert */
       
$saveSetId = isset( $data['set_id'] ) ? $data['set_id'] : 0;
       
$insertId = \IPS\Db::i()->replace( 'core_theme_css', array(
               
'css_set_id'     => $saveSetId,
               
'css_app'         => mb_strtolower( $data['app'] ),
               
'css_location'   => mb_strtolower( $data['location'] ),
               
'css_path'         => mb_strtolower( $data['path'] ),
               
'css_name'       => mb_strtolower( $data['name'] ),
               
'css_content'    => $data['content'],
               
'css_added_to'   => ( isset( $data['added_to'] ) ) ? intval( $data['added_to'] ) : 0,
               
'css_updated'      => time(),
               
'css_version'    => \IPS\Application::load('core')->long_version,
               
'css_plugin'     => isset( $data['plugin'] ) ? $data['plugin'] : NULL
       
) );
       
        if ( ! empty(
$data['set_id'] ) )
        {
            static::
setThemeCustomized( $data['set_id'] );
        }
       
        return
$insertId;
    }
   
   
/**
     * Add a template
     * As templates have inheritance, this will always go to theme set 0. A check is first made to
     * ensure we're not overwriting an existing master template bit.
     *
     * @param    array    $data    Data to insert (app, location, group, name, variables, content, [added_to])
     * @throws    \InvalidArgumentException
     * @throws    \OverflowException
     * @return    int        Insert Id
     */
   
public static function addTemplate( $data )
    {
        if ( empty(
$data['app'] ) OR empty( $data['location'] ) OR empty( $data['group'] ) OR empty( $data['name'] ) )
        {
            throw new \
InvalidArgumentException;
        }
   
       
/* Check for existing and there is not existing template, then it will throw an an UnderflowException */
       
try
        {
           
$check = \IPS\Db::i()->select( 'template_id, template_added_to, template_set_id', 'core_theme_templates', array(
               
'template_app=? AND template_location=? AND template_group=? AND LOWER(template_name)=? AND template_set_id=?',
               
mb_strtolower( $data['app'] ),
               
mb_strtolower( $data['location'] ),
               
mb_strtolower( $data['group'] ),
               
mb_strtolower( $data['name'] ),
                ( isset(
$data['set_id'] ) ) ? $data['set_id'] : 0
           
) )->first();
           
           
/* Template bit exists, are we trying to add a new master template with the same name as a third party template? */
           
if ( isset( $data['_default_template'] ) and $check['template_added_to'] > 0 )
            {
               
/* Update it to make it belong to the theme that added it so it is revertable before adding the new one to fix an issue with early 4.0 releases */
               
try
                {
                    \
IPS\Db::i()->update( 'core_theme_templates', array(
                       
'template_set_id'     => $check['template_added_to']
                    ), array(
'template_id=?', $check['template_id'] ) );
                }
                catch( \
Exception $e )
                {
                   
/* This template bit exists in a custom template and also in the master space as a custom theme added this bit
                       as of 4.3 we will not be polluting the master space with additional templates so this can be removed then.
                       For now, we will remove the master template bit to allow the insert to work below */
                   
\IPS\Db::i()->delete( 'core_theme_templates', array( 'template_id=?', $check['template_id'] ) );
                }
            }
            else
            {
               
/* Master bit exists, skip */
               
throw new \OverflowException;
            }
        }
        catch( \
UnderflowException $e )
        {
           
/* Template doesn't exist, so it's all good bro */
       
}
   
       
/* Insert */
       
$insertId = \IPS\Db::i()->replace( 'core_theme_templates', array(
               
'template_set_id'      => ( isset( $data['set_id'] ) ) ? $data['set_id'] : 0,
               
'template_app'          => mb_strtolower( $data['app'] ),
               
'template_location'   => mb_strtolower( $data['location'] ),
               
'template_group'      => mb_strtolower( $data['group'] ),
               
'template_name'       => $data['name'],
               
'template_data'       => $data['variables'],
               
'template_content'    => $data['content'],
               
'template_added_to'   => ( isset($data['added_to']) ) ? intval( $data['added_to'] ) : 0,
               
'template_updated'      => time(),
               
'template_user_added' => ( isset($data['_default_template']) ) ? 0 : 1,
               
'template_version'    => \IPS\Application::load('core')->long_version,
               
'template_plugin'      => isset( $data['plugin'] ) ? $data['plugin'] : NULL
       
), TRUE );
       
        if ( ! empty(
$data['set_id'] ) )
        {
            static::
setThemeCustomized( $data['set_id'] );
        }
       
        return
$insertId;
    }
   
   
/**
     * Remove templates completely from the system.
     * Used by hooks/application manager, etc.
     *
     * @param    string        $app        Application Key
     * @param    string|null    $location    Location
     * @param    string|null $group        Group
     * @param    int|null    $plugin        Plugin ID
     * @param    bool        $doAll        Delete all - by default only the master set is cleared
     * @param    string|null $template    Template name if you just want to do one template. If specified, deleteCompiledTemplate is NOT called and must be done manually
     * @return    void
     */
   
public static function removeTemplates( $app, $location=NULL, $group=NULL, $plugin=NULL, $doAll=FALSE, $template=NULL )
    {
        if ( !
$template )
        {
            static::
deleteCompiledTemplate( $app, $location, $group );
        }

       
$where = array( array( 'template_app=?', $app ) );
       
        if (
$location !== NULL )
        {
           
$where[] = array( 'template_location=?', $location );
        }
       
        if (
$group !== NULL )
        {
           
$where[] = array( 'template_group=?', $group );
        }
       
        if (
$template !== NULL )
        {
           
$where[] = array( 'template_name=?', $template );
        }
       
        if (
$plugin !== NULL )
        {
           
$where[] = array( 'template_plugin=?', $plugin );
        }
        else
        {
           
$where[] = array( 'template_plugin IS NULL' );
        }
       
       
/* Coming from build script */
       
if ( !$doAll )
        {
            if (
$plugin )
            {
               
$where[] = array( 'template_set_id=0' );
            }
            else
            {
               
$where[] = array( '(template_set_id=0 and template_user_added=0)' );
            }
        }
       
        \
IPS\Db::i()->delete( 'core_theme_templates', $where );
    }
   
   
/**
     * Remove CSS completely from the system.
     * Used by hooks/application manager, etc.
     *
     * @param    string        $app        Application Key
     * @param    string|null    $location    Location
     * @param    string|null $path        Group
     * @param    int|null    $plugin        Plugin ID
     * @param    bool        $doAll        Delete all - by default only the master set is cleared
     * @param    string|null $name        CSS file name if you just want to do one file. If specified, deleteCompiledCss is NOT called and must be done manually
     * @return    void
     */
   
public static function removeCss( $app, $location=NULL, $path=NULL, $plugin=NULL, $doAll=FALSE, $name=NULL )
    {
        if ( !
$name )
        {
            static::
deleteCompiledCss( $app, $location, $path );
        }

       
$where = array( array( 'css_app=?', $app ) );
       
        if (
$location !== NULL )
        {
           
$where[] = array( 'css_location=?', $location );
        }
       
        if (
$path !== NULL )
        {
           
$where[] = array( 'css_path=?', $path );
        }
       
        if (
$name !== NULL )
        {
           
$where[] = array( 'css_name=?', $name );
        }
       
        if (
$plugin !== NULL )
        {
           
$where[] = array( 'css_plugin=?', $plugin );
        }
        else
        {
           
$where[] = array( 'css_plugin IS NULL' );
        }
       
       
/* Coming from build script */
       
if ( !$doAll )
        {
           
$where[] = array( '(css_set_id=0 and css_added_to=0)' );
        }
       
        \
IPS\Db::i()->delete( 'core_theme_css', $where );
    }
   
   
/**
     * Remove resources completely from the system.
     * Used by hooks/application manager, etc.
     *
     * @param    string        $app        Application Key
     * @param    string|null    $location    Location
     * @param    string|null $path        Path
     * @param    int|null    $plugin        Plugin ID
     * @param    bool        $doAll        Delete all - by default only the master set is cleared
     * @return void
     */
   
public static function removeResources( $app, $location=NULL, $path=NULL, $plugin=NULL, $doAll=FALSE )
    {
        static::
deleteCompiledResources( $app, $location, $path );

       
$where = array( array( 'resource_app=?', $app ) );
       
        if (
$location !== NULL )
        {
           
$where[] = array( 'resource_location=?', $location );
        }
       
        if (
$path !== NULL )
        {
           
$where[] = array( 'resource_path=?', $path );
        }
       
        if (
$plugin !== NULL )
        {
           
$where[] = array( 'resource_plugin=?', $plugin );
        }
        else
        {
           
$where[] = array( 'resource_plugin IS NULL' );
        }
       
       
/* Coming from build script */
       
if ( !$doAll )
        {
           
$where[] = array( '(resource_set_id=0 OR resource_user_edited=0)' );
        }

        \
IPS\Db::i()->delete( 'core_theme_resources', $where );

        foreach( static::
themes() as $id => $set )
        {
           
$set->buildResourceMap( $app );
        }
    }

   
/**
     * Because css still has {resource} tags when built, and the building is done via the ACP,
     * Tags without a set "location" parameter are set to "admin" incorrectly.
     *
     * @param   string    $css        CSS Text
     * @param   string  $location   CSS Location
     * @return  string    Fixed CSS
     */
   
public static function fixResourceTags( $css, $location )
    {
       
preg_match_all( '#\{resource=([\'"])(\S+?)\\1([^\}]+?)?\}#i', $css, $items, PREG_SET_ORDER );
   
        foreach(
$items as $id => $attr )
        {
           
/* Has manually added params */
           
if ( isset( $attr[3] ) )
            {
                if ( ! \
strstr( $attr[3], 'location=' ) )
                {
                   
$new = \str_replace( $attr[3], $attr[3] . ' location="' . $location . '"', $attr[0] );
       
                   
$css = \str_replace( $attr[0], $new, $css );
                }
            }
            else
            {
               
$new = \str_replace( '}',  ' location="' . $location . '"}', $attr[0] );
               
$css = \str_replace( $attr[0], $new, $css );
            }
        }
   
        return
$css;
    }
   
   
/**
     * Inserts a built record
     *
     * @param    array    $css    css_* table data
     * @return    object    IPS\File
     */
   
protected static function writeCss( $css )
    {
       
$css['css_path']    = ( empty( $css['css_path'] ) ) ? '.' : $css['css_path'];
   
       
$functionName = "css_" . $css['css_app'] . '_' . $css['css_location'] . '_' . str_replace( array( '-', '/', '.' ), '_', $css['css_path'] . '_' . $css['css_name'] );

        if ( !
function_exists( $functionName ) )
        {
            static::
makeProcessFunction( static::fixResourceTags( $css['css_content'], $css['css_location'] ), $functionName, '', FALSE, TRUE );
        }
       
       
$content = static::minifyCss( call_user_func( 'IPS\\Theme\\'. $functionName ) );
       
$name    = static::makeBuiltTemplateLookupHash( $css['css_app'], $css['css_location'], $css['css_path'] . '/' .$css['css_name'] ) . '_' . $css['css_name'];
       
       
/* Replace any <fileStore.xxx> tags in the CSS */
       
\IPS\Output::i()->parseFileObjectUrls( $content );
       
        return \
IPS\File::create( 'core_Theme', $name, $content, 'css_built_' . $css['css_set_id'] );
    }
   
   
/**
     * Minifies CSS
     *
     * @param   string  $content    Content to minify
     * @return    string  $content    Minified
     */
   
public static function minifyCss( $content )
    {
       
/* Comments */
       
$content = preg_replace( '#/\*[^*]*\*+([^/][^*]*\*+)*/#', '', $content );
   
       
/* Multiple spaces, tabs and newlines */
       
$content = str_replace( array( "\r\n", "\r", "\n", "\t" ), ' ', $content );
       
$content = preg_replace( '!\s+!', ' ', $content );

       
/* Some more space removal */
       
$content = str_replace( ' {', '{', $content );
       
$content = str_replace( '{ ', '{', $content );
       
$content = str_replace( ' }', '}', $content );
       
$content = str_replace( '} ', '}', $content );
       
$content = str_replace( '; ', ';', $content );
       
$content = str_replace( ': ', ':', $content );
   
        return
$content;
    }
   
   
/**
     * This method is executed when theme settings have changed from saveForm()
     *
     * @param    int        $setId        Theme set id
     * @erturn  void
     */
   
public static function themeSettingsHaveChanged( $setId )
    {
       
$themeSetToBuild = static::load( $setId );
       
        \
IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $themeSetToBuild->id );
       
$themeSetToBuild->css_map = array();
       
        foreach(
$themeSetToBuild->allChildren() as $id => $child )
        {
            \
IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $child->id );
           
$child->css_map = array();
           
$child->save();
        }
    }
   
   
/**
     * Reset the cache key
     * This will make all existing template caches stale for this theme
     *
     * @return void
     */
   
public function resetCacheKey()
    {
       
$this->cache_key = md5( microtime() . mt_rand( 0, 1000 ) );
       
$this->save();
    }
   
   
/**
     * Any custom images/css/templates?
     *
     * @return boolean
     */
   
public function isCustomized()
    {
        try
        {
            \
IPS\Db::i()->select( 'template_id', 'core_theme_templates', array( 'template_set_id=?', $this->id ) )->first();
            return
TRUE;
        }
        catch( \
UnderflowException $e ) { }
       
        try
        {
            \
IPS\Db::i()->select( 'css_id', 'core_theme_css', array( 'css_set_id=?', $this->id ) )->first();
            return
TRUE;
        }
        catch( \
UnderflowException $e ) { }
       
        try
        {
            \
IPS\Db::i()->select( 'resource_id', 'core_theme_resources', array( 'resource_set_id=? and resource_user_edited=1', $this->id ) )->first();
            return
TRUE;
        }
        catch( \
UnderflowException $e ) { }
       
        return
FALSE;
    }
   
   
/**
     * Static wrapper for customized
     *
     * return @void
     */
   
public static function setThemeCustomized( $themeId )
    {
        try
        {
           
$theme = static::load( $themeId );
           
$theme->customized = $theme->isCustomized();
           
$theme->save();
        }
        catch( \
OutOfRangeException $e )
        {
           
        }
    }
   
   
/**
     * Reset all cache keys
     * This will make all existing template caches stale for all themes
     *
     * @return void
     */
   
public static function resetAllCacheKeys()
    {
       
/* There is no specific need for each theme to have a unique key */
       
\IPS\Db::i()->update( 'core_themes', array( 'set_cache_key' => md5( microtime() . mt_rand( 0, 1000 ) ) ) );
       
        if ( isset( \
IPS\Data\Store::i()->themes ) )
        {
            unset( \
IPS\Data\Store::i()->themes );
        }
    }
   
   
/**
     * Simple check to see if we have template caching enabled
     *
     * @return boolean
     */
   
public static function isUsingTemplateDiskCache()
    {
        if ( \
IPS\CIC and ! \IPS\Settings::i()->theme_disk_cache_templates )
        {
            \
IPS\Settings::i()->theme_disk_cache_templates = 1;
            \
IPS\Settings::i()->theme_disk_cache_path = \IPS\ROOT_PATH . '/uploads';
        }
       
        return \
IPS\Settings::i()->theme_disk_cache_templates and \IPS\Settings::i()->theme_disk_cache_path and is_dir( \IPS\Settings::i()->theme_disk_cache_path );
    }
   
   
/**
     * Add a lock to prevent race conditions
     *
     * @param    string    $key     Unique key to sign the lock
     * @return    void
     */
   
public static function lock( $key )
    {
        \
IPS\Db::i()->replace( 'core_cache', array(
           
'cache_key'    => 'locking_' . $key,
           
'cache_value'  => $key,
           
'cache_expire' => time() + 30
       
) );
    }
   
   
/**
     * Check a race condition lock
     *
     * @param    string    $key     Unique key to sign the lock
     * @return    boolean
     */
   
public static function checkLock( $key )
    {
        try
        {
            \
IPS\Db::i()->select( 'SQL_NO_CACHE *', 'core_cache', array( 'cache_key=? and cache_expire > ?', 'locking_' . $key, time() - 30 ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
            return
true;
        }
        catch( \
Exception $ex )
        {
            return
false;
        }
    }
   
   
/**
     * Remove a race condition lock
     *
     * @param    string    $key     Unique key to sign the lock
     * @return    void
     */
   
public static function unlock( $key )
    {
        \
IPS\Db::i()->delete( 'core_cache', array( 'cache_key=?', 'locking_' . $key ) );
    }
}