5743 lines
167 KiB
PHP
5743 lines
167 KiB
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', 'set_marketplace_id');
|
|
|
|
/**
|
|
* @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"
|
|
*/
|
|
public static $inDevTheme = NULL;
|
|
|
|
/**
|
|
* @brief Setup "theme"
|
|
*/
|
|
protected static $setupSkin = NULL;
|
|
|
|
/**
|
|
* @brief Admin "theme"
|
|
*/
|
|
public static $adminSkin = 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] 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;
|
|
|
|
/**
|
|
* @brief Bit option for theme settings
|
|
*/
|
|
const TEMPLATE_SETTINGS_DEFAULT = 4;
|
|
|
|
/**
|
|
* @brief Disable the copy button - useful when the forms are very distinctly different
|
|
*/
|
|
public $noCopyButton = TRUE;
|
|
|
|
/**
|
|
* @brief [ActiveRecord] Caches
|
|
* @note Defined cache keys will be cleared automatically as needed
|
|
*/
|
|
protected $caches = array( 'updatecount_themes' );
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Set Admin Theme */
|
|
if ( static::$adminSkin === NULL )
|
|
{
|
|
static::$adminSkin = static::master();
|
|
}
|
|
|
|
/* 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() AND \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'front' )
|
|
{
|
|
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;
|
|
}
|
|
|
|
if ( static::isUsingEasyModeEditor() )
|
|
{
|
|
$setId = \intval( \IPS\Request::i()->cookie['vseThemeId'] );
|
|
static::$memberTheme = empty( $setId ) ? static::defaultTheme() : static::load( $setId );
|
|
}
|
|
}
|
|
|
|
return static::$inDevTheme;
|
|
}
|
|
else if ( \IPS\Dispatcher::hasInstance() AND class_exists( '\IPS\Dispatcher', FALSE ) and \IPS\Dispatcher::i()->controllerLocation === 'admin' )
|
|
{
|
|
if ( static::$adminSkin === NULL )
|
|
{
|
|
static::$adminSkin = static::master();
|
|
}
|
|
return static::$adminSkin;
|
|
}
|
|
else
|
|
{
|
|
if ( static::$memberTheme === NULL )
|
|
{
|
|
static::themes();
|
|
|
|
$setId = NULL;
|
|
if ( \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
|
|
{
|
|
$setId = static::currentThemeId();
|
|
|
|
if ( $setId )
|
|
{
|
|
if( ! \IPS\Request::i()->isAjax() and \IPS\Session\Front::loggedIn() )
|
|
{
|
|
/* 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 )
|
|
{
|
|
$setId = NULL;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
if ( ! $setId and \IPS\Member::loggedIn()->skin and array_key_exists( \IPS\Member::loggedIn()->skin, static::themes() ) )
|
|
{
|
|
$setId = \IPS\Member::loggedIn()->skin;
|
|
|
|
if ( static::load( $setId )->canAccess() !== true )
|
|
{
|
|
$setId = static::defaultTheme();
|
|
|
|
/* Restore default theme for member */
|
|
\IPS\Member::loggedIn()->skin = $setId;
|
|
|
|
if( \IPS\Member::loggedIn()->member_id )
|
|
{
|
|
\IPS\Member::loggedIn()->save();
|
|
}
|
|
}
|
|
}
|
|
else if ( ! $setId )
|
|
{
|
|
$setId = static::defaultTheme();
|
|
}
|
|
|
|
if ( static::isUsingEasyModeEditor() )
|
|
{
|
|
$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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current theme id
|
|
*
|
|
* @return int|null
|
|
*/
|
|
public static function currentThemeId()
|
|
{
|
|
if ( \IPS\Session\Front::loggedIn() )
|
|
{
|
|
return \IPS\Session\Front::i()->getTheme();
|
|
}
|
|
|
|
return isset( \IPS\Request::i()->cookie['theme'] ) ? \IPS\Request::i()->cookie['theme'] : NULL;
|
|
}
|
|
|
|
/**
|
|
* 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'];
|
|
}
|
|
|
|
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
|
|
*/
|
|
public static function getThemesWithAccessPermission()
|
|
{
|
|
$visibleThemes = array();
|
|
foreach( \IPS\Theme::roots() as $theme )
|
|
{
|
|
if ( $theme->canAccess() )
|
|
{
|
|
$visibleThemes[$theme->id] = $theme;
|
|
}
|
|
|
|
static::getChildrenWithAccessPermission( $theme, $visibleThemes );
|
|
}
|
|
|
|
return $visibleThemes;
|
|
}
|
|
|
|
/**
|
|
* Returns only the visible children
|
|
* @param \IPS\Theme $theme Parent theme
|
|
* @param Array $visibleThemes Visible themes array passed by reference
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function getChildrenWithAccessPermission( \IPS\Theme $theme, Array &$visibleThemes )
|
|
{
|
|
foreach ( $theme->children() as $child )
|
|
{
|
|
if( $child->canAccess() )
|
|
{
|
|
$visibleThemes[ $child->id ] = $child;
|
|
static::getChildrenWithAccessPermission( $child, $visibleThemes );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the master theme object
|
|
*
|
|
* @return \IPS\Theme
|
|
*/
|
|
public static function master()
|
|
{
|
|
static::themes();
|
|
$default = static::$multitons[ static::$defaultFrontendThemeSet ];
|
|
$master = static::constructFromData( array_merge( static::$themeSetRelationships[ $default->parent_id ][ $default->id ], array( 'set_id' => 0 ) ) );
|
|
$master->_data['settings'] = $default->getThemeSettings( static::TEMPLATE_SETTINGS_DEFAULT | static::THEME_KEY_VALUE_PAIRS );
|
|
|
|
unset( static::$multitons[ 0 ] );
|
|
|
|
return $master;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
/* Make sure the member theme is set */
|
|
\IPS\Theme::i();
|
|
|
|
$class = static::i();
|
|
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\Dispatcher::i()->application->directory;
|
|
$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();
|
|
// If we don't let the CSS rebuild here as needed then the AdminCP upgrader can result in a completely unstyled page when interrupted
|
|
}
|
|
|
|
/* 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\Dispatcher::i()->application->directory;
|
|
$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, TRUE, 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;
|
|
}
|
|
|
|
$hashedKey = \strtolower( static::makeBuiltTemplateLookupHash( $app, $location, $group ) . '_' . static::cleanGroupName( $group ) );
|
|
$key = 'template_' . $this->id . '_' . $hashedKey;
|
|
|
|
$cachedObject = NULL;
|
|
|
|
/* We cannot use isset( static::$calledTemplates[ $key ] ) here because it fails with NULL while in_array does not */
|
|
if ( !\in_array( $hashedKey, 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::debug( "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[ $hashedKey ] = 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[ $hashedKey ] = new \IPS\Theme\SandboxedTemplate( new $class( $app, $location, $group ) );
|
|
}
|
|
}
|
|
|
|
return static::$calledTemplates[ $hashedKey ];
|
|
}
|
|
|
|
/**
|
|
* Determine the path to load the file from
|
|
*
|
|
* @param string $file File path
|
|
* @return string
|
|
*/
|
|
public static function getHookPath( $file )
|
|
{
|
|
$path = \IPS\ROOT_PATH;
|
|
if ( \IPS\CIC2 AND \IPS\IPS::isThirdParty( $file ) )
|
|
{
|
|
$path = \IPS\SITE_FILES_PATH;
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* 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 )
|
|
{
|
|
$path = static::getHookPath( $data['file'] );
|
|
|
|
if ( file_exists( $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( $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 );
|
|
|
|
if ( \is_array( $logo ) )
|
|
{
|
|
if ( isset( $logo['front'] ) )
|
|
{
|
|
$obj->_data['logo']['front'] = $logo['front'];
|
|
}
|
|
}
|
|
|
|
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( $obj->id > 0 )
|
|
{
|
|
if ( $imgMap = json_decode( $obj->_data['resource_map'], true ) )
|
|
{
|
|
$obj->_data['resource_map'] = $imgMap;
|
|
}
|
|
else
|
|
{
|
|
$obj->_data['resource_map'] = array();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( !isset( \IPS\Data\Store::i()->acp_resource_map ) )
|
|
{
|
|
$obj->_data['resource_map'] = [];
|
|
}
|
|
else
|
|
{
|
|
$obj->_data['resource_map'] = \IPS\Data\Store::i()->acp_resource_map;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! \is_array( $obj->_data['css_map'] ) )
|
|
{
|
|
if ( $obj->id > 0 )
|
|
{
|
|
if ( $cssMap = json_decode( $obj->_data['css_map'], true ) )
|
|
{
|
|
$obj->_data['css_map'] = $cssMap;
|
|
}
|
|
else
|
|
{
|
|
$obj->_data['css_map'] = array();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$obj->_data['css_map'] = json_decode( \IPS\Settings::i()->acp_css_map, true );
|
|
}
|
|
}
|
|
|
|
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 );
|
|
|
|
if ( $this->id === 0 )
|
|
{
|
|
\IPS\Data\Store::i()->acp_resource_map = $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 );
|
|
|
|
if ( $this->id === 0 )
|
|
{
|
|
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => json_encode( $value ) ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add in the header and footer special tags if they are not already present in globalTemplate
|
|
*
|
|
* @return void
|
|
*/
|
|
public function fixHeaderAndFooterTags()
|
|
{
|
|
/* Check the globalTemplate for the special tags we need */
|
|
try
|
|
{
|
|
$template = \IPS\Db::i()->select( '*', 'core_theme_templates', [ 'template_set_id=? AND template_app=? AND template_group=? AND template_location=? and template_name=?', $this->_id, 'core', 'global', 'front', 'globalTemplate'] )->first();
|
|
$update = FALSE;
|
|
|
|
if ( ! \stristr( $template['template_content'], '{theme="headerHtml"}') )
|
|
{
|
|
$template['template_content'] = preg_replace( '#(<body.*>\n)#i', "$1\n{theme=\"headerHtml\"}\n", $template['template_content'] );
|
|
$update = TRUE;
|
|
}
|
|
|
|
if ( ! \stristr( $template['template_content'], '{theme="footerHtml"}') )
|
|
{
|
|
$template['template_content'] = preg_replace( '#(</body>\n)#i', "{theme=\"footerHtml\"}\n$1", $template['template_content'] );
|
|
$update = TRUE;
|
|
}
|
|
|
|
if ( $update )
|
|
{
|
|
\IPS\Db::i()->update( 'core_theme_templates', [ 'template_content' => $template['template_content'] ], [ 'template_id=?', $template['template_id'] ] );
|
|
}
|
|
|
|
}
|
|
catch( \UnderflowException $e )
|
|
{
|
|
/* No custom template, so we're good */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 );
|
|
|
|
/* If we are using a customized globalTemplate, we need to slap in the header/footer tags */
|
|
if ( ! empty( $this->custom_footer ) or ! empty( $this->customer_header ) )
|
|
{
|
|
$this->fixHeaderAndFooterTags();
|
|
}
|
|
|
|
foreach( [ 'header', 'footer' ] as $type )
|
|
{
|
|
$key = 'custom_' . $type . '_' . $this->_id;
|
|
if ( isset( \IPS\Data\Store::i()->$key ) )
|
|
{
|
|
unset( \IPS\Data\Store::i()->$key );
|
|
}
|
|
}
|
|
|
|
/* 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 header logo
|
|
*
|
|
* @return \IPS\Http\Url|string
|
|
*/
|
|
public function get_logo_front()
|
|
{
|
|
return $this->logoImage( 'front' );
|
|
}
|
|
|
|
/**
|
|
* Return a string of CSS var definitions
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_css_vars()
|
|
{
|
|
$output = array();
|
|
|
|
foreach( $this->settings as $key => $value )
|
|
{
|
|
if( preg_match( '/^#[0-9a-fA-F]{6}$/', $value ) )
|
|
{
|
|
$value = str_replace('#', '', $value);
|
|
$rgb = array();
|
|
|
|
if( \strlen( $value ) === 3 )
|
|
{
|
|
$rgb[] = hexdec( \substr( $value, 0, 1 ) . \substr( $value, 0, 1 ) );
|
|
$rgb[] = hexdec( \substr( $value, 1, 1 ) . \substr( $value, 1, 1 ) );
|
|
$rgb[] = hexdec( \substr( $value, 2, 1 ) . \substr( $value, 2, 1 ) );
|
|
}
|
|
else
|
|
{
|
|
$rgb[] = hexdec( \substr( $value, 0, 2 ) );
|
|
$rgb[] = hexdec( \substr( $value, 2, 2 ) );
|
|
$rgb[] = hexdec( \substr( $value, 4, 2 ) );
|
|
}
|
|
|
|
$output[] = "\t--theme-" . $key . ": " . implode(', ', $rgb) . ";";
|
|
}
|
|
}
|
|
|
|
return implode("\n", $output);
|
|
}
|
|
|
|
/**
|
|
* 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 );
|
|
}
|
|
|
|
/**
|
|
* Get the authors website
|
|
* *
|
|
* @return \IPS\Http\Url|null
|
|
*/
|
|
public function website()
|
|
{
|
|
if ( $this->_data['author_url'] )
|
|
{
|
|
return \IPS\Http\Url::createFromString( $this->_data['author_url'] );
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* [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 )
|
|
{
|
|
$message = 'theme_is_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 )
|
|
{
|
|
$message = 'default_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 => 'new',
|
|
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;
|
|
|
|
/* Wipe marketplace association */
|
|
$this->marketplace_id = NULL;
|
|
|
|
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 )
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
foreach( $this->children( NULL ) as $childTheme )
|
|
{
|
|
if( $childTheme->is_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 )
|
|
{
|
|
\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\CheckboxSet(
|
|
'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', 'impliedUnlimited' => TRUE ),
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
'core_theme_set_permissions'
|
|
) );
|
|
|
|
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' ) );
|
|
|
|
$form->add( new \IPS\Helpers\Form\Number( 'core_theme_set_logo_height', ( isset( $this->logo['front']['height'] ) ? $this->logo['front']['height'] : 100 ), FALSE, array( 'min' => 1, 'max' => 100 ), NULL, NULL, '%' ) );
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( \IPS\CIC2 )
|
|
{
|
|
if ( is_dir( \IPS\SITE_FILES_PATH . '/ckeditor_custom' ) )
|
|
{
|
|
foreach( new \DirectoryIterator( \IPS\SITE_FILES_PATH . '/ckeditor_custom' ) AS $f )
|
|
{
|
|
if ( !$f->isDot() AND $f->isDir() )
|
|
{
|
|
$_name = (string) $f;
|
|
|
|
if ( mb_substr( $_name, 0, 5) === 'skin_' )
|
|
{
|
|
$_name = str_replace( 'skin_', '', $_name );
|
|
if ( \IPS\Member::loggedIn()->language()->checkKeyExists( 'ckeditor_name_' . $_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\CIC2 OR ( !\IPS\NO_WRITES and class_exists( 'ZipArchive', FALSE ) ) )
|
|
{
|
|
if ( !\IPS\CIC2 AND !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' ) );
|
|
}
|
|
}
|
|
|
|
$form->addTab('core_theme_set_header_and_footer_header');
|
|
$form->add( new \IPS\Helpers\Form\Codemirror( 'theme_simple_header_settings', $this->custom_header, FALSE, [ 'mode' => 'htmlmixed', 'height' => 400 ] ) );
|
|
$form->add( new \IPS\Helpers\Form\Codemirror( 'theme_simple_footer_settings', $this->custom_footer, FALSE, [ 'mode' => 'htmlmixed', 'height' => 400 ] ) );
|
|
|
|
/* 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}" )->csrf(),
|
|
'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' ) AND !\IPS\DEMO_MODE )
|
|
{
|
|
if( !$this->marketplace_id )
|
|
{
|
|
$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 ) )->csrf(),
|
|
'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}" )->csrf()
|
|
);
|
|
}
|
|
|
|
/*$buttons[] = array(
|
|
'icon' => 'cogs',
|
|
'title' => 'theme_set_build',
|
|
'link' => \IPS\Http\Url::internal( "{$url}&do=build&id={$this->_id}" )->csrf()
|
|
);*/
|
|
}
|
|
|
|
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 );
|
|
|
|
if ( \IPS\CIC2 )
|
|
{
|
|
$tmpName = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
|
|
move_uploaded_file( $values['core_theme_set_editor_new'], $tmpName );
|
|
|
|
\IPS\Cicloud\file( "IPScustomcke_skin_{$values['core_theme_set_editor_existing']}.zip", \file_get_contents( $tmpName ) );
|
|
|
|
do
|
|
{
|
|
if ( isset( \IPS\Data\Store::i()->syncCompleted ) AND \IPS\Data\Store::i()->syncCompleted )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
while( TRUE );
|
|
}
|
|
else
|
|
{
|
|
/* 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();
|
|
$changedSettings = false;
|
|
|
|
/* 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, 'height' => 100 );
|
|
}
|
|
catch ( \RuntimeException $e )
|
|
{
|
|
$this->_data['logo']['front'] = NULL;
|
|
}
|
|
|
|
if ( isset( $values['core_theme_set_logo_height'] ) )
|
|
{
|
|
$this->_data['logo']['front']['height'] = $values['core_theme_set_logo_height'];
|
|
}
|
|
|
|
$dataChanged = true;
|
|
$changedSettings = true;
|
|
}
|
|
else
|
|
{
|
|
$dataChanged = true;
|
|
$this->_data['logo']['front'] = 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 ( $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'];
|
|
unset( \IPS\Data\Store::i()->syncCompleted );
|
|
}
|
|
|
|
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->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->css_updated = time();
|
|
|
|
if ( isset( $values['theme_simple_header_settings'] ) )
|
|
{
|
|
$this->custom_header = $values['theme_simple_header_settings'];
|
|
}
|
|
|
|
if ( isset( $values['theme_simple_footer_settings'] ) )
|
|
{
|
|
$this->custom_footer = $values['theme_simple_footer_settings'];
|
|
}
|
|
|
|
$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
|
|
* @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, 'height' => $data['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 )
|
|
{
|
|
$path = static::getHookPath( "applications/{$appKey}/data/themesettings.json" );
|
|
|
|
/* Root skin? Add default theme settings */
|
|
if ( ! file_exists( $path . "/applications/{$appKey}/data/themesettings.json" ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$json = json_decode( file_get_contents( $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 array $return Array to return children into
|
|
* @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 ) )
|
|
{
|
|
$path = static::getHookPath( $data['file'] );
|
|
|
|
/* Like code hooks, we should only attempt to load the files contents if it actually exists */
|
|
if ( file_exists( $path . '/' . $data['file'] ) )
|
|
{
|
|
try
|
|
{
|
|
if ( eval( "namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp EXTENDS \IPS\Theme\Compile\ThemeHook', 'array()' ), file_get_contents( $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()->insert( '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 ), TRUE );
|
|
}
|
|
|
|
static::deleteCompiledCss( $css['css_app'], $css['css_location'], $css['css_path'], $css['css_name'], $data['set_id'] );
|
|
|
|
static::setThemeCustomized( $data['set_id'] );
|
|
|
|
/* Update the CSS last updated timestamp */
|
|
$this->css_updated = time();
|
|
$this->save();
|
|
|
|
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 the theme set and any children
|
|
\IPS\Db::i()->delete( 'core_theme_templates', array(
|
|
"template_name=? AND template_group=? AND template_app=? AND template_location=? AND " . \IPS\Db::i()->in( 'template_set_id', $children ),
|
|
$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 )
|
|
{
|
|
if ( $flags & static::TEMPLATE_SETTINGS_DEFAULT )
|
|
{
|
|
$row['_value'] = $row['sc_default'];
|
|
}
|
|
else
|
|
{
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Ensure children get new theme settings
|
|
*
|
|
* @return void
|
|
*/
|
|
public function updateChildrenThemeSettings()
|
|
{
|
|
$mainRows = iterator_to_array( \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' ) );
|
|
|
|
$mainRowsByKey = [];
|
|
|
|
foreach( $mainRows as $row )
|
|
{
|
|
$mainRowsByKey[ $row['sc_key'] ] = $row;
|
|
}
|
|
|
|
foreach( $this->children() as $child )
|
|
{
|
|
$childRows = iterator_to_array( \IPS\Db::i()->select( 'sc.*, sv.sv_value', array('core_theme_settings_fields', 'sc'), array( 'sc.sc_set_id=?', $child->id ) )
|
|
->join( array('core_theme_settings_values', 'sv'), 'sv.sv_id=sc.sc_id' ) );
|
|
|
|
$rowsByKey = [];
|
|
foreach( $childRows as $row )
|
|
{
|
|
$rowsByKey[ $row['sc_key'] ] = $row;
|
|
}
|
|
|
|
$diff = array_diff( array_keys( $mainRowsByKey ), array_keys( $rowsByKey ) );
|
|
if ( \count( $diff ) )
|
|
{
|
|
foreach( $diff as $keyName )
|
|
{
|
|
$save = [];
|
|
foreach( $mainRowsByKey[ $keyName ] as $k => $v )
|
|
{
|
|
if ( \mb_substr( $k, 0, 3 ) == 'sc_' and $k != 'sc_id' )
|
|
{
|
|
$save[ $k ] = $v;
|
|
}
|
|
}
|
|
|
|
$save['sc_set_id'] = $child->id;
|
|
$save['sc_order'] = \IPS\Db::i()->select( 'MAX(sc_order)', 'core_theme_settings_fields', array( 'sc_set_id=? AND sc_tab_key=?', $child->id, $save['sc_tab_key'] ) )->first() + 1;
|
|
$id = \IPS\Db::i()->insert( 'core_theme_settings_fields', $save );
|
|
|
|
$themeSettings = $child->getThemeSettings();
|
|
|
|
$json = array();
|
|
foreach( $themeSettings as $settingId => $row )
|
|
{
|
|
$json[ $row['sc_key'] ] = $row['_value'];
|
|
}
|
|
|
|
$child->template_settings = json_encode( $json );
|
|
$child->save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get theme header and footer
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getHeaderAndFooter(): array
|
|
{
|
|
$return = [ 'header' => null, 'footer' => null ];
|
|
|
|
if ( $this->custom_header )
|
|
{
|
|
$key = 'custom_header_' . $this->_id;
|
|
if ( ! isset( \IPS\Data\Store::i()->$key ) )
|
|
{
|
|
\IPS\Data\Store::i()->$key = static::compileTemplate( $this->custom_header, NULL );
|
|
}
|
|
|
|
$return['header'] = \IPS\Data\Store::i()->$key;
|
|
}
|
|
|
|
if ( $this->custom_footer )
|
|
{
|
|
$key = 'custom_footer_' . $this->_id;
|
|
if ( ! isset( \IPS\Data\Store::i()->$key ) )
|
|
{
|
|
\IPS\Data\Store::i()->$key = static::compileTemplate( $this->custom_footer, NULL );
|
|
}
|
|
|
|
$return['footer'] = \IPS\Data\Store::i()->$key;
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* 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 array $colors Theme setting colors
|
|
* @param string $customCss User added CSS
|
|
* @param array $settings Settings to update
|
|
* @return void
|
|
*/
|
|
public function vseSave( $colors, $customCss, $settings )
|
|
{
|
|
$css = $this->getRawCss( 'core', 'front', 'custom', static::RETURN_ALL, true );
|
|
|
|
$custom = array( 'app' => 'core',
|
|
'location' => 'front',
|
|
'group' => 'custom',
|
|
'name' => 'custom.css',
|
|
'set_id' => $this->id,
|
|
'content' => $customCss
|
|
);
|
|
|
|
/* 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( $custom );
|
|
|
|
/* 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 ) )
|
|
{
|
|
$settings = array();
|
|
}
|
|
|
|
$settings = array_merge( $settings, $colors );
|
|
|
|
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', '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'] );
|
|
}
|
|
|
|
if ( isset( $data['logo']['front'] ) )
|
|
{
|
|
$this->_data['logo']['front'] = $data['logo']['front'];
|
|
}
|
|
|
|
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') );
|
|
|
|
$masterTemplateBits = \IPS\Theme::master()->getRawTemplates();
|
|
|
|
/* Find changed and new template bits */
|
|
foreach( $results['templates'] as $key => $data )
|
|
{
|
|
$data['added'] = false;
|
|
$data['deleted'] = false;
|
|
|
|
$masterSetTemplate = $masterTemplateBits[$data['template_app']][$data['template_location']][$data['template_group']][$data['template_name']] ?? NULL;
|
|
|
|
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 if ( $masterSetTemplate and md5( $masterSetTemplate['template_content'] ) != md5( $data['template_content'] ) )
|
|
{
|
|
$data['diff'] = \Diff::toTable( \Diff::compare( $masterSetTemplate['template_content'], $data['template_content'] ) );
|
|
}
|
|
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->css_updated = time();
|
|
$set->save();
|
|
}
|
|
}
|
|
|
|
/* ACP CSS */
|
|
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => '[]' ) );
|
|
|
|
/* 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->css_updated = time();
|
|
$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 ] );
|
|
|
|
try
|
|
{
|
|
if ( $row['resource_filename'] )
|
|
{
|
|
\IPS\File::get( 'core_Theme', $row['resource_filename'] )->delete();
|
|
}
|
|
}
|
|
catch ( \InvalidArgumentException $ex ) { }
|
|
}
|
|
}
|
|
catch ( \OutOfRangeException $ex )
|
|
{
|
|
$map[$row['resource_set_id']] = array();
|
|
}
|
|
}
|
|
}
|
|
|
|
\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 ) { }
|
|
}
|
|
|
|
if ( empty( $themeId ) )
|
|
{
|
|
/* ACP CSS and Resources */
|
|
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => '[]' ) );
|
|
unset( \IPS\Data\Store::i()->acp_resource_map );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
|
|
$pseudoSelectors = array(
|
|
'active', 'checked', 'disabled', 'empty', 'enabled', 'first-child', 'first-of-type', 'focus', 'hover', 'in-range', 'invalid', 'lang', 'last-child',
|
|
'last-of-type', 'link', 'eq', 'not', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'only-of-type', 'only-child', 'optional', 'out-of-range', 'read-only', 'read-write', 'required',
|
|
'root', 'target', 'valid', 'visited',
|
|
'after', 'before', 'first-letter', 'first-line', 'selection'
|
|
);
|
|
|
|
/* 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 );
|
|
|
|
/* Make custom classes like ipsPadding_top:half selectable by PQ */
|
|
preg_match_all( '#class\s?=\s?[\'"]([^\'"]+?)[\'"]#', $content, $classes, PREG_SET_ORDER );
|
|
|
|
foreach( $classes as $idx => $data )
|
|
{
|
|
$cleaned = preg_replace_callback( "/([\d\w0-9-]+?):(.+?)(\.|\s|\(|$)/", function( $matches ) use ( $pseudoSelectors )
|
|
{
|
|
foreach( $pseudoSelectors as $selector )
|
|
{
|
|
if ( $matches[2] == $selector )
|
|
{
|
|
/* It's a pseudo selector */
|
|
return $matches[0];
|
|
}
|
|
}
|
|
|
|
/* Still here? */
|
|
return str_replace( ':', '---cln---', $matches[0] );
|
|
}, $data[1] );
|
|
|
|
$content = str_replace( $data[1], $cleaned, $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 )
|
|
{
|
|
/* Temporarily adjust class names with : in it that are not also psuedo-selectors */
|
|
if ( isset( $hook['selector'] ) )
|
|
{
|
|
$hook['selector'] = preg_replace_callback( "/([\d\w0-9-]+?):(.+?)(\.|\s|\(|$)/", function( $matches ) use ( $pseudoSelectors )
|
|
{
|
|
foreach( $pseudoSelectors as $selector )
|
|
{
|
|
if ( $matches[2] == $selector )
|
|
{
|
|
/* It's a pseudo selector */
|
|
return $matches[0];
|
|
}
|
|
}
|
|
|
|
/* Still here? */
|
|
return str_replace( ':', '---cln---', $matches[0] );
|
|
|
|
}, $hook['selector'] );
|
|
}
|
|
|
|
/* 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 );
|
|
}
|
|
|
|
/* Fix ---cln--- classes */
|
|
preg_match_all( '#class\s?=\s?[\'"]([^\'"]+?)[\'"]#', $return, $classes, PREG_SET_ORDER );
|
|
|
|
foreach( $classes as $idx => $data )
|
|
{
|
|
$return = str_replace( $data[1], str_replace( '---cln---', ':', $data[1] ), $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;
|
|
}
|
|
|
|
/**
|
|
* Encode the content for php query
|
|
*
|
|
* @return void
|
|
*/
|
|
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
|
|
* @return void
|
|
*/
|
|
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
|
|
* @return void
|
|
*/
|
|
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|NULL $functionName Desired function name if given, otherwise <<<CONTENT CONTENT; is returned without a function wrapper or $return .= statement
|
|
* @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
|
|
* @param string $app Application the template belongs to
|
|
* @param string $location Location the template belongs to (admin,front)
|
|
* @param string $group Group the template belongs to
|
|
* @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
|
|
{
|
|
if ( ! $functionName )
|
|
{
|
|
$function = <<<PHP
|
|
<<<CONTENT\n
|
|
{$content}
|
|
CONTENT;\n
|
|
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 )
|
|
{
|
|
$path = static::getHookPath( "applications/{$app->directory}/extensions/core/OutputPlugins/" . mb_ucfirst( $plugin ) . ".php" );
|
|
|
|
if ( file_exists( $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 = $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 );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$all = preg_replace( '/\{\$([^\}]+?)\|raw\}/', '{\$$1}', $all );
|
|
$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();
|
|
}
|
|
|
|
/* ACP CSS and Resources */
|
|
\IPS\Settings::i()->changeValues( array( 'acp_css_map' => '[]' ) );
|
|
unset( \IPS\Data\Store::i()->acp_resource_map );
|
|
}
|
|
|
|
/**
|
|
* 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'] )
|
|
{
|
|
$saveName = $name;
|
|
if ( $resource !== NULL and $resource['resource_data'] != $data['content'] )
|
|
{
|
|
/* This resource exists, so let us create a unique name to prevent cache issues */
|
|
$ext = mb_substr( $name, ( mb_strrpos( $name, '.' ) + 1 ) );
|
|
$saveName = mb_substr( $name, 0, ( mb_strrpos( $name, '.' ) ) ) . '_' . mt_rand() . '.' . $ext;
|
|
}
|
|
|
|
$fileName = (string) \IPS\File::create( 'core_Theme', $saveName, $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();
|
|
}
|
|
|
|
// create the plugin template
|
|
if( \IPS\IN_DEV AND $data['app'] === 'core' AND $data['location'] === 'global' AND $data['path'] === '/plugins/' )
|
|
{
|
|
$plugin = \IPS\Plugin::load( $data['plugin'] );
|
|
|
|
\file_put_contents( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/resources/{$data['name']}", $data['content'] );
|
|
@chmod( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/resources/{$data['name']}", \IPS\IPS_FILE_PERMISSION );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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()->insert( '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
|
|
), TRUE );
|
|
|
|
if ( ! empty( $data['set_id'] ) )
|
|
{
|
|
/* This is a specific theme set */
|
|
static::setThemeCustomized( $data['set_id'] );
|
|
}
|
|
else
|
|
{
|
|
/* Update all themes to let them know that a new master CSS has been added */
|
|
\IPS\Db::i()->update( 'core_themes', array( 'set_css_updated' => time() ) );
|
|
}
|
|
|
|
// create the plugin template
|
|
if( \IPS\IN_DEV AND $data['app'] === 'core' AND $data['location'] === 'front' AND $data['path'] === 'custom' AND isset( $data['plugin'] ) )
|
|
{
|
|
$plugin = \IPS\Plugin::load( $data['plugin'] );
|
|
|
|
\file_put_contents( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/css/{$data['name']}", $data['content'] );
|
|
@chmod( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/css/{$data['name']}", \IPS\IPS_FILE_PERMISSION );
|
|
}
|
|
|
|
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()->insert( 'core_theme_templates', array(
|
|
'template_set_id' => ( isset( $data['set_id'] ) ) ? $data['set_id'] : 0,
|
|
'template_app' => $data['app'],
|
|
'template_location' => $data['location'],
|
|
'template_group' => $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'] );
|
|
}
|
|
|
|
/* Reset disk cache */
|
|
if ( static::isUsingTemplateDiskCache() )
|
|
{
|
|
static::resetAllCacheKeys();
|
|
}
|
|
|
|
// create the plugin templates
|
|
if( \IPS\IN_DEV AND $data['app'] === 'core' AND $data['location'] === 'global' AND $data['group'] === 'plugins' )
|
|
{
|
|
$plugin = \IPS\Plugin::load( $data['plugin'] );
|
|
|
|
\file_put_contents( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/html/{$data['name']}.phtml", $data['rawContent'] );
|
|
@chmod( \IPS\ROOT_PATH . "/plugins/{$plugin->location}/dev/html/{$data['name']}.phtml", \IPS\IPS_FILE_PERMISSION );
|
|
}
|
|
|
|
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
|
|
* @param string|null $name Resource file name if you just want to do one file. If specified, deleteCompiledResources AND buildResourceMap are both NOT called and must be done manually
|
|
* @return void
|
|
*/
|
|
public static function removeResources( $app, $location=NULL, $path=NULL, $plugin=NULL, $doAll=FALSE, $name=NULL )
|
|
{
|
|
if ( !$name )
|
|
{
|
|
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 ( $name !== NULL )
|
|
{
|
|
$where[] = array( 'resource_name=?', $name );
|
|
}
|
|
|
|
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 );
|
|
|
|
if ( !$name )
|
|
{
|
|
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 );
|
|
}
|
|
|
|
$fqFunc = 'IPS\\Theme\\'. $functionName;
|
|
$content = static::minifyCss( $fqFunc() );
|
|
$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'], FALSE, NULL, FALSE );
|
|
}
|
|
|
|
/**
|
|
* 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->css_updated = time();
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Cache bust key for CSS
|
|
*
|
|
* @return string
|
|
*/
|
|
public function cssCacheBustKey()
|
|
{
|
|
return \IPS\CACHEBUST_KEY . $this->css_updated;
|
|
}
|
|
|
|
/**
|
|
* 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 ) );
|
|
}
|
|
|
|
/**
|
|
* Are we actively editing an easy mode theme?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isUsingEasyModeEditor(): bool
|
|
{
|
|
return isset( \IPS\Request::i()->cookie['vseThemeId'] ) AND \IPS\Member::loggedIn()->isAdmin() AND \IPS\Member::loggedIn()->members_bitoptions['bw_using_skin_gen'];
|
|
}
|
|
|
|
/**
|
|
* Pre output handling for easy mode theme if actively used
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function easyModePreOutput()
|
|
{
|
|
/* We are using the easy mode editor, and we're a logged in admin and the theme is an easy mode theme */
|
|
if ( static::isUsingEasyModeEditor() and static::$memberTheme->by_skin_gen )
|
|
{
|
|
$storage = \IPS\File::getClass( 'core_Theme' );
|
|
$baseUrl = parse_url( \IPS\Http\Url::internal('') );
|
|
$storageUrl = parse_url( $storage->baseUrl() );
|
|
|
|
/* is it a different host? */
|
|
if ( $storageUrl['host'] != $baseUrl['host'] )
|
|
{
|
|
$cssFiles = [];
|
|
foreach( \IPS\Output::i()->cssFiles as $css )
|
|
{
|
|
if ( $css->data['host'] == $storageUrl['host'] )
|
|
{
|
|
$cssFiles[] = \IPS\Http\Url::internal('app=core&module=system&controller=easymode&do=proxy')->setQueryString( [ 'f' => base64_encode( (string) $css ) ] );
|
|
}
|
|
else
|
|
{
|
|
$cssFiles[] = $css;
|
|
}
|
|
}
|
|
|
|
\IPS\Output::i()->cssFiles = $cssFiles;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|