1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
<?php
/**
* Turn on sanitisation of all data by default so it's not possible for XSS flaws to occur in PFA
*/
class PFASmarty
{
public static ?self $instance = null;
protected Smarty $template;
public static function getInstance()
{
if (self::$instance) {
return self::$instance;
}
self::$instance = new PFASmarty();
return self::$instance;
}
private function __construct()
{
$CONF = Config::getInstance()->getAll();
$theme = '';
if (isset($CONF['theme']) && is_dir(dirname(__FILE__) . "/../templates/" . $CONF['theme'])) {
$theme = $CONF['theme'];
}
$this->template = new Smarty();
$this->template->registerPlugin('function', 'htmlentities', 'htmlentities');
$this->template->registerPlugin('modifier', 'htmlentities_no_double_encode', function (string $string) {
return htmlentities($string, ENT_QUOTES, 'UTF-8', false);
});
$template_dir = __DIR__ . '/../templates/' . $theme;
if (!is_dir($template_dir)) {
$template_dir = __DIR__ . '/../templates/';
}
$this->template->setTemplateDir($template_dir);
// if it's not present or writeable, smarty should just not cache.
$templates_c = dirname(__FILE__) . '/../templates_c';
if (is_dir($templates_c) && is_writeable($templates_c)) {
$this->template->setCompileDir($templates_c);
} else {
# unfortunately there's no sane way to just disable compiling of templates
clearstatcache(); // just incase someone just fixed it; on their next refresh it should work.
error_log("ERROR: directory $templates_c doesn't exist or isn't writeable for the webserver");
die("ERROR: the templates_c directory doesn't exist or isn't writeable for the webserver");
}
$this->configureTheme('');// default to something.
}
/**
* get relative path to css etc. based on permissions (user vs. admin)
* Note: this function only works after login
*/
public function getRelPath()
{
if (authentication_has_role('user')) {
return '../';
} else {
return '';
}
}
/**
* @param string $rel_path - relative path for referenced css etc dependencies - e.g. users/edit.php needs '../' else, it's ''.
*/
public function configureTheme(string $rel_path = '')
{
$CONF = Config::getInstance()->getAll();
// see: https://github.com/postfixadmin/postfixadmin/issues/410
// ignore $CONF['theme_css'] if it points to css/default.css and we have static/bootstrap.css.
if ($CONF['theme_css'] == 'css/default.css' && is_file(__DIR__ . '/../public/static/bootstrap.css')) {
// silently upgrade to bootstrap, css/default.css does not exist.
$CONF['theme_css'] = 'static/bootstrap.css';
}
$CONF['theme_css'] = $rel_path . htmlentities($CONF['theme_css']);
if (!empty($CONF['theme_custom_css'])) {
$CONF['theme_custom_css'] = $rel_path . htmlentities($CONF['theme_custom_css']);
}
if (array_key_exists('theme_favicon', $CONF)) {
$CONF['theme_favicon'] = $rel_path . htmlentities($CONF['theme_favicon']);
}
$CONF['theme_logo'] = $rel_path . htmlentities($CONF['theme_logo']);
$this->assign('rel_path', $rel_path);
$this->assign('CONF', $CONF);
}
/**
* @param string $key
* @param mixed $value
* @param bool $sanitise
*/
public function assign($key, $value, $sanitise = true)
{
$this->template->assign("RAW_$key", $value);
if ($sanitise == false) {
return $this->template->assign($key, $value);
}
$clean = $this->sanitise($value);
/* we won't run the key through sanitise() here... some might argue we should */
return $this->template->assign($key, $clean);
}
/**
* @param string $template
* @return void
*/
public function display($template)
{
$CONF = Config::getInstance()->getAll();
$this->assign('PALANG', $CONF['__LANG'] ?? []);
$this->assign('url_domain', '');
$this->assign('version', $CONF['version'] ?? 'unknown');
$this->assign('boolconf_alias_domain', Config::bool('alias_domain'));
$this->assign('boolconf_dkim', Config::bool('dkim'));
$this->assign('boolconf_dkim_all_admins', Config::bool('dkim_all_admins'));
$this->assign('authentication_has_role', array('global_admin' => authentication_has_role('global-admin'), 'admin' => authentication_has_role('admin'), 'user' => authentication_has_role('user')));
header("Expires: Sun, 16 Mar 2003 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Content-Type: text/html; charset=UTF-8");
$this->template->setConfigDir(__DIR__ . '/../configs');
$this->template->display($template);
unset($_SESSION['flash']); # cleanup flash messages
}
/**
* Recursive cleaning of data, using htmlentities - this assumes we only ever output to HTML and we're outputting in UTF-8 charset
*
* @param mixed $data - array or primitive type; objects not supported.
* @return mixed $data
* */
public function sanitise($data)
{
if (!is_array($data) && !is_string($data)) {
return $data; // bool, int, null, object etc - can't sanitise.
}
if (is_string($data)) {
return htmlentities($data, ENT_QUOTES, 'UTF-8', false);
}
$clean = array();
foreach ($data as $key => $value) {
/* as this is a nested data structure it's more likely we'll output the key too (at least in my opinion, so we'll sanitise it too */
$clean[$this->sanitise($key)] = $this->sanitise($value);
}
return $clean;
}
}
|