Config class overhaul.

This commit is contained in:
flash 2023-07-18 21:48:44 +00:00
parent cecfaf4852
commit 2f7cddde19
30 changed files with 838 additions and 396 deletions

View file

@ -36,10 +36,13 @@
.manage-list-setting-type--string { .manage-list-setting-type--string {
--type-colour: #ef8323; --type-colour: #ef8323;
} }
.manage-list-setting-type--integer { .manage-list-setting-type--int {
--type-colour: #8c90bc; --type-colour: #8c90bc;
} }
.manage-list-setting-type--boolean { .manage-list-setting-type--float {
--type-colour: #cb09c8;
}
.manage-list-setting-type--bool {
--type-colour: #77b34c; --type-colour: #77b34c;
} }
.manage-list-setting-type--array { .manage-list-setting-type--array {

View file

@ -5,7 +5,6 @@ use Index\Autoloader;
use Index\Environment; use Index\Environment;
use Index\Data\ConnectionFailedException; use Index\Data\ConnectionFailedException;
use Index\Data\DbTools; use Index\Data\DbTools;
use Misuzu\Config\IConfig;
use Misuzu\Config\DbConfig; use Misuzu\Config\DbConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException; use Misuzu\Users\UserNotFoundException;
@ -47,7 +46,7 @@ set_exception_handler(function(\Throwable $ex) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo (string)$ex; echo (string)$ex;
} else { } else {
header('Content-Type: text/html; charset-utf-8'); header('Content-Type: text/html; charset=utf-8');
echo file_get_contents(MSZ_TEMPLATES . '/500.html'); echo file_get_contents(MSZ_TEMPLATES . '/500.html');
} }
} }
@ -83,24 +82,8 @@ DB::init(DbTools::parse($dbConfig['dsn']));
DB::exec(MSZ_DB_INIT); DB::exec(MSZ_DB_INIT);
$cfg = new DbConfig($db); $cfg = new DbConfig($db);
$cfg->reload();
Config::init($cfg); Mailer::init($cfg->scopeTo('mail'));
Mailer::init($cfg->getValue('mail.method', IConfig::T_STR), [
'host' => $cfg->getValue('mail.host', IConfig::T_STR),
'port' => $cfg->getValue('mail.port', IConfig::T_INT, 25),
'username' => $cfg->getValue('mail.username', IConfig::T_STR),
'password' => $cfg->getValue('mail.password', IConfig::T_STR),
'encryption' => $cfg->getValue('mail.encryption', IConfig::T_STR),
'sender_name' => $cfg->getValue('mail.sender.name', IConfig::T_STR),
'sender_addr' => $cfg->getValue('mail.sender.address', IConfig::T_STR),
]);
// replace this with a better storage mechanism
define('MSZ_STORAGE', $cfg->getValue('storage.path', IConfig::T_STR, MSZ_ROOT . '/store'));
if(!is_dir(MSZ_STORAGE))
mkdir(MSZ_STORAGE, 0775, true);
$msz = new MisuzuContext($db, $cfg); $msz = new MisuzuContext($db, $cfg);
@ -115,17 +98,12 @@ ob_start();
if(file_exists(MSZ_ROOT . '/.migrating')) { if(file_exists(MSZ_ROOT . '/.migrating')) {
http_response_code(503); http_response_code(503);
if(!isset($_GET['_check'])) { if(!isset($_GET['_check'])) {
header('Content-Type: text/html; charset-utf-8'); header('Content-Type: text/html; charset=utf-8');
echo file_get_contents(MSZ_TEMPLATES . '/503.html'); echo file_get_contents(MSZ_TEMPLATES . '/503.html');
} }
exit; exit;
} }
if(!is_readable(MSZ_STORAGE) || !is_writable(MSZ_STORAGE)) {
echo 'Cannot access storage directory.';
exit;
}
if(!MSZ_DEBUG) { if(!MSZ_DEBUG) {
$twigCacheDirSfx = GitInfo::hash(true); $twigCacheDirSfx = GitInfo::hash(true);
if(empty($twigCacheDirSfx)) if(empty($twigCacheDirSfx))
@ -136,16 +114,27 @@ if(!MSZ_DEBUG) {
mkdir($twigCache, 0775, true); mkdir($twigCache, 0775, true);
} }
$globals = $cfg->getValues([
['site.name:s', 'Misuzu'],
'site.desc:s',
'site.url:s',
'sockChat.chatPath.normal:s',
'eeprom.path:s',
'eeprom.app:s',
['auth.secret:s', 'meow'],
['csrf.secret:s', 'soup'],
]);
Template::init($msz, $twigCache ?? null, MSZ_DEBUG); Template::init($msz, $twigCache ?? null, MSZ_DEBUG);
Template::set('globals', [ Template::set('globals', [
'site_name' => $cfg->getValue('site.name', IConfig::T_STR, 'Misuzu'), 'site_name' => $globals['site.name'],
'site_description' => $cfg->getValue('site.desc', IConfig::T_STR), 'site_description' => $globals['site.desc'],
'site_url' => $cfg->getValue('site.url', IConfig::T_STR), 'site_url' => $globals['site.url'],
'site_chat' => $cfg->getValue('sockChat.chatPath.normal', IConfig::T_STR), 'site_chat' => $globals['sockChat.chatPath.normal'],
'eeprom' => [ 'eeprom' => [
'path' => $cfg->getValue('eeprom.path', IConfig::T_STR), 'path' => $globals['eeprom.path'],
'app' => $cfg->getValue('eeprom.app', IConfig::T_STR), 'app' => $globals['eeprom.app'],
], ],
]); ]);
@ -156,7 +145,7 @@ unset($mszAssetsInfo);
Template::addPath(MSZ_TEMPLATES); Template::addPath(MSZ_TEMPLATES);
AuthToken::setSecretKey($cfg->getValue('auth.secret', IConfig::T_STR, 'meow')); AuthToken::setSecretKey($globals['auth.secret']);
if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) { if(isset($_COOKIE['msz_uid']) && isset($_COOKIE['msz_sid'])) {
$authToken = new AuthToken; $authToken = new AuthToken;
@ -220,22 +209,21 @@ if($authToken->isValid()) {
} }
CSRF::init( CSRF::init(
$cfg->getValue('csrf.secret', IConfig::T_STR, 'soup'), $globals['csrf.secret'],
(UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : ($_SERVER['REMOTE_ADDR'] ?? '::1')) (UserSession::hasCurrent() ? UserSession::getCurrent()->getToken() : ($_SERVER['REMOTE_ADDR'] ?? '::1'))
); );
function mszLockdown(): void { function mszLockdown(): void {
global $misuzuBypassLockdown, $cfg; global $misuzuBypassLockdown, $cfg;
if($cfg->getValue('private.enabled', IConfig::T_BOOL)) { if($cfg->getBoolean('private.enabled')) {
$onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login'); $onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login');
$onPasswordPage = parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH) === url('auth-forgot'); $onPasswordPage = parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH) === url('auth-forgot');
$misuzuBypassLockdown = !empty($misuzuBypassLockdown) || $onLoginPage; $misuzuBypassLockdown = !empty($misuzuBypassLockdown) || $onLoginPage;
if(!$misuzuBypassLockdown) { if(!$misuzuBypassLockdown) {
if(UserSession::hasCurrent()) { if(UserSession::hasCurrent()) {
$privatePermCat = $cfg->getValue('private.perm.cat', IConfig::T_STR); ['private.perm.cat' => $privatePermCat, 'private.perm.val' => $privatePermVal] = $cfg->getValues(['private.perm.cat:s', 'private.perm.val:i']);
$privatePermVal = $cfg->getValue('private.perm.val', IConfig::T_INT);
if(!empty($privatePermCat) && $privatePermVal > 0) { if(!empty($privatePermCat) && $privatePermVal > 0) {
if(!perms_check_user($privatePermCat, User::getCurrent()->getId(), $privatePermVal)) { if(!perms_check_user($privatePermCat, User::getCurrent()->getId(), $privatePermVal)) {
@ -244,7 +232,7 @@ function mszLockdown(): void {
User::unsetCurrent(); User::unsetCurrent();
} }
} }
} elseif(!$onLoginPage && !($onPasswordPage && $cfg->getValue('private.allow_password_reset', IConfig::T_BOOL, true))) { } elseif(!$onLoginPage && !($onPasswordPage && $cfg->getBoolean('private.allow_password_reset', true))) {
url_redirect('auth-login'); url_redirect('auth-login');
exit; exit;
} }

View file

@ -2,7 +2,6 @@
namespace Misuzu; namespace Misuzu;
use Misuzu\AuthToken; use Misuzu\AuthToken;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException; use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserAuthSession; use Misuzu\Users\UserAuthSession;
@ -43,11 +42,28 @@ if(!empty($_GET['resolve'])) {
$notices = []; $notices = [];
$ipAddress = $_SERVER['REMOTE_ADDR']; $ipAddress = $_SERVER['REMOTE_ADDR'];
$countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
$siteIsPrivate = $cfg->getValue('private.enable', IConfig::T_BOOL);
$loginPermCat = $siteIsPrivate ? $cfg->getValue('private.perm.cat', IConfig::T_STR) : '';
$loginPermVal = $siteIsPrivate ? $cfg->getValue('private.perm.val', IConfig::T_INT) : 0;
$remainingAttempts = UserLoginAttempt::remaining($ipAddress); $remainingAttempts = UserLoginAttempt::remaining($ipAddress);
$siteIsPrivate = $cfg->getBoolean('private.enable');
if($siteIsPrivate) {
[
'private.perm.cat' => $loginPermCat,
'private.perm.val' => $loginPermVal,
'private.msg' => $sitePrivateMessage,
'private.allow_password_reset' => $canResetPassword,
] = $cfg->getValues([
'private.perm.cat:s',
'private.perm.val:i',
'private.msg:s',
['private.allow_password_reset:b', true],
]);
} else {
$loginPermCat = '';
$loginPermVal = 0;
$sitePrivateMessage = '';
$canResetPassword = true;
}
while(!empty($_POST['login']) && is_array($_POST['login'])) { while(!empty($_POST['login']) && is_array($_POST['login'])) {
if(!CSRF::validateRequest()) { if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!'; $notices[] = 'Was unable to verify the request, please try again!';
@ -134,8 +150,6 @@ $loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login'
!empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : '' !empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : ''
); );
$loginRedirect = $welcomeMode ? url('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? url('index'); $loginRedirect = $welcomeMode ? url('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? url('index');
$sitePrivateMessage = $siteIsPrivate ? $cfg->getValue('private.msg', IConfig::T_STR) : '';
$canResetPassword = $siteIsPrivate ? $cfg->getValue('private.allow_password_reset', IConfig::T_BOOL, true) : true;
$canRegisterAccount = !$siteIsPrivate; $canRegisterAccount = !$siteIsPrivate;
Template::render('auth.login', [ Template::render('auth.login', [

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException; use Misuzu\Users\UserNotFoundException;
use Misuzu\Users\UserLoginAttempt; use Misuzu\Users\UserLoginAttempt;
@ -33,8 +32,8 @@ if($userId > 0)
$notices = []; $notices = [];
$ipAddress = $_SERVER['REMOTE_ADDR']; $ipAddress = $_SERVER['REMOTE_ADDR'];
$siteIsPrivate = $cfg->getValue('private.enable', IConfig::T_BOOL); $siteIsPrivate = $cfg->getBoolean('private.enable');
$canResetPassword = $siteIsPrivate ? $cfg->getValue('private.allow_password_reset', IConfig::T_BOOL, true) : true; $canResetPassword = $siteIsPrivate ? $cfg->getBoolean('private.allow_password_reset', true) : true;
$remainingAttempts = UserLoginAttempt::remaining($ipAddress); $remainingAttempts = UserLoginAttempt::remaining($ipAddress);
while($canResetPassword) { while($canResetPassword) {

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
require_once '../../misuzu.php'; require_once '../../misuzu.php';
@ -21,8 +20,16 @@ $leaderboardIdLength = strlen($leaderboardId);
$leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null; $leaderboardYear = $leaderboardIdLength === 4 || $leaderboardIdLength === 6 ? substr($leaderboardId, 0, 4) : null;
$leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) : null; $leaderboardMonth = $leaderboardIdLength === 6 ? substr($leaderboardId, 4, 2) : null;
$unrankedForums = !empty($_GET['allow_unranked']) ? [] : $cfg->getValue('forum_leader.unranked.forum', IConfig::T_ARR); if(empty($_GET['allow_unranked'])) {
$unrankedTopics = !empty($_GET['allow_unranked']) ? [] : $cfg->getValue('forum_leader.unranked.topic', IConfig::T_ARR); [
'forum_leader.unranked.forum' => $unrankedForums,
'forum_leader.unranked.topic' => $unrankedTopics,
] = $cfg->getValues([
'forum_leader.unranked.forum:a',
'forum_leader.unranked.topic:a',
]);
} else $unrankedForums = $unrankedTopics = [];
$leaderboards = forum_leaderboard_categories(); $leaderboards = forum_leaderboard_categories();
$leaderboard = forum_leaderboard_listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics); $leaderboard = forum_leaderboard_listing($leaderboardYear, $leaderboardMonth, $unrankedForums, $unrankedTopics);
@ -31,10 +38,9 @@ $leaderboardName = 'All Time';
if($leaderboardYear) { if($leaderboardYear) {
$leaderboardName = "Leaderboard {$leaderboardYear}"; $leaderboardName = "Leaderboard {$leaderboardYear}";
if($leaderboardMonth) { if($leaderboardMonth)
$leaderboardName .= "-{$leaderboardMonth}"; $leaderboardName .= "-{$leaderboardMonth}";
} }
}
if($leaderboardMode === 'markdown') { if($leaderboardMode === 'markdown') {
$markdown = <<<MD $markdown = <<<MD

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config;
use Misuzu\Config\CfgTools; use Misuzu\Config\CfgTools;
use Misuzu\Users\User; use Misuzu\Users\User;
@ -13,25 +12,21 @@ if(!User::hasCurrent()
return; return;
} }
$sName = (string)filter_input(INPUT_GET, 'name'); $valueName = (string)filter_input(INPUT_GET, 'name');
if(!CfgTools::validateName($sName) || !$cfg->hasValue($sName)) $valueInfo = $cfg->getValueInfo($valueName);
throw new \Exception("Config value does not exist."); if($valueInfo === null) {
echo render_error(404);
return;
}
if($_SERVER['REQUEST_METHOD'] === 'POST') { if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!CSRF::validateRequest()) $valueName = $valueInfo->getName();
throw new \Exception("Request verification failed."); $msz->createAuditLog('CONFIG_DELETE', [$valueName]);
$cfg->removeValues($valueName);
$msz->createAuditLog('CONFIG_DELETE', [$sName]);
$cfg->removeValue($sName);
url_redirect('manage-general-settings'); url_redirect('manage-general-settings');
} else { return;
$sValue = $cfg->getValue($sName); }
Template::render('manage.general.setting-delete', [ Template::render('manage.general.setting-delete', [
'conf_var' => [ 'config_value' => $valueInfo,
'name' => $sName,
'type' => CfgTools::type($sValue),
'value' => $sValue,
],
]); ]);
}

View file

@ -1,9 +1,7 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config; use Misuzu\Config\DbConfig;
use Misuzu\Config\CfgTools;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
require_once '../../../misuzu.php'; require_once '../../../misuzu.php';
@ -14,105 +12,84 @@ if(!User::hasCurrent()
return; return;
} }
$sVar = [ $isNew = true;
'name' => '',
'type' => '',
'value' => null,
'new' => true,
];
$sName = (string)filter_input(INPUT_GET, 'name'); $sName = (string)filter_input(INPUT_GET, 'name');
$sType = (string)filter_input(INPUT_GET, 'type');
$sValue = null;
$loadValueInfo = fn() => $cfg->getValueInfo($sName);
if(!empty($sName)) { if(!empty($sName)) {
if(!CfgTools::validateName($sName)) $sInfo = $loadValueInfo();
throw new \Exception("Config key name has invalid format."); if($sInfo !== null) {
$isNew = false;
$sVar['name'] = $sName; $sName = $sInfo->getName();
$sType = $sInfo->getType();
$sValue = $sInfo->getValue();
}
} }
$sType = (string)filter_input(INPUT_GET, 'type'); while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!empty($sType)) { if($isNew) {
if(!CfgTools::isValidType($sType)) $sName = trim((string)filter_input(INPUT_POST, 'conf_name'));
throw new \Exception("Specified type is invalid."); if(!DbConfig::validateName($sName)) {
echo 'Name contains invalid characters.';
$sVar['type'] = $sType; break;
$sVar['value'] = CfgTools::default($sType);
} }
if($_SERVER['REQUEST_METHOD'] === 'POST') { $sType = trim((string)filter_input(INPUT_POST, 'conf_type'));
if(!CSRF::validateRequest()) if(!in_array($sType, ['string', 'int', 'float', 'bool', 'array'])) {
throw new \Exception("Request verification failed."); echo 'Invalid type specified.';
break;
if(empty($sName)) {
$sName = (string)filter_input(INPUT_POST, 'conf_name');
if(empty($sName) || !CfgTools::validateName($sName))
throw new \Exception("Config key name has invalid format.");
$sVar['name'] = $sName;
} }
$sLogAction = 'CONFIG_CREATE';
if($cfg->hasValue($sName)) {
$sType = CfgTools::type($cfg->getValue($sName));
$sVar['new'] = false;
$sLogAction = 'CONFIG_UPDATE';
} elseif(empty($sType)) {
$sType = (string)filter_input(INPUT_POST, 'conf_type');
if(empty($sType) || !CfgTools::isValidType($sType))
throw new \Exception("Specified type is invalid.");
} }
$sVar['type'] = $sType;
$sValue = CfgTools::default($sType);
if($sType === 'array') { if($sType === 'array') {
if(!empty($_POST['conf_value']) && is_array($_POST['conf_value'])) { $applyFunc = $cfg->setArray(...);
foreach($_POST['conf_value'] as $fv) { $sValue = [];
$fv = (string)$fv; $sRaw = filter_input(INPUT_POST, 'conf_value', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
foreach($sRaw as $rValue) {
if(str_starts_with($fv, 's:')) { if(strpos($rValue, ':') === 1) {
$fv = substr($fv, 2); $rType = $rValue[0];
} elseif(str_starts_with($fv, 'i:')) { $rValue = substr($rValue, 2);
$fv = (int)substr($fv, 2); $sValue[] = match($rType) {
} elseif(str_starts_with($fv, 'b:')) { 's' => $rValue,
$fv = strtolower(substr($fv, 2)); 'i' => (int)$rValue,
$fv = $fv !== 'false' && $fv !== '0' && $fv !== ''; 'd' => (float)$rValue,
'f' => (float)$rValue,
'b' => $rValue !== 'false' && $rValue !== '0' && $rValue !== '',
default => '',
};
} else
$sValue[] = $rValue;
} }
} elseif($sType === 'bool') {
$sValue[] = $fv;
}
}
} elseif($sType === 'boolean') {
$sValue = !empty($_POST['conf_value']); $sValue = !empty($_POST['conf_value']);
$applyFunc = $cfg->setBoolean(...);
} else { } else {
$sValue = (string)filter_input(INPUT_POST, 'conf_value'); $sValue = filter_input(INPUT_POST, 'conf_value');
if($sType === 'integer') if($sType === 'int') {
$applyFunc = $cfg->setInteger(...);
$sValue = (int)$sValue; $sValue = (int)$sValue;
} elseif($sType === 'float') {
$applyFunc = $cfg->setFloat(...);
$sValue = (float)$sValue;
} else
$applyFunc = $cfg->setString(...);
} }
$sVar['value'] = $sValue; $msz->createAuditLog($isNew ? 'CONFIG_CREATE' : 'CONFIG_UPDATE', [$sName]);
$applyFunc($sName, $sValue);
$msz->createAuditLog($sLogAction, [$sName]);
$cfg->setValue($sName, $sValue);
url_redirect('manage-general-settings'); url_redirect('manage-general-settings');
return; return;
} }
if($cfg->hasValue($sName)) { if($sType === 'array' && !empty($sValue))
$sVar['new'] = false; foreach($sValue as $key => $value)
$sValue = $cfg->getValue($sName); $sValue[$key] = gettype($value)[0] . ':' . $value;
$sVar['type'] = $sType = CfgTools::type($sValue);
if($sType === IConfig::T_ARR)
foreach($sValue as $fk => $fv)
$sValue[$fk] = ['integer' => 'i', 'string' => 's', 'boolean' => 'b'][gettype($fv)] . ':' . $fv;
$sVar['value'] = $sValue;
}
Template::render('manage.general.setting', [ Template::render('manage.general.setting', [
'conf_var' => $sVar, 'config_new' => $isNew,
'config_name' => $sName,
'config_type' => $sType,
'config_value' => $sValue,
]); ]);

View file

@ -1,9 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config;
use Misuzu\Config\CfgTools;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
require_once '../../../misuzu.php'; require_once '../../../misuzu.php';
@ -14,18 +11,10 @@ if(!User::hasCurrent()
return; return;
} }
$hidden = $cfg->getValue('settings.hidden', IConfig::T_ARR, []); $hidden = $cfg->getArray('settings.hidden');
$vars = $cfg->getAllValueInfos();
$vars = [];
foreach($cfg->getNames() as $key) {
$var = $cfg->getValue($key);
$vars[] = [
'key' => $key,
'type' => CfgTools::type($var),
'value' => in_array($key, $hidden) ? '*** hidden ***' : json_encode($var),
];
}
Template::render('manage.general.settings', [ Template::render('manage.general.settings', [
'conf_vars' => $vars, 'config_vars' => $vars,
'config_hidden' => $hidden,
]); ]);

View file

@ -1,8 +1,6 @@
<?php <?php
namespace Misuzu; namespace Misuzu;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
use Misuzu\Users\UserRole; use Misuzu\Users\UserRole;
use Misuzu\Users\UserRoleNotFoundException; use Misuzu\Users\UserRoleNotFoundException;
@ -49,7 +47,7 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTP() !== (bool)$_POST['tfa']['enable']) { if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTP() !== (bool)$_POST['tfa']['enable']) {
if((bool)$_POST['tfa']['enable']) { if((bool)$_POST['tfa']['enable']) {
$tfaKey = TOTP::generateKey(); $tfaKey = TOTP::generateKey();
$tfaIssuer = $cfg->getValue('site.name', IConfig::T_STR, 'Misuzu'); $tfaIssuer = $cfg->getString('site.name', 'Misuzu');
$tfaQrcode = (new QRCode(new QROptions([ $tfaQrcode = (new QRCode(new QROptions([
'version' => 5, 'version' => 5,
'outputType' => QRCode::OUTPUT_IMAGE_JPG, 'outputType' => QRCode::OUTPUT_IMAGE_JPG,

View file

@ -1,32 +0,0 @@
<?php
namespace Misuzu;
use Misuzu\Config\IConfig;
final class Config {
private static IConfig $config;
public static function init(IConfig $config): void {
self::$config = $config;
}
public static function keys(): array {
return self::$config->getNames();
}
public static function get(string $key, string $type = IConfig::T_ANY, $default = null): mixed {
return self::$config->getValue($key, $type, $default);
}
public static function has(string $key): bool {
return self::$config->hasValue($key);
}
public static function set(string $key, $value, bool $soft = false): void {
self::$config->setValue($key, $value, !$soft);
}
public static function remove(string $key, bool $soft = false): void {
self::$config->removeValue($key, !$soft);
}
}

View file

@ -1,32 +0,0 @@
<?php
namespace Misuzu\Config;
final class CfgTools {
public static function type($value): string {
return gettype($value);
}
public static function isValidType(string $type): bool {
return $type === IConfig::T_ARR
|| $type === IConfig::T_BOOL
|| $type === IConfig::T_INT
|| $type === IConfig::T_STR;
}
public static function validateName(string $name): bool {
// this should better validate the format, this allows for a lot of shittery
return preg_match('#^([a-z][a-zA-Z0-9._]+)$#', $name) === 1;
}
private const DEFAULTS = [
IConfig::T_ANY => null,
IConfig::T_STR => '',
IConfig::T_INT => 0,
IConfig::T_BOOL => false,
IConfig::T_ARR => [],
];
public static function default(string $type) {
return self::DEFAULTS[$type] ?? null;
}
}

View file

@ -1,78 +1,327 @@
<?php <?php
namespace Misuzu\Config; namespace Misuzu\Config;
use RuntimeException;
use InvalidArgumentException;
use Index\Data\DataException; use Index\Data\DataException;
use Index\Data\IDbConnection; use Index\Data\IDbConnection;
use Index\Data\IDbStatement; use Index\Data\IDbStatement;
use Index\Data\IDbResult;
use Misuzu\DbStatementCache;
use Misuzu\Pagination;
class DbConfig implements IConfig { class DbConfig implements IConfig {
private IDbConnection $dbConn; private IDbConnection $dbConn;
private DbStatementCache $cache;
private array $values = []; private array $values = [];
private ?IDbStatement $stmtSet = null;
private ?IDbStatement $stmtDelete = null;
public function __construct(IDbConnection $dbConn) { public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn; $this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
} }
public function reload(): void { public static function validateName(string $name): bool {
$this->values = []; // this should better validate the format, this allows for a lot of shittery
return preg_match('#^([a-z][a-zA-Z0-9._]+)$#', $name) === 1;
}
try { private static function whereInList(int $count): string {
$result = $this->dbConn->query('SELECT config_name, config_value FROM msz_config;'); return implode(', ', array_fill(0, $count, '?'));
while($result->next()) }
$this->values[$result->getString(0)] = unserialize($result->getString(1));
} catch(DataException $ex) { } public function reset(): void {
$this->values = [];
}
public function unload(string|array $names): void {
if(empty($names))
return;
if(is_string($names))
$names = [$names];
foreach($names as $name)
unset($this->values[$name]);
} }
public function scopeTo(string $prefix): IConfig { public function scopeTo(string $prefix): IConfig {
return new ScopedConfig($this, $prefix); return new ScopedConfig($this, $prefix);
} }
public function getNames(): array { public function hasValues(string|array $names): bool {
return array_keys($this->values); if(empty($names))
return true;
if(is_string($names))
$names = [$names];
$cachedNames = array_keys($this->values);
$names = array_diff($names, $cachedNames);
if(!empty($names)) {
// array_diff preserves keys, the for() later would fuck up without it
$names = array_values($names);
$nameCount = count($names);
$stmt = $this->cache->get(sprintf(
'SELECT COUNT(*) FROM msz_config WHERE config_name IN (%s)',
self::whereInList($nameCount)
));
for($i = 0; $i < $nameCount; ++$i)
$stmt->addParameter($i + 1, $names[$i]);
$stmt->execute();
$result = $stmt->getResult();
if($result->next())
return $result->getInteger(0) >= $nameCount;
} }
public function getValue(string $name, string $type = IConfig::T_ANY, $default = null): mixed { return true;
$value = $this->values[$name] ?? null;
if($type !== IConfig::T_ANY && CfgTools::type($value) !== $type)
$value = null;
return $value ?? $default ?? CfgTools::default($type);
} }
public function hasValue(string $name): bool { public function removeValues(string|array $names): void {
return array_key_exists($name, $this->values); if(empty($names))
} return;
if(is_string($names))
$names = [$names];
public function setValue(string $name, $value, bool $save = true): void { foreach($names as $name)
$this->values[$name] = $value;
if($save) {
$value = serialize($value);
if($this->stmtSet === null)
$this->stmtSet = $this->dbConn->prepare('REPLACE INTO msz_config (config_name, config_value) VALUES (?, ?)');
$this->stmtSet->reset();
$this->stmtSet->addParameter(1, $name);
$this->stmtSet->addParameter(2, $value);
$this->stmtSet->execute();
}
}
public function removeValue(string $name, bool $save = true): void {
unset($this->values[$name]); unset($this->values[$name]);
if($save) { $nameCount = count($names);
if($this->stmtDelete === null) $stmt = $this->cache->get(sprintf(
$this->stmtDelete = $this->dbConn->prepare('DELETE FROM msz_config WHERE config_name = ?'); 'DELETE FROM msz_config WHERE config_name IN (%s)',
self::whereInList($nameCount)
));
$this->stmtDelete->reset(); for($i = 0; $i < $nameCount; ++$i)
$this->stmtDelete->addParameter(1, $name); $stmt->addParameter($i + 1, $names[$i]);
$this->stmtDelete->execute();
$stmt->execute();
}
public function getAllValueInfos(?Pagination $pagination = null): array {
$this->reset();
$infos = [];
$hasPagination = $pagination !== null;
$query = 'SELECT config_name, config_value FROM msz_config';
if($hasPagination)
$query .= ' LIMIT ? RANGE ?';
$stmt = $this->cache->get($query);
if($hasPagination) {
$stmt->addParameter(1, $pagination->getRange());
$stmt->addParameter(2, $pagination->getOffset());
}
$stmt->execute();
$result = $stmt->getResult();
while($result->next()) {
$name = $result->getString(0);
$infos[] = $this->values[$name] = new DbConfigValueInfo($result);
}
return $infos;
}
public function getValueInfos(string|array $names): array {
if(empty($names))
return [];
if(is_string($names))
$names = [$names];
$infos = [];
$skip = [];
foreach($names as $name)
if(array_key_exists($name, $this->values)) {
$infos[] = $this->values[$name];
$skip[] = $name;
}
$names = array_diff($names, $skip);
if(!empty($names)) {
// array_diff preserves keys, the for() later would fuck up without it
$names = array_values($names);
$nameCount = count($names);
$stmt = $this->cache->get(sprintf(
'SELECT config_name, config_value FROM msz_config WHERE config_name IN (%s)',
self::whereInList($nameCount)
));
for($i = 0; $i < $nameCount; ++$i)
$stmt->addParameter($i + 1, $names[$i]);
$stmt->execute();
$result = $stmt->getResult();
while($result->next()) {
$name = $result->getString(0);
$infos[] = $this->values[$name] = new DbConfigValueInfo($result);
} }
} }
return $infos;
}
public function getValueInfo(string $name): ?IConfigValueInfo {
$infos = $this->getValueInfos($name);
return empty($infos) ? null : $infos[0];
}
public function getValues(array $specs): array {
$names = [];
foreach($specs as $key => $spec) {
if(is_string($spec)) {
$name = $spec;
$default = null;
} elseif(is_array($spec) && !empty($spec)) {
$name = $spec[0];
$default = $spec[1] ?? null;
} else
throw new InvalidArgumentException('$specs array contains an invalid entry.');
if(($colon = strpos($name, ':')) !== false) {
$type = substr($name, $colon + 1, 1);
$name = substr($name, 0, $colon);
} else $type = '';
$names[] = $name;
$specs[$key] = [
'name' => $name,
'type' => $type,
'default' => $default,
];
}
$infos = $this->getValueInfos($names);
$results = [];
foreach($specs as $spec) {
foreach($infos as $infoTest)
if($infoTest->getName() === $spec['name']) {
$info = $infoTest;
break;
}
if(!isset($info)) {
$defaultValue = $spec['default'] ?? null;
if($spec['type'] !== '')
settype($defaultValue, match($spec['type']) {
's' => 'string',
'a' => 'array',
'i' => 'int',
'b' => 'bool',
'f' => 'float',
'd' => 'double',
default => throw new RuntimeException('Invalid type letter encountered.'),
});
$results[$spec['name']] = $defaultValue;
continue;
}
$results[$spec['name']] = match($spec['type']) {
's' => $info->getString(),
'a' => $info->getArray(),
'i' => $info->getInteger(),
'b' => $info->getBoolean(),
'f' => $info->getFloat(),
'd' => $info->getFloat(),
'' => $info->getValue(),
default => throw new RuntimeException('Unknown type encountered in $specs.'),
};
unset($info);
}
return $results;
}
public function getString(string $name, string $default = ''): string {
$valueInfo = $this->getValueInfo($name);
return $valueInfo?->isString() ? $valueInfo->getString() : $default;
}
public function getInteger(string $name, int $default = 0): int {
$valueInfo = $this->getValueInfo($name);
return $valueInfo?->isInteger() ? $valueInfo->getInteger() : $default;
}
public function getFloat(string $name, float $default = 0): float {
$valueInfo = $this->getValueInfo($name);
return $valueInfo?->isFloat() ? $valueInfo->getFloat() : $default;
}
public function getBoolean(string $name, bool $default = false): bool {
$valueInfo = $this->getValueInfo($name);
return $valueInfo?->isBoolean() ? $valueInfo->getBoolean() : $default;
}
public function getArray(string $name, array $default = []): array {
$valueInfo = $this->getValueInfo($name);
return $valueInfo?->isArray() ? $valueInfo->getArray() : $default;
}
private function setValue(string $name, $value): void {
$this->values[$name] = $value;
$value = serialize($value);
$stmt = $this->cache->get('REPLACE INTO msz_config (config_name, config_value) VALUES (?, ?)');
$stmt->addParameter(1, $name);
$stmt->addParameter(2, $value);
$stmt->execute();
}
public function setValues(array $values): void {
if(empty($values))
return;
$valueCount = count($values);
$stmt = $this->cache->get(sprintf(
'REPLACE INTO msz_config (config_name, config_value) VALUES %s',
implode(', ', array_fill(0, $valueCount, '(?, ?)'))
));
$args = 0;
foreach($values as $name => $value) {
if(!self::validateName($name))
throw new InvalidArgumentException('Invalid name encountered in $values.');
if(is_array($value)) {
foreach($value as $entry)
if(!is_scalar($entry))
throw new InvalidArgumentException('An array value in $values contains a non-scalar type.');
} elseif(!is_scalar($value))
throw new InvalidArgumentException('Invalid value type encountered in $values.');
$stmt->addParameter(++$args, $name);
$stmt->addParameter(++$args, serialize($value));
}
$stmt->execute();
}
public function setString(string $name, string $value): void {
$this->setValues([$name => $value]);
}
public function setInteger(string $name, int $value): void {
$this->setValues([$name => $value]);
}
public function setFloat(string $name, float $value): void {
$this->setValues([$name => $value]);
}
public function setBoolean(string $name, bool $value): void {
$this->setValues([$name => $value]);
}
public function setArray(string $name, array $value): void {
$this->setValues([$name => $value]);
}
} }

View file

@ -0,0 +1,92 @@
<?php
namespace Misuzu\Config;
use RuntimeException;
use Index\Data\IDbResult;
class DbConfigValueInfo implements IConfigValueInfo {
private string $name;
private string $value;
public function __construct(IDbResult $result) {
$this->name = $result->getString(0);
$this->value = $result->getString(1);
}
public function getName(): string {
return $this->name;
}
public function getType(): string {
return match($this->value[0]) {
's' => 'string',
'a' => 'array',
'i' => 'int',
'b' => 'bool',
'd' => 'float',
};
}
public function isString(): bool {
return $this->value[0] === 's';
}
public function isInteger(): bool {
return $this->value[0] === 'i';
}
public function isFloat(): bool {
return $this->value[0] === 'd';
}
public function isBoolean(): bool {
return $this->value[0] === 'b';
}
public function isArray(): bool {
return $this->value[0] === 'a';
}
public function getValue(): mixed {
return unserialize($this->value);
}
public function getString(): string {
$value = $this->getValue();
if(!is_string($value))
throw new RuntimeException('Value is not a string.');
return $value;
}
public function getInteger(): int {
$value = $this->getValue();
if(!is_int($value))
throw new RuntimeException('Value is not an integer.');
return $value;
}
public function getFloat(): float {
$value = $this->getValue();
if(!is_float($value))
throw new RuntimeException('Value is not a floating point number.');
return $value;
}
public function getBoolean(): bool {
$value = $this->getValue();
if(!is_bool($value))
throw new RuntimeException('Value is not a boolean.');
return $value;
}
public function getArray(): array {
$value = $this->getValue();
if(!is_array($value))
throw new RuntimeException('Value is not an array.');
return $value;
}
public function __toString(): string {
return $this->isArray() ? implode(', ', $this->getArray()) : (string)$this->getValue();
}
}

View file

@ -1,23 +1,29 @@
<?php <?php
namespace Misuzu\Config; namespace Misuzu\Config;
// getValue (and hasValue?) should probably be replaced with something that allows grouped loading use Misuzu\Pagination;
// that way the entire config doesn't have to be kept in memory on every request
// this probably has to be delayed until the backwards compat static Config object isn't needed anymore
// otherwise there'd be increased overhead
// bulk operations for setValue and removeValue would also be cool
interface IConfig { interface IConfig {
public const T_ANY = '';
public const T_STR = 'string';
public const T_INT = 'integer';
public const T_BOOL = 'boolean';
public const T_ARR = 'array';
public function scopeTo(string $prefix): IConfig; public function scopeTo(string $prefix): IConfig;
public function getNames(): array;
public function getValue(string $name, string $type = IConfig::T_ANY, $default = null): mixed; public function hasValues(string|array $names): bool;
public function hasValue(string $name): bool; public function removeValues(string|array $names): void;
public function setValue(string $name, $value, bool $save = true): void;
public function removeValue(string $name, bool $save = true): void; public function getAllValueInfos(?Pagination $pagination = null): array;
public function getValueInfos(string|array $names): array;
public function getValueInfo(string $name): ?IConfigValueInfo;
public function getValues(array $specs): array;
public function getString(string $name, string $default = ''): string;
public function getInteger(string $name, int $default = 0): int;
public function getFloat(string $name, float $default = 0): float;
public function getBoolean(string $name, bool $default = false): bool;
public function getArray(string $name, array $default = []): array;
public function setValues(array $values): void;
public function setString(string $name, string $value): void;
public function setInteger(string $name, int $value): void;
public function setFloat(string $name, float $value): void;
public function setBoolean(string $name, bool $value): void;
public function setArray(string $name, array $value): void;
} }

View file

@ -0,0 +1,22 @@
<?php
namespace Misuzu\Config;
use Stringable;
interface IConfigValueInfo extends Stringable {
public function getName(): string;
public function getType(): string;
public function isString(): bool;
public function isInteger(): bool;
public function isFloat(): bool;
public function isBoolean(): bool;
public function isArray(): bool;
public function getValue(): mixed;
public function getString(): string;
public function getInteger(): int;
public function getFloat(): float;
public function getBoolean(): bool;
public function getArray(): array;
}

View file

@ -2,12 +2,13 @@
namespace Misuzu\Config; namespace Misuzu\Config;
use InvalidArgumentException; use InvalidArgumentException;
use Misuzu\Pagination;
class ScopedConfig implements IConfig { class ScopedConfig implements IConfig {
private IConfig $config; private IConfig $config;
private string $prefix; private string $prefix;
private int $prefixLength;
// update this to : at some point, would need adjustment of the live site's config
private const SCOPE_CHAR = '.'; private const SCOPE_CHAR = '.';
public function __construct(IConfig $config, string $prefix) { public function __construct(IConfig $config, string $prefix) {
@ -18,41 +19,120 @@ class ScopedConfig implements IConfig {
$this->config = $config; $this->config = $config;
$this->prefix = $prefix; $this->prefix = $prefix;
$this->prefixLength = strlen($prefix);
} }
private function getName(string $name): string { private function prefixNames(string|array $names): array {
if(is_string($names))
return [$this->prefix . $name];
foreach($names as $key => $name)
$names[$key] = $this->prefix . $name;
return $names;
}
private function prefixName(string $name): string {
return $this->prefix . $name; return $this->prefix . $name;
} }
public function scopeTo(string $prefix): IConfig { public function scopeTo(string $prefix): IConfig {
return $this->config->scopeTo($this->getName($prefix)); return $this->config->scopeTo($this->prefixName($prefix));
} }
public function getNames(): array { public function hasValues(string|array $names): bool {
$orig = $this->config->getNames(); $this->config->hasValues($this->prefixNames($names));
$pfxLength = strlen($this->prefix);
$filter = [];
foreach($orig as $name)
if(str_starts_with($name, $this->prefix))
$filter[] = substr($name, $pfxLength);
return $filter;
} }
public function getValue(string $name, string $type = IConfig::T_ANY, $default = null): mixed { public function removeValues(string|array $names): void {
return $this->config->getValue($this->getName($name), $type, $default); $this->config->removeValues($this->prefixNames($names));
} }
public function hasValue(string $name): bool { public function getAllValueInfos(?Pagination $pagination = null): array {
return $this->config->hasValue($this->getName($name)); $infos = $this->config->getAllValueInfos($pagination);
foreach($infos as $key => $info)
$infos[$key] = new ScopedConfigValueInfo($info, $this->prefixLength);
return $infos;
} }
public function setValue(string $name, $value, bool $save = true): void { public function getValueInfos(string|array $names): array {
$this->config->setValue($this->getName($name), $value, $save); $infos = $this->config->getValueInfos($this->prefixNames($names));
foreach($infos as $key => $info)
$infos[$key] = new ScopedConfigValueInfo($info, $this->prefixLength);
return $infos;
} }
public function removeValue(string $name, bool $save = true): void { public function getValueInfo(string $name): ?IConfigValueInfo {
$this->config->removeValue($this->getName($name), $save); $info = $this->config->getValueInfo($this->prefixName($name));
if($info !== null)
$info = new ScopedConfigValueInfo($info, $this->prefixLength);
return $info;
}
public function getValues(array $specs): array {
foreach($specs as $key => $spec) {
if(is_string($spec))
$specs[$key] = $this->prefixName($spec);
elseif(is_array($spec) && !empty($spec))
$specs[$key][0] = $this->prefixName($spec[0]);
else
throw new InvalidArgumentException('$specs array contains an invalid entry.');
}
$results = [];
foreach($this->config->getValues($specs) as $name => $result)
$results[substr($name, $this->prefixLength)] = $result;
return $results;
}
public function getString(string $name, string $default = ''): string {
return $this->config->getString($this->prefixName($name), $default);
}
public function getInteger(string $name, int $default = 0): int {
return $this->config->getInteger($this->prefixName($name), $default);
}
public function getFloat(string $name, float $default = 0): float {
return $this->config->getFloat($this->prefixName($name), $default);
}
public function getBoolean(string $name, bool $default = false): bool {
return $this->config->getBoolean($this->prefixName($name), $default);
}
public function getArray(string $name, array $default = []): array {
return $this->config->getArray($this->prefixName($name), $default);
}
public function setValues(array $values): void {
if(empty($values))
return;
$prefixed = [];
foreach($values as $name => $value)
$prefixed[$this->prefixName($name)] = $value;
$this->config->setValues($values);
}
public function setString(string $name, string $value): void {
$this->config->setString($this->prefixName($name), $value);
}
public function setInteger(string $name, int $value): void {
$this->config->setInteger($this->prefixName($name), $value);
}
public function setFloat(string $name, float $value): void {
$this->config->setFloat($this->prefixName($name), $value);
}
public function setBoolean(string $name, bool $value): void {
$this->config->setBoolean($this->prefixName($name), $value);
}
public function setArray(string $name, array $value): void {
$this->config->setArray($this->prefixName($name), $value);
} }
} }

View file

@ -0,0 +1,72 @@
<?php
namespace Misuzu\Config;
class ScopedConfigValueInfo implements IConfigValueInfo {
private IConfigValueInfo $info;
private int $prefixLength;
public function __construct(IConfigValueInfo $info, int $prefixLength) {
$this->info = $info;
$this->prefixLength = $prefixLength;
}
public function getName(): string {
return substr($this->info->getName(), $this->prefixLength);
}
public function getRealName(): string {
return $this->info->getName();
}
public function getType(): string {
return $this->info->getType();
}
public function isString(): bool {
return $this->info->isString();
}
public function isInteger(): bool {
return $this->info->isInteger();
}
public function isFloat(): bool {
return $this->info->isFloat();
}
public function isBoolean(): bool {
return $this->info->isBoolean();
}
public function isArray(): bool {
return $this->info->isArray();
}
public function getValue(): mixed {
return $this->info->getValue();
}
public function getString(): string {
return $this->info->getString();
}
public function getInteger(): int {
return $this->info->getInteger();
}
public function getFloat(): float {
return $this->info->getFloat();
}
public function getBoolean(): bool {
return $this->info->getBoolean();
}
public function getArray(): array {
return $this->info->getArray();
}
public function __toString(): string {
return (string)$this->info;
}
}

View file

@ -535,7 +535,7 @@ function forum_get_user_most_active_category_info(int $userId): ?object {
$getActiveForum = \Misuzu\DB::prepare(sprintf( $getActiveForum = \Misuzu\DB::prepare(sprintf(
'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY forum_id ORDER BY post_count DESC LIMIT 1', 'SELECT forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY forum_id ORDER BY post_count DESC LIMIT 1',
implode(',', $cfg->getValue('forum_leader.unranked.forum', \Misuzu\Config\IConfig::T_ARR)) implode(',', $cfg->getArray('forum_leader.unranked.forum'))
)); ));
$getActiveForum->bind('user', $userId); $getActiveForum->bind('user', $userId);

View file

@ -711,7 +711,7 @@ function forum_get_user_most_active_topic_info(int $userId): ?object {
$getActiveForum = \Misuzu\DB::prepare(sprintf( $getActiveForum = \Misuzu\DB::prepare(sprintf(
'SELECT topic_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY topic_id ORDER BY post_count DESC LIMIT 1', 'SELECT topic_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY topic_id ORDER BY post_count DESC LIMIT 1',
implode(',', $cfg->getValue('forum_leader.unranked.forum', \Misuzu\Config\IConfig::T_ARR)) implode(',', $cfg->getArray('forum_leader.unranked.forum'))
)); ));
$getActiveForum->bind('user', $userId); $getActiveForum->bind('user', $userId);

View file

@ -3,8 +3,6 @@ namespace Misuzu\Http\Handlers;
use ErrorException; use ErrorException;
use RuntimeException; use RuntimeException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Comments\CommentsEx; use Misuzu\Comments\CommentsEx;
@ -119,11 +117,12 @@ class ChangelogHandler extends Handler {
} }
private function createFeed(string $feedMode): Feed { private function createFeed(string $feedMode): Feed {
$siteName = $this->context->getConfig()->getString('site.name', 'Misuzu');
$changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10)); $changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$feed = (new Feed) $feed = (new Feed)
->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » Changelog') ->setTitle($siteName . ' » Changelog')
->setDescription('Live feed of changes to ' . Config::get('site.name', IConfig::T_STR, 'Misuzu') . '.') ->setDescription('Live feed of changes to ' . $siteName . '.')
->setContentUrl(url_prefix(false) . url('changelog-index')) ->setContentUrl(url_prefix(false) . url('changelog-index'))
->setFeedUrl(url_prefix(false) . url("changelog-feed-{$feedMode}")); ->setFeedUrl(url_prefix(false) . url("changelog-feed-{$feedMode}"));

View file

@ -2,8 +2,6 @@
namespace Misuzu\Http\Handlers; namespace Misuzu\Http\Handlers;
use RuntimeException; use RuntimeException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Template; use Misuzu\Template;
@ -21,13 +19,22 @@ final class HomeHandler extends Handler {
} }
public function landing($response, $request): void { public function landing($response, $request): void {
$linkedData = Config::get('social.embed_linked', IConfig::T_BOOL) $config = $this->context->getConfig();
? [
'name' => Config::get('site.name', IConfig::T_STR, 'Misuzu'), if($config->getBoolean('social.embed_linked')) {
'url' => Config::get('site.url', IConfig::T_STR), $ldr = $config->getValues([
'logo' => Config::get('site.ext_logo', IConfig::T_STR), ['site.name:s', 'Misuzu'],
'same_as' => Config::get('social.linked', IConfig::T_ARR), 'site.url:s',
] : null; 'site.ext_logo:s',
'social.linked:a'
]);
$linkedData = [
'name' => $ldr['site.name'],
'url' => $ldr['site.url'],
'logo' => $ldr['site.ext_logo'],
'same_as' => $ldr['social.linked'],
];
} else $linkedData = null;
$featuredNews = $this->context->getNews()->getAllPosts( $featuredNews = $this->context->getNews()->getAllPosts(
onlyFeatured: true, onlyFeatured: true,
@ -55,7 +62,7 @@ final class HomeHandler extends Handler {
)->fetchAll(); )->fetchAll();
// TODO: don't hardcode forum ids // TODO: don't hardcode forum ids
$featuredForums = Config::get('landing.forum_categories', IConfig::T_ARR); $featuredForums = $config->getArray('landing.forum_categories');
$popularTopics = []; $popularTopics = [];
$activeTopics = []; $activeTopics = [];

View file

@ -2,13 +2,11 @@
namespace Misuzu\Http\Handlers; namespace Misuzu\Http\Handlers;
use RuntimeException; use RuntimeException;
use Misuzu\Config;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\Template; use Misuzu\Template;
use Misuzu\Comments\CommentsCategory; use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsEx; use Misuzu\Comments\CommentsEx;
use Misuzu\Config\IConfig;
use Misuzu\Feeds\Feed; use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem; use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer; use Misuzu\Feeds\AtomFeedSerializer;
@ -154,9 +152,10 @@ final class NewsHandler extends Handler {
private function createFeed(string $feedMode, ?NewsCategoryInfo $categoryInfo, array $posts): Feed { private function createFeed(string $feedMode, ?NewsCategoryInfo $categoryInfo, array $posts): Feed {
$hasCategory = $categoryInfo !== null; $hasCategory = $categoryInfo !== null;
$siteName = $this->context->getConfig()->getString('site.name', 'Misuzu');
$feed = (new Feed) $feed = (new Feed)
->setTitle(Config::get('site.name', IConfig::T_STR, 'Misuzu') . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News')) ->setTitle($siteName . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
->setDescription($hasCategory ? $categoryInfo->getDescription() : 'A live featured news feed.') ->setDescription($hasCategory ? $categoryInfo->getDescription() : 'A live featured news feed.')
->setContentUrl(url_prefix(false) . ($hasCategory ? url('news-category', ['category' => $categoryInfo->getId()]) : url('news-index'))) ->setContentUrl(url_prefix(false) . ($hasCategory ? url('news-category', ['category' => $categoryInfo->getId()]) : url('news-index')))
->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedMode}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedMode}"))); ->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedMode}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedMode}")));

View file

@ -2,6 +2,7 @@
namespace Misuzu; namespace Misuzu;
use InvalidArgumentException; use InvalidArgumentException;
use Misuzu\Config\IConfig;
use Symfony\Component\Mime\Email as SymfonyMessage; use Symfony\Component\Mime\Email as SymfonyMessage;
use Symfony\Component\Mime\Address as SymfonyAddress; use Symfony\Component\Mime\Address as SymfonyAddress;
use Symfony\Component\Mailer\Mailer as SymfonyMailer; use Symfony\Component\Mailer\Mailer as SymfonyMailer;
@ -11,25 +12,29 @@ use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
final class Mailer { final class Mailer {
private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt'; private const TEMPLATE_PATH = MSZ_ROOT . '/config/emails/%s.txt';
private static $dsn = 'null://null'; private static IConfig $config;
private static $transport = null; private static $transport = null;
private static string $senderName = 'Flashii'; private static string $senderName = 'Flashii';
private static string $senderAddr = 'sys@flashii.net'; private static string $senderAddr = 'sys@flashii.net';
public static function init(string $method, array $config): void { public static function init(IConfig $config): void {
if($method !== 'smtp') { self::$config = $config;
self::$dsn = 'null://null';
return;
} }
$dsn = $method; private static function createDsn(): string {
$config = self::$config->getValues([
'method:s',
'host:s',
['port:i', 25],
'username:s',
'password:s',
]);
// guess this is applied automatically based on the port? if($config['method'] !== 'smtp')
//if(!empty($config['encryption'])) return 'null://null';
// $dsn .= 't';
$dsn .= '://'; $dsn = $config['method'] . '://';
if(!empty($config['username'])) { if(!empty($config['username'])) {
$dsn .= $config['username']; $dsn .= $config['username'];
@ -44,18 +49,18 @@ final class Mailer {
$dsn .= ':'; $dsn .= ':';
$dsn .= $config['port'] ?? 25; $dsn .= $config['port'] ?? 25;
self::$dsn = $dsn; if(!empty($config['sender.name']))
self::$senderName = $config['sender.name'];
if(!empty($config['sender_name'])) if(!empty($config['sender.address']))
self::$senderName = $config['sender_name']; self::$senderAddr = $config['sender.address'];
if(!empty($config['sender_addr'])) return $dsn;
self::$senderAddr = $config['sender_addr'];
} }
public static function getTransport() { public static function getTransport() {
if(self::$transport === null) if(self::$transport === null)
self::$transport = SymfonyTransport::fromDsn(self::$dsn); self::$transport = SymfonyTransport::fromDsn(self::createDsn());
return self::$transport; return self::$transport;
} }
@ -65,11 +70,16 @@ final class Mailer {
break; break;
} }
$config = self::$config->getValues([
'sender.name:s',
'sender.address:s',
]);
$message = new SymfonyMessage; $message = new SymfonyMessage;
$message->from(new SymfonyAddress( $message->from(new SymfonyAddress(
self::$senderAddr, $config['sender.address'],
self::$senderName $config['sender.name']
)); ));
if($bcc) if($bcc)

View file

@ -24,10 +24,10 @@ final class SharpChatRoutes {
$this->config = $config; $this->config = $config;
$this->emotes = $emotes; $this->emotes = $emotes;
$hashKey = $this->config->getValue('hashKey', IConfig::T_STR, ''); $hashKey = $this->config->getString('hashKey', '');
if(empty($hashKey)) { if(empty($hashKey)) {
$hashKeyPath = $this->config->getValue('hashKeyPath', IConfig::T_STR, ''); $hashKeyPath = $this->config->getString('hashKeyPath', '');
if(is_file($hashKeyPath)) if(is_file($hashKeyPath))
$this->hashKey = file_get_contents($hashKeyPath); $this->hashKey = file_get_contents($hashKeyPath);
} else { } else {
@ -96,7 +96,7 @@ final class SharpChatRoutes {
public function getLogin($response, $request): void { public function getLogin($response, $request): void {
$currentUser = User::getCurrent(); $currentUser = User::getCurrent();
$configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal'; $configKey = $request->hasParam('legacy') ? 'chatPath.legacy' : 'chatPath.normal';
$chatPath = $this->config->getValue($configKey, IConfig::T_STR, '/'); $chatPath = $this->config->getString($configKey, '/');
$response->redirect( $response->redirect(
$currentUser === null $currentUser === null
@ -111,7 +111,7 @@ final class SharpChatRoutes {
$originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? ''); $originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
if(!empty($originHost) && $originHost !== $host) { if(!empty($originHost) && $originHost !== $host) {
$whitelist = $this->config->getValue('origins', IConfig::T_ARR, []); $whitelist = $this->config->getArray('origins', []);
if(!in_array($originHost, $whitelist)) if(!in_array($originHost, $whitelist))
return 403; return 403;

View file

@ -1,8 +1,6 @@
<?php <?php
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Imaging\Image; use Misuzu\Imaging\Image;
use Misuzu\Users\User; use Misuzu\Users\User;
@ -20,13 +18,15 @@ class UserAvatarAsset extends UserImageAsset implements UserAssetScalableInterfa
private const MAX_BYTES = 1000000; private const MAX_BYTES = 1000000;
public function getMaxWidth(): int { public function getMaxWidth(): int {
return Config::get('avatar.max_res', IConfig::T_INT, self::MAX_RES); global $cfg;
return $cfg->getInteger('avatar.max_res', self::MAX_RES);
} }
public function getMaxHeight(): int { public function getMaxHeight(): int {
return $this->getMaxWidth(); return $this->getMaxWidth();
} }
public function getMaxBytes(): int { public function getMaxBytes(): int {
return Config::get('avatar.max_size', IConfig::T_INT, self::MAX_BYTES); global $cfg;
return $cfg->getInteger('avatar.max_size', self::MAX_BYTES);
} }
public function getUrl(): string { public function getUrl(): string {

View file

@ -2,8 +2,6 @@
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
use InvalidArgumentException; use InvalidArgumentException;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
// attachment and attributes are to be stored in the same byte // attachment and attributes are to be stored in the same byte
@ -49,13 +47,16 @@ class UserBackgroundAsset extends UserImageAsset {
} }
public function getMaxWidth(): int { public function getMaxWidth(): int {
return Config::get('background.max_width', IConfig::T_INT, self::MAX_WIDTH); global $cfg;
return $cfg->getInteger('background.max_width', self::MAX_WIDTH);
} }
public function getMaxHeight(): int { public function getMaxHeight(): int {
return Config::get('background.max_height', IConfig::T_INT, self::MAX_HEIGHT); global $cfg;
return $cfg->getInteger('background.max_height', self::MAX_HEIGHT);
} }
public function getMaxBytes(): int { public function getMaxBytes(): int {
return Config::get('background.max_size', IConfig::T_INT, self::MAX_BYTES); global $cfg;
return $cfg->getInteger('background.max_size', self::MAX_BYTES);
} }
public function getUrl(): string { public function getUrl(): string {

View file

@ -1,8 +1,6 @@
<?php <?php
namespace Misuzu\Users\Assets; namespace Misuzu\Users\Assets;
use Misuzu\Config;
use Misuzu\Config\IConfig;
use Misuzu\Users\User; use Misuzu\Users\User;
class UserImageAssetFileCreationFailedException extends UserAssetException {} class UserImageAssetFileCreationFailedException extends UserAssetException {}
@ -83,7 +81,7 @@ abstract class UserImageAsset implements UserImageAssetInterface {
} }
public function getStoragePath(): string { public function getStoragePath(): string {
return Config::get('storage.path', IConfig::T_STR, MSZ_ROOT . DIRECTORY_SEPARATOR . 'store'); return MSZ_ROOT . DIRECTORY_SEPARATOR . 'store';
} }
public function getPath(): string { public function getPath(): string {

View file

@ -2,7 +2,7 @@
{% from 'macros.twig' import container_title %} {% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %} {% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %}
{% set title = ('Removing setting ' ~ conf_var.name) %} {% set title = ('Removing setting ' ~ config_value.name) %}
{% block manage_content %} {% block manage_content %}
<div class="container manage-settings"> <div class="container manage-settings">
@ -12,7 +12,7 @@
Are you sure you want to delete this setting? It cannot be recovered. Are you sure you want to delete this setting? It cannot be recovered.
</div> </div>
<form method="post" action="{{ url('manage-general-setting-delete', {'name': conf_var.name}) }}" class="manage-setting"> <form method="post" action="{{ url('manage-general-setting-delete', {'name': config_value.name}) }}" class="manage-setting">
{{ input_csrf() }} {{ input_csrf() }}
<div class="manage-settings-list-container"> <div class="manage-settings-list-container">
@ -20,13 +20,13 @@
<tbody> <tbody>
<tr class="manage-list-setting"> <tr class="manage-list-setting">
<td class="manage-list-setting-key"> <td class="manage-list-setting-key">
<div class="manage-list-setting-key-text">{{ conf_var.name }}</div> <div class="manage-list-setting-key-text">{{ config_value.name }}</div>
</td> </td>
<td class="manage-list-setting-type manage-list-setting-type--{{ conf_var.type }}"> <td class="manage-list-setting-type manage-list-setting-type--{{ config_value.type }}">
<div class="manage-list-setting-type-text">{{ conf_var.type }}</div> <div class="manage-list-setting-type-text">{{ config_value.type }}</div>
</td> </td>
<td class="manage-list-setting-value"> <td class="manage-list-setting-value">
<div class="manage-list-setting-value-text">{{ conf_var.value|json_encode }}</div> <div class="manage-list-setting-value-text">{{ config_value }}</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -2,44 +2,44 @@
{% from 'macros.twig' import container_title %} {% from 'macros.twig' import container_title %}
{% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %} {% from '_layout/input.twig' import input_csrf, input_text, input_checkbox, input_file, input_select, input_colour %}
{% set title = conf_var.name is empty ? 'Adding a new setting' : ((conf_var.new ? 'Adding ' : 'Editing ') ~ ' setting ' ~ conf_var.name) %} {% set title = config_new ? 'Adding a new setting' : ((config_new ? 'Adding ' : 'Editing ') ~ ' setting ' ~ config_name) %}
{% block manage_content %} {% block manage_content %}
<div class="container manage-settings"> <div class="container manage-settings">
{{ container_title('<i class="fas fa-cogs fa-fw"></i> ' ~ title) }} {{ container_title('<i class="fas fa-cogs fa-fw"></i> ' ~ title) }}
<form method="post" action="{{ url('manage-general-setting', {'name': conf_var.name}) }}" class="manage-setting" id="-msz-manage-setting-form"> <form method="post" action="{{ url('manage-general-setting', {'name': config_name|default()}) }}" class="manage-setting" id="-msz-manage-setting-form">
{{ input_csrf() }} {{ input_csrf() }}
{% if conf_var.new %} {% if config_new %}
<label class="manage-setting-field"> <label class="manage-setting-field">
<div class="manage-setting-field-name">Name</div> <div class="manage-setting-field-name">Name</div>
{{ input_text('conf_name', 'manage__emote__field__value', conf_var.name, 'text', null, false, {'id': '-msz-manage-setting-name'}) }} {{ input_text('conf_name', 'manage__emote__field__value', config_name|default(), 'text', null, false, {'id': '-msz-manage-setting-name'}) }}
</label> </label>
<label class="manage-setting-field"> <label class="manage-setting-field">
<div class="manage-setting-field-name">Type</div> <div class="manage-setting-field-name">Type</div>
{{ input_select( {{ input_select(
'conf_type', 'conf_type',
['', 'string', 'integer', 'boolean', 'array'], ['', 'string', 'int', 'bool', 'float', 'array'],
conf_var.type, null, null, true, 'manage__emote__field__value', config_type, null, null, true, 'manage__emote__field__value',
{'onchange': ('location.assign("' ~ url('manage-general-setting', {'name': '-name-', 'type': '-type-'}) ~ '".replace("-name-", document.getElementById("-msz-manage-setting-name").value).replace("-type-", this.value))')} {'onchange': ('location.assign("' ~ url('manage-general-setting', {'name': '-name-', 'type': '-type-'}) ~ '".replace("-name-", document.getElementById("-msz-manage-setting-name").value).replace("-type-", this.value))')}
) }} ) }}
</label> </label>
{% endif %} {% endif %}
{% if conf_var.type is not empty %} {% if config_type is not empty %}
<label class="manage-setting-field"> <label class="manage-setting-field">
<div class="manage-setting-field-name">Value</div> <div class="manage-setting-field-name">Value</div>
{% if conf_var.type == 'array' %} {% if config_type == 'array' %}
<div class="manage-setting-array"> <div class="manage-setting-array">
<div class="manage-setting-array-warning"> <div class="manage-setting-array-warning">
<noscript><div class="warning"><div class="warning__content">Touching this without Javascript will destroy everything.</div></div></noscript> <noscript><div class="warning"><div class="warning__content">Touching this without Javascript will destroy everything.</div></div></noscript>
</div> </div>
<div class="manage-setting-array-select"> <div class="manage-setting-array-select">
<select class="input__select" multiple id="-msz-manage-setting-array" name="conf_value[]"> <select class="input__select" multiple id="-msz-manage-setting-array" name="conf_value[]">
{% for entry in conf_var.value %} {% for entry in config_value %}
<option>{{ entry }}</option> <option>{{ entry }}</option>
{% endfor %} {% endfor %}
</select> </select>
@ -86,12 +86,14 @@
} }
}); });
</script> </script>
{% elseif conf_var.type == 'string' %} {% elseif config_type == 'string' %}
{{ input_text('conf_value', 'manage-setting-field-value', conf_var.value, 'text') }} {{ input_text('conf_value', 'manage-setting-field-value', config_value, 'text') }}
{% elseif conf_var.type == 'integer' %} {% elseif config_type == 'int' %}
{{ input_text('conf_value', 'manage-setting-field-value', conf_var.value, 'number') }} {{ input_text('conf_value', 'manage-setting-field-value', config_value, 'number') }}
{% elseif conf_var.type == 'boolean' %} {% elseif config_type == 'float' %}
{{ input_checkbox('conf_value', 'Enabled', conf_var.value) }} {{ input_text('conf_value', 'manage-setting-field-value', config_value, 'number', '', false, {'step':'0.01'}) }}
{% elseif config_type == 'bool' %}
{{ input_checkbox('conf_value', 'Enabled', config_value) }}
{% endif %} {% endif %}
</label> </label>

View file

@ -24,20 +24,20 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for var in conf_vars %} {% for var in config_vars %}
<tr class="manage-list-setting"> <tr class="manage-list-setting">
<td class="manage-list-setting-key"> <td class="manage-list-setting-key">
<div class="manage-list-setting-key-text">{{ var.key }}</div> <div class="manage-list-setting-key-text">{{ var.name }}</div>
</td> </td>
<td class="manage-list-setting-type manage-list-setting-type--{{ var.type }}"> <td class="manage-list-setting-type manage-list-setting-type--{{ var.type }}">
<div class="manage-list-setting-type-text">{{ var.type }}</div> <div class="manage-list-setting-type-text">{{ var.type }}</div>
</td> </td>
<td class="manage-list-setting-value"> <td class="manage-list-setting-value">
<div class="manage-list-setting-value-text">{{ var.value }}</div> <div class="manage-list-setting-value-text">{{ var.name in config_hidden ? '*** hidden ***' : var }}</div>
</td> </td>
<td class="manage-list-setting-options"> <td class="manage-list-setting-options">
<a class="input__button input__button--autosize" href="{{ url('manage-general-setting', {'name': var.key}) }}" title="Edit"><i class="fas fa-pen fa-fw"></i></a> <a class="input__button input__button--autosize" href="{{ url('manage-general-setting', {'name': var.name}) }}" title="Edit"><i class="fas fa-pen fa-fw"></i></a>
<a class="input__button input__button--autosize input__button--destroy" href="{{ url('manage-general-setting-delete', {'name': var.key}) }}" title="Delete"><i class="fas fa-times fa-fw"></i></a> <a class="input__button input__button--autosize input__button--destroy" href="{{ url('manage-general-setting-delete', {'name': var.name}) }}" title="Delete"><i class="fas fa-times fa-fw"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}