mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
Credit to @vojtasvoboda. Will avoid asking the database for the currently active theme if there is only one theme present and its code matches the code set in cms.activeTheme
607 lines
17 KiB
PHP
607 lines
17 KiB
PHP
<?php namespace Cms\Classes;
|
|
|
|
use App;
|
|
use Url;
|
|
use File;
|
|
use Yaml;
|
|
use Lang;
|
|
use Cache;
|
|
use Event;
|
|
use Config;
|
|
use Exception;
|
|
use SystemException;
|
|
use DirectoryIterator;
|
|
use ApplicationException;
|
|
use Cms\Models\ThemeData;
|
|
use System\Models\Parameter;
|
|
use October\Rain\Halcyon\Datasource\DbDatasource;
|
|
use October\Rain\Halcyon\Datasource\FileDatasource;
|
|
use October\Rain\Halcyon\Datasource\DatasourceInterface;
|
|
|
|
/**
|
|
* This class represents the CMS theme.
|
|
* CMS theme is a directory that contains all CMS objects - pages, layouts, partials and asset files..
|
|
* The theme parameters are specified in the theme.ini file in the theme root directory.
|
|
*
|
|
* @package october\cms
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class Theme
|
|
{
|
|
/**
|
|
* @var string Specifies the theme directory name.
|
|
*/
|
|
protected $dirName;
|
|
|
|
/**
|
|
* @var mixed Keeps the cached configuration file values.
|
|
*/
|
|
protected $configCache;
|
|
|
|
/**
|
|
* @var mixed Active theme cache in memory
|
|
*/
|
|
protected static $activeThemeCache = false;
|
|
|
|
/**
|
|
* @var mixed Edit theme cache in memory
|
|
*/
|
|
protected static $editThemeCache = false;
|
|
|
|
const ACTIVE_KEY = 'cms::theme.active';
|
|
const EDIT_KEY = 'cms::theme.edit';
|
|
|
|
/**
|
|
* Loads the theme.
|
|
* @return self
|
|
*/
|
|
public static function load($dirName)
|
|
{
|
|
$theme = new static;
|
|
$theme->setDirName($dirName);
|
|
$theme->registerHalyconDatasource();
|
|
|
|
return $theme;
|
|
}
|
|
|
|
/**
|
|
* Returns the absolute theme path.
|
|
* @param string $dirName Optional theme directory. Defaults to $this->getDirName()
|
|
* @return string
|
|
*/
|
|
public function getPath($dirName = null)
|
|
{
|
|
if (!$dirName) {
|
|
$dirName = $this->getDirName();
|
|
}
|
|
|
|
return themes_path().'/'.$dirName;
|
|
}
|
|
|
|
/**
|
|
* Sets the theme directory name.
|
|
* @return void
|
|
*/
|
|
public function setDirName($dirName)
|
|
{
|
|
$this->dirName = $dirName;
|
|
}
|
|
|
|
/**
|
|
* Returns the theme directory name.
|
|
* @return string
|
|
*/
|
|
public function getDirName()
|
|
{
|
|
return $this->dirName;
|
|
}
|
|
|
|
/**
|
|
* Helper for {{ theme.id }} twig vars
|
|
* Returns a unique string for this theme.
|
|
* @return string
|
|
*/
|
|
public function getId()
|
|
{
|
|
return snake_case(str_replace('/', '-', $this->getDirName()));
|
|
}
|
|
|
|
/**
|
|
* Determines if a theme with given directory name exists
|
|
* @param string $dirName The theme directory
|
|
* @return bool
|
|
*/
|
|
public static function exists($dirName)
|
|
{
|
|
$theme = static::load($dirName);
|
|
$path = $theme->getPath();
|
|
|
|
return File::isDirectory($path);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of pages in the theme.
|
|
* This method is used internally in the routing process and in the back-end UI.
|
|
* @param boolean $skipCache Indicates if the pages should be reloaded from the disk bypassing the cache.
|
|
* @return array Returns an array of \Cms\Classes\Page objects.
|
|
*/
|
|
public function listPages($skipCache = false)
|
|
{
|
|
return Page::listInTheme($this, $skipCache);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this theme is the chosen active theme.
|
|
*/
|
|
public function isActiveTheme()
|
|
{
|
|
$activeTheme = self::getActiveTheme();
|
|
|
|
return $activeTheme && $activeTheme->getDirName() == $this->getDirName();
|
|
}
|
|
|
|
/**
|
|
* Returns the active theme code.
|
|
* By default the active theme is loaded from the cms.activeTheme parameter,
|
|
* but this behavior can be overridden by the cms.theme.getActiveTheme event listener.
|
|
* @return string
|
|
* If the theme doesn't exist, returns null.
|
|
*/
|
|
public static function getActiveThemeCode()
|
|
{
|
|
$activeTheme = Config::get('cms.activeTheme');
|
|
$themes = static::all();
|
|
$havingMoreThemes = count($themes) > 1;
|
|
$themeHasChanged = !empty($themes[0]) && $themes[0]->dirName !== $activeTheme;
|
|
$checkDatabase = $havingMoreThemes || $themeHasChanged;
|
|
|
|
if ($checkDatabase && App::hasDatabase()) {
|
|
try {
|
|
try {
|
|
$dbResult = Cache::remember(self::ACTIVE_KEY, 1440, function () {
|
|
return Parameter::applyKey(self::ACTIVE_KEY)->value('value');
|
|
});
|
|
}
|
|
catch (Exception $ex) {
|
|
// Cache failed
|
|
$dbResult = Parameter::applyKey(self::ACTIVE_KEY)->value('value');
|
|
}
|
|
}
|
|
catch (Exception $ex) {
|
|
// Database failed
|
|
$dbResult = null;
|
|
}
|
|
|
|
if ($dbResult !== null && static::exists($dbResult)) {
|
|
$activeTheme = $dbResult;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @event cms.theme.getActiveTheme
|
|
* Overrides the active theme code.
|
|
*
|
|
* If a value is returned from this halting event, it will be used as the active
|
|
* theme code. Example usage:
|
|
*
|
|
* Event::listen('cms.theme.getActiveTheme', function () {
|
|
* return 'mytheme';
|
|
* });
|
|
*
|
|
*/
|
|
$apiResult = Event::fire('cms.theme.getActiveTheme', [], true);
|
|
if ($apiResult !== null) {
|
|
$activeTheme = $apiResult;
|
|
}
|
|
|
|
if (!strlen($activeTheme)) {
|
|
throw new SystemException(Lang::get('cms::lang.theme.active.not_set'));
|
|
}
|
|
|
|
return $activeTheme;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the active theme object.
|
|
* @return \Cms\Classes\Theme Returns the loaded theme object.
|
|
* If the theme doesn't exist, returns null.
|
|
*/
|
|
public static function getActiveTheme()
|
|
{
|
|
if (self::$activeThemeCache !== false) {
|
|
return self::$activeThemeCache;
|
|
}
|
|
|
|
$theme = static::load(static::getActiveThemeCode());
|
|
|
|
if (!File::isDirectory($theme->getPath())) {
|
|
return self::$activeThemeCache = null;
|
|
}
|
|
|
|
return self::$activeThemeCache = $theme;
|
|
}
|
|
|
|
/**
|
|
* Sets the active theme.
|
|
* The active theme code is stored in the database and overrides the configuration cms.activeTheme parameter.
|
|
* @param string $code Specifies the active theme code.
|
|
*/
|
|
public static function setActiveTheme($code)
|
|
{
|
|
self::resetCache();
|
|
|
|
Parameter::set(self::ACTIVE_KEY, $code);
|
|
|
|
/**
|
|
* @event cms.theme.setActiveTheme
|
|
* Fires when the active theme has been changed.
|
|
*
|
|
* If a value is returned from this halting event, it will be used as the active
|
|
* theme code. Example usage:
|
|
*
|
|
* Event::listen('cms.theme.setActiveTheme', function ($code) {
|
|
* \Log::info("Theme has been changed to $code");
|
|
* });
|
|
*
|
|
*/
|
|
Event::fire('cms.theme.setActiveTheme', compact('code'));
|
|
}
|
|
|
|
/**
|
|
* Returns the edit theme code.
|
|
* By default the edit theme is loaded from the cms.editTheme parameter,
|
|
* but this behavior can be overridden by the cms.theme.getEditTheme event listeners.
|
|
* If the edit theme is not defined in the configuration file, the active theme
|
|
* is returned.
|
|
* @return string
|
|
*/
|
|
public static function getEditThemeCode()
|
|
{
|
|
$editTheme = Config::get('cms.editTheme');
|
|
if (!$editTheme) {
|
|
$editTheme = static::getActiveThemeCode();
|
|
}
|
|
|
|
/**
|
|
* @event cms.theme.getEditTheme
|
|
* Overrides the edit theme code.
|
|
*
|
|
* If a value is returned from this halting event, it will be used as the edit
|
|
* theme code. Example usage:
|
|
*
|
|
* Event::listen('cms.theme.getEditTheme', function () {
|
|
* return "the-edit-theme-code";
|
|
* });
|
|
*
|
|
*/
|
|
$apiResult = Event::fire('cms.theme.getEditTheme', [], true);
|
|
if ($apiResult !== null) {
|
|
$editTheme = $apiResult;
|
|
}
|
|
|
|
if (!strlen($editTheme)) {
|
|
throw new SystemException(Lang::get('cms::lang.theme.edit.not_set'));
|
|
}
|
|
|
|
return $editTheme;
|
|
}
|
|
|
|
/**
|
|
* Returns the edit theme.
|
|
* @return \Cms\Classes\Theme Returns the loaded theme object.
|
|
*/
|
|
public static function getEditTheme()
|
|
{
|
|
if (self::$editThemeCache !== false) {
|
|
return self::$editThemeCache;
|
|
}
|
|
|
|
$theme = static::load(static::getEditThemeCode());
|
|
|
|
if (!File::isDirectory($theme->getPath())) {
|
|
return self::$editThemeCache = null;
|
|
}
|
|
|
|
return self::$editThemeCache = $theme;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all themes.
|
|
* @return array Returns an array of the Theme objects.
|
|
*/
|
|
public static function all()
|
|
{
|
|
$it = new DirectoryIterator(themes_path());
|
|
$it->rewind();
|
|
|
|
$result = [];
|
|
foreach ($it as $fileinfo) {
|
|
if (!$fileinfo->isDir() || $fileinfo->isDot()) {
|
|
continue;
|
|
}
|
|
|
|
$theme = static::load($fileinfo->getFilename());
|
|
|
|
$result[] = $theme;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Reads the theme.yaml file and returns the theme configuration values.
|
|
* @return array Returns the parsed configuration file values.
|
|
*/
|
|
public function getConfig()
|
|
{
|
|
if ($this->configCache !== null) {
|
|
return $this->configCache;
|
|
}
|
|
|
|
$path = $this->getPath().'/theme.yaml';
|
|
if (!File::exists($path)) {
|
|
return $this->configCache = [];
|
|
}
|
|
|
|
$config = Yaml::parseFile($path);
|
|
|
|
/**
|
|
* @event cms.theme.extendConfig
|
|
* Extend basic theme configuration supplied by the theme by returning an array.
|
|
*
|
|
* Note if planning on extending form fields, use the `cms.theme.extendFormConfig`
|
|
* event instead.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('cms.theme.extendConfig', function ($themeCode, &$config) {
|
|
* $config['name'] = 'October Theme';
|
|
* $config['description'] = 'Another great theme from October CMS';
|
|
* });
|
|
*
|
|
*/
|
|
Event::fire('cms.theme.extendConfig', [$this->getDirName(), &$config]);
|
|
|
|
return $this->configCache = $config;
|
|
}
|
|
|
|
/**
|
|
* Themes have a dedicated `form` option that provide form fields
|
|
* for customization, this is an immutable accessor for that and
|
|
* also an solid anchor point for extension.
|
|
* @return array
|
|
*/
|
|
public function getFormConfig()
|
|
{
|
|
$config = $this->getConfigArray('form');
|
|
|
|
/**
|
|
* @event cms.theme.extendFormConfig
|
|
* Extend form field configuration supplied by the theme by returning an array.
|
|
*
|
|
* Note if you are planning on using `assetVar` to inject CSS variables from a
|
|
* plugin registration file, make sure the plugin has elevated permissions.
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Event::listen('cms.theme.extendFormConfig', function ($themeCode, &$config) {
|
|
* array_set($config, 'tabs.fields.header_color', [
|
|
* 'label' => 'Header Colour',
|
|
* 'type' => 'colorpicker',
|
|
* 'availableColors' => [#34495e, #708598, #3498db],
|
|
* 'assetVar' => 'header-bg',
|
|
* 'tab' => 'Global'
|
|
* ]);
|
|
* });
|
|
*
|
|
*/
|
|
Event::fire('cms.theme.extendFormConfig', [$this->getDirName(), &$config]);
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Returns a value from the theme configuration file by its name.
|
|
* @param string $name Specifies the configuration parameter name.
|
|
* @param mixed $default Specifies the default value to return in case if the parameter
|
|
* doesn't exist in the configuration file.
|
|
* @return mixed Returns the parameter value or a default value
|
|
*/
|
|
public function getConfigValue($name, $default = null)
|
|
{
|
|
return array_get($this->getConfig(), $name, $default);
|
|
}
|
|
|
|
/**
|
|
* Returns an array value from the theme configuration file by its name.
|
|
* If the value is a string, it is treated as a YAML file and loaded.
|
|
* @param string $name Specifies the configuration parameter name.
|
|
* @return array
|
|
*/
|
|
public function getConfigArray($name)
|
|
{
|
|
$result = array_get($this->getConfig(), $name, []);
|
|
|
|
if (is_string($result)) {
|
|
$fileName = File::symbolizePath($result);
|
|
|
|
if (File::isLocalPath($fileName)) {
|
|
$path = $fileName;
|
|
}
|
|
else {
|
|
$path = $this->getPath().'/'.$result;
|
|
}
|
|
|
|
if (!File::exists($path)) {
|
|
throw new ApplicationException('Path does not exist: '.$path);
|
|
}
|
|
|
|
$result = Yaml::parseFile($path);
|
|
}
|
|
|
|
return (array) $result;
|
|
}
|
|
|
|
/**
|
|
* Writes to the theme.yaml file with the supplied array values.
|
|
* @param array $values Data to write
|
|
* @param array $overwrite If true, undefined values are removed.
|
|
* @return void
|
|
*/
|
|
public function writeConfig($values = [], $overwrite = false)
|
|
{
|
|
if (!$overwrite) {
|
|
$values = $values + (array) $this->getConfig();
|
|
}
|
|
|
|
$path = $this->getPath().'/theme.yaml';
|
|
if (!File::exists($path)) {
|
|
throw new ApplicationException('Path does not exist: '.$path);
|
|
}
|
|
|
|
$contents = Yaml::render($values);
|
|
File::put($path, $contents);
|
|
$this->configCache = $values;
|
|
|
|
self::resetCache();
|
|
}
|
|
|
|
/**
|
|
* Returns the theme preview image URL.
|
|
* If the image file doesn't exist returns the placeholder image URL.
|
|
* @return string Returns the image URL.
|
|
*/
|
|
public function getPreviewImageUrl()
|
|
{
|
|
$previewPath = $this->getConfigValue('previewImage', 'assets/images/theme-preview.png');
|
|
|
|
if (File::exists($this->getPath().'/'.$previewPath)) {
|
|
return Url::asset('themes/'.$this->getDirName().'/'.$previewPath);
|
|
}
|
|
|
|
return Url::asset('modules/cms/assets/images/default-theme-preview.png');
|
|
}
|
|
|
|
/**
|
|
* Resets any memory or cache involved with the active or edit theme.
|
|
* @return void
|
|
*/
|
|
public static function resetCache()
|
|
{
|
|
self::$activeThemeCache = false;
|
|
self::$editThemeCache = false;
|
|
|
|
Cache::forget(self::ACTIVE_KEY);
|
|
Cache::forget(self::EDIT_KEY);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this theme has form fields that supply customization data.
|
|
* @return bool
|
|
*/
|
|
public function hasCustomData()
|
|
{
|
|
return $this->getConfigValue('form', false);
|
|
}
|
|
|
|
/**
|
|
* Returns data specific to this theme
|
|
* @return Cms\Models\ThemeData
|
|
*/
|
|
public function getCustomData()
|
|
{
|
|
return ThemeData::forTheme($this);
|
|
}
|
|
|
|
/**
|
|
* Remove data specific to this theme
|
|
* @return bool
|
|
*/
|
|
public function removeCustomData()
|
|
{
|
|
if ($this->hasCustomData()) {
|
|
return $this->getCustomData()->delete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the database layer has been enabled
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public static function databaseLayerEnabled()
|
|
{
|
|
$enableDbLayer = Config::get('cms.databaseTemplates', false);
|
|
if (is_null($enableDbLayer)) {
|
|
$enableDbLayer = !Config::get('app.debug', false);
|
|
}
|
|
|
|
return $enableDbLayer && App::hasDatabase();
|
|
}
|
|
|
|
/**
|
|
* Ensures this theme is registered as a Halcyon datasource.
|
|
* @return void
|
|
*/
|
|
public function registerHalyconDatasource()
|
|
{
|
|
$resolver = App::make('halcyon');
|
|
|
|
if (!$resolver->hasDatasource($this->dirName)) {
|
|
if (static::databaseLayerEnabled()) {
|
|
$datasource = new AutoDatasource([
|
|
'database' => new DbDatasource($this->dirName, 'cms_theme_templates'),
|
|
'filesystem' => new FileDatasource($this->getPath(), App::make('files')),
|
|
]);
|
|
} else {
|
|
$datasource = new FileDatasource($this->getPath(), App::make('files'));
|
|
}
|
|
|
|
$resolver->addDatasource($this->dirName, $datasource);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the theme's datasource
|
|
*
|
|
* @return DatasourceInterface
|
|
*/
|
|
public function getDatasource()
|
|
{
|
|
$resolver = App::make('halcyon');
|
|
return $resolver->datasource($this->getDirName());
|
|
}
|
|
|
|
/**
|
|
* Implements the getter functionality.
|
|
* @param string $name
|
|
* @return void
|
|
*/
|
|
public function __get($name)
|
|
{
|
|
if ($this->hasCustomData()) {
|
|
return $this->getCustomData()->{$name};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Determine if an attribute exists on the object.
|
|
* @param string $key
|
|
* @return void
|
|
*/
|
|
public function __isset($key)
|
|
{
|
|
if ($this->hasCustomData()) {
|
|
$theme = $this->getCustomData();
|
|
return $theme->offsetExists($key);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|