Samuel Georges fb2aa1730c Fixes security issue
Refs #3604
2018-06-22 22:57:38 +10:00

473 lines
13 KiB
PHP

<?php namespace Cms\Classes;
use App;
use Url;
use File;
use Yaml;
use Lang;
use Cache;
use Event;
use Config;
use Cms\Models\ThemeData;
use System\Models\Parameter;
use October\Rain\Halcyon\Datasource\FileDatasource;
use ApplicationException;
use SystemException;
use DirectoryIterator;
use Exception;
/**
* 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 = null;
/**
* @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');
if (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::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();
}
$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 = [];
}
return $this->configCache = Yaml::parseFile($path);
}
/**
* 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;
}
/**
* 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);
}
/**
* Ensures this theme is registered as a Halcyon them datasource.
* @return void
*/
public function registerHalyconDatasource()
{
$resolver = App::make('halcyon');
if (!$resolver->hasDatasource($this->dirName)) {
$datasource = new FileDatasource($this->getPath(), App::make('files'));
$resolver->addDatasource($this->dirName, $datasource);
}
}
/**
* Implements the getter functionality.
* @param string $name
* @return void
*/
public function __get($name)
{
if ($this->hasCustomData()) {
$theme = $this->getCustomData();
return $theme->{$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;
}
}