preparing for re-design using new template-engine for future 0.11.x releases

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2022-02-15 11:18:19 +01:00
parent 1d7d32130a
commit 56a9a71248
39 changed files with 1685 additions and 1397 deletions

View File

@@ -47,7 +47,7 @@ class PhpHelper
* @param string $charset
* See php documentation about this
*
* @return array The array with htmlentitie'd strings
* @return array|string The string or an array with htmlentities converted strings
* @author Florian Lippert <flo@syscp.org>
*/
public static function htmlentitiesArray($subject, $fields = '', $quote_style = ENT_QUOTES, $charset = 'UTF-8')
@@ -64,7 +64,7 @@ class PhpHelper
}
}
} else {
$subject = htmlentities($subject, $quote_style, $charset);
$subject = empty($subject) ? "" : htmlentities($subject, $quote_style, $charset);
}
return $subject;
@@ -120,22 +120,30 @@ class PhpHelper
}
if (! isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) {
global $theme;
// fallback
if (empty($theme)) {
$theme = "Sparkle";
}
// prevent possible file-path-disclosure
$errfile = str_replace(\Froxlor\Froxlor::getInstallDir(), "", $errfile);
// if we're not on the shell, output a nicer error-message
$err_hint = file_get_contents(\Froxlor\Froxlor::getInstallDir() . '/templates/' . $theme . '/misc/phperrornice.tpl');
// replace values
$err_hint = str_replace("<TEXT>", '#' . $errno . ' ' . $errstr, $err_hint);
$err_hint = str_replace("<DEBUG>", $errfile . ':' . $errline, $err_hint);
// show
echo $err_hint;
// build alert
$type = 'danger';
if ($errno == E_NOTICE || $errno == E_DEPRECATED || $errno == E_STRICT) {
$type = 'info';
} elseif ($errno = E_WARNING) {
$type = 'warning';
}
$err_display = '<div class="alert alert-'.$type.' my-1" role="alert">';
$err_display .= '<strong>#' . $errno . ' ' . $errstr . '</strong><br>';
$err_display .= $errfile . ':' . $errline;
// later depended on whether to show or now
$err_display .= '<br><p><pre>';
$debug = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
foreach ($debug as $dline) {
$err_display .= $dline['function'] . '() called at [' . str_replace(\Froxlor\Froxlor::getInstallDir(), '', $dline['file']) . ':' . $dline['line'] . ']<br>';
}
$err_display .= '</pre></p>';
// end later
$err_display .= '</div>';
// check for more existing errors
$errors = isset(\Froxlor\UI\Panel\UI::Twig()->getGlobals()['global_errors']) ? \Froxlor\UI\Panel\UI::Twig()->getGlobals()['global_errors'] : "";
\Froxlor\UI\Panel\UI::Twig()->addGlobal('global_errors', $errors . $err_display);
// return true to ignore php standard error-handler
return true;
}
@@ -144,6 +152,28 @@ class PhpHelper
return false;
}
public static function phpExceptionHandler(\Exception $exception)
{
if (! isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) {
$err_display = '<div class="alert alert-danger my-1" role="alert">';
$err_display .= '<strong>#' . $exception->getCode() . ' ' . $exception->getMessage() . '</strong><br>';
// later depended on whether to show or now
$err_display .= '<br><p><pre>';
$debug = $exception->getTrace();
foreach ($debug as $dline) {
$err_display .= $dline['function'] . '() called at [' . str_replace(\Froxlor\Froxlor::getInstallDir(), '', $dline['file']) . ':' . $dline['line'] . ']<br>';
}
$err_display .= '</pre></p>';
// end later
$err_display .= '</div>';
// check for more existing errors
$errors = isset(\Froxlor\UI\Panel\UI::Twig()->getGlobals()['global_errors']) ? \Froxlor\UI\Panel\UI::Twig()->getGlobals()['global_errors'] : "";
\Froxlor\UI\Panel\UI::Twig()->addGlobal('global_errors', $errors . $err_display);
// return true to ignore php standard error-handler
return true;
}
}
public static function loadConfigArrayDir()
{
// Workaround until we use gettext

View File

@@ -0,0 +1,54 @@
<?php
namespace Froxlor\UI\Panel;
class CustomReflection extends \Twig\Extension\AbstractExtension
{
/**
*
* {@inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig\TwigFunction('call_static', [
$this,
'callStaticMethod'
]),
new \Twig\TwigFunction('get_static', [
$this,
'getStaticProperty'
])
);
}
public function callStaticMethod($class, $method, array $args = [])
{
$refl = new \reflectionClass($class);
// Check that method is static AND public
if ($refl->hasMethod($method) && $refl->getMethod($method)->isStatic() && $refl->getMethod($method)->isPublic()) {
return call_user_func_array($class . '::' . $method, $args);
}
throw new \RuntimeException(sprintf('Invalid static method call for class %s and method %s', $class, $method));
}
public function getStaticProperty($class, $property)
{
$refl = new \reflectionClass($class);
// Check that property is static AND public
if ($refl->hasProperty($property) && $refl->getProperty($property)->isStatic() && $refl->getProperty($property)->isPublic()) {
return $refl->getProperty($property)->getValue();
}
throw new \RuntimeException(sprintf('Invalid static property get for class %s and property %s', $class, $property));
}
/**
*
* {@inheritdoc}
*/
public function getName()
{
return 'customreflection';
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Froxlor\UI\Panel;
class FroxlorTwig extends \Twig\Extension\AbstractExtension
{
public function getFilters()
{
return array(
new \Twig\TwigFilter('formatBytes', array(
$this,
'formatBytesFilter'
)),
new \Twig\TwigFilter('formatIP', array(
$this,
'formatIPFilter'
)),
new \Twig\TwigFilter('idnDecode', array(
$this,
'idnDecodeFilter'
))
);
}
public function getTests()
{
return array(
new \Twig\TwigTest('numeric', function ($value) {
return is_numeric($value);
})
);
}
public function getFunctions()
{
return array(
new \Twig\TwigFunction('get_setting', [
$this,
'getSetting'
]),
new \Twig\TwigFunction('lng', [
$this,
'getLang'
])
);
}
public function formatBytesFilter($size, $suffix = "B", $factor = 1)
{
$size = $size * $factor;
$units = array(
'',
'K',
'M',
'G',
'T',
'P',
'E',
'Z',
'Y'
);
$power = $size > 0 ? floor(log($size, 1024)) : 0;
if ($power < 0) {
$size = 0.00;
$power = 0;
}
return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $units[$power] . $suffix;
}
public function formatIPFilter($addr)
{
return inet_ntop(inet_pton($addr));
}
public function idnDecodeFilter($entity)
{
$idna_convert = new \Froxlor\Idna\IdnaWrapper();
return $idna_convert->decode($entity);
}
public function getSetting($setting = null)
{
return \Froxlor\Settings::Get($setting);
}
public function getLang($identifier = null)
{
return \Froxlor\UI\Panel\UI::getLng($identifier);
}
/**
*
* {@inheritdoc}
*/
public function getName()
{
return 'froxlortwig';
}
}

274
lib/Froxlor/UI/Panel/UI.php Normal file
View File

@@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
namespace Froxlor\UI\Panel;
class UI
{
/**
* twig object
*
* @var \Twig\Environment
*/
private static $twig = null;
/**
* twig buffer
*
* @var array
*/
private static $twigbuf = array();
/**
* language strigs array
*
* @var array
*/
private static $lng = array();
/**
* default fallback theme
*
* @var string
*/
private static $default_theme = 'Sparkle2';
private static $install_mode = false;
/**
* send various security related headers
*/
public static function sendHeaders()
{
header("Content-Type: text/html; charset=UTF-8");
// prevent Froxlor pages from being cached
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', time()));
header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time()));
// Prevent inline - JS to be executed (i.e. XSS) in browsers which support this,
// Inline-JS is no longer allowed and used
// See: http://people.mozilla.org/~bsterne/content-security-policy/index.html
// New stuff see: https://www.owasp.org/index.php/List_of_useful_HTTP_headers and https://www.owasp.org/index.php/Content_Security_Policy
$csp_content = "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';";
header("Content-Security-Policy: " . $csp_content);
header("X-Content-Security-Policy: " . $csp_content);
header("X-WebKit-CSP: " . $csp_content);
header("X-XSS-Protection: 1; mode=block");
// Don't allow to load Froxlor in an iframe to prevent i.e. clickjacking
header("X-Frame-Options: DENY");
// Internet Explorer shall not guess the Content-Type, see:
// http://blogs.msdn.com/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx
header("X-Content-Type-Options: nosniff");
// ensure that default timezone is set
if (function_exists("date_default_timezone_set") && function_exists("date_default_timezone_get")) {
@date_default_timezone_set(@date_default_timezone_get());
}
self::sendSslHeaders();
}
private static function sendSslHeaders()
{
/**
* If Froxlor was called via HTTPS -> enforce it for the next time by settings HSTS header according to settings
*/
if (isset($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off')) {
$maxage = \Froxlor\Settings::Get('system.hsts_maxage');
if (empty($maxage)) {
$maxage = 0;
}
$hsts_header = "Strict-Transport-Security: max-age=" . $maxage;
if (\Froxlor\Settings::Get('system.hsts_incsub') == '1') {
$hsts_header .= "; includeSubDomains";
}
if (\Froxlor\Settings::Get('system.hsts_preload') == '1') {
$hsts_header .= "; preload";
}
header($hsts_header);
}
}
/**
* initialize Twig template engine
*/
public static function initTwig(bool $install_mode = false)
{
self::$install_mode = $install_mode;
// init twig template engine
$loader = new \Twig\Loader\FilesystemLoader(\Froxlor\Froxlor::getInstallDir() . '/templates/');
self::$twig = new \Twig\Environment($loader, array(
'debug' => true,
'cache' => \Froxlor\Froxlor::getInstallDir() . '/cache',
'auto_reload' => true
));
self::$twig->addExtension(new \Twig\Extension\DebugExtension());
self::$twig->addExtension(new CustomReflection());
self::$twig->addExtension(new FroxlorTwig());
// empty buffer
self::$twigbuf = [];
}
/**
* twig wrapper
*
* @return \Twig\Environment
*/
public static function Twig()
{
return self::$twig;
}
/**
* wrapper for twig's "render" function to buffer the output
*
* @see \Twig\Environment::render()
*/
public static function TwigBuffer($name, array $context = [])
{
self::$twigbuf[] = [
self::getTheme() . '/' . $name => $context
];
}
public static function getTheme()
{
// fallback
$theme = self::$default_theme;
if (!self::$install_mode) {
// system default
if (\Froxlor\Froxlor::DBVERSION <= 201909150) {
\Froxlor\Settings::Set('panel.default_theme', 'Sparkle2');
}
$theme = (\Froxlor\Settings::Get('panel.default_theme') !== null) ? \Froxlor\Settings::Get('panel.default_theme') : $theme;
// customer theme
/*
if (\Froxlor\CurrentUser::hasSession() && \Froxlor\CurrentUser::getField('theme') != $theme) {
$theme = \Froxlor\CurrentUser::getField('theme');
}
*/
}
if (!file_exists(\Froxlor\Froxlor::getInstallDir() . '/templates/' . $theme)) {
\Froxlor\PhpHelper::phpErrHandler(E_USER_WARNING, "Theme '" . $theme . "' could not be found.", __FILE__, __LINE__, null);
$theme = self::$default_theme;
}
return $theme;
}
/**
* echo output buffer and empty buffer-content
*/
public static function TwigOutputBuffer()
{
$output = "";
foreach (self::$twigbuf as $buf) {
foreach ($buf as $name => $context) {
try {
$output .= self::$twig->render($name, $context);
} catch (\Exception $e) {
// whoops, template error
$errtpl = 'alert_nosession.html.twig';
/*
if (\Froxlor\CurrentUser::hasSession()) {
$errtpl = 'alert.html.twig';
}
*/
$edata = array(
'type' => "danger",
'heading' => "Template error",
'alert_msg' => $e->getMessage(),
'alert_info' => $e->getTraceAsString()
);
try {
// try with user theme if set
$output .= self::$twig->render(self::getTheme() . '/misc/' . $errtpl, $edata);
} catch (\Exception $e) {
// try with default theme if different from user theme
if (self::getTheme() != self::$default_theme) {
$output .= self::$twig->render(self::$default_theme . '/misc/' . $errtpl, $edata);
} else {
throw $e;
}
}
}
}
}
echo $output;
// empty buffer
self::$twigbuf = [];
}
public static function setLng($lng = array())
{
self::$lng = $lng;
}
public static function getLng($identifier, $context = null)
{
$id = explode(".", $identifier);
if (is_null($context)) {
$id_first = array_shift($id);
if (!isset(self::$lng[$id_first])) {
return null;
}
if (empty($id)) {
return self::$lng[$id_first];
} else {
return self::getLng(implode(".", $id), self::$lng[$id_first]);
}
} else {
$id_first = array_shift($id);
if (empty($id)) {
return isset($context[$id_first]) ? $context[$id_first] : null;
} else {
return self::getLng(implode(".", $id), $context[$id_first]);
}
}
}
/**
* returns an array of available themes
*
* @return array
*/
public static function getThemes()
{
$themespath = \Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . '/templates/');
$themes_available = array();
if (is_dir($themespath)) {
$its = new \DirectoryIterator($themespath);
foreach ($its as $it) {
if ($it->isDir() && $it->getFilename() != '.' && $it->getFilename() != '..' && $it->getFilename() != 'misc') {
$theme = $themespath . $it->getFilename();
if (file_exists($theme . '/config.json')) {
$themeconfig = json_decode(file_get_contents($theme . '/config.json'), true);
if (array_key_exists('variants', $themeconfig) && is_array($themeconfig['variants'])) {
foreach ($themeconfig['variants'] as $variant => $data) {
if ($variant == "default") {
$themes_available[$it->getFilename()] = $it->getFilename();
} elseif (array_key_exists('description', $data)) {
$themes_available[$it->getFilename() . '_' . $variant] = $data['description'];
} else {
$themes_available[$it->getFilename() . '_' . $variant] = $it->getFilename() . ' (' . $variant . ')';
}
}
} else {
$themes_available[$it->getFilename()] = $it->getFilename();
}
}
}
}
}
return $themes_available;
}
}