Authn/z rework.
This commit is contained in:
parent
28be4f16c2
commit
e4c3e4c052
49 changed files with 833 additions and 664 deletions
public-legacy
public
src
Auth
CSRF.phpChangelog
Comments
CsrfContext.phpForum
Home
Info
Messages
MisuzuContext.phpNews
OAuth2
SharpChat
Storage/Uploads
TemplatingExtension.phpUsers
|
@ -68,7 +68,7 @@ if($siteIsPrivate) {
|
|||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Was unable to verify the request, please try again!';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
|
|||
die('Script must be called through the Misuzu route dispatcher.');
|
||||
|
||||
if($msz->authInfo->loggedIn) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
Template::render('auth.logout');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ $remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAdd
|
|||
|
||||
while($canResetPassword) {
|
||||
if(!empty($_POST['verification']) && is_scalar($_POST['verification']) && !empty($userInfo)) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Was unable to verify the request, please try again!';
|
||||
break;
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ while($canResetPassword) {
|
|||
}
|
||||
|
||||
if(!empty($_POST['email']) && is_scalar($_POST['email'])) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Was unable to verify the request, please try again!';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX';
|
|||
$remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress);
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Was unable to verify the request, please try again!';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use Misuzu\Auth\AuthTokenCookie;
|
|||
if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
|
||||
die('Script must be called through the Misuzu route dispatcher.');
|
||||
|
||||
if(CSRF::validateRequest()) {
|
||||
if($msz->csrfCtx->verifyLegacy()) {
|
||||
$tokenInfo = $msz->authInfo->tokenInfo;
|
||||
|
||||
if($tokenInfo->hasImpersonatedUserId) {
|
||||
|
|
|
@ -37,7 +37,7 @@ if($totpInfo === null) {
|
|||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Was unable to verify the request, please try again!';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ if(!empty($_POST)) {
|
|||
$topicType = isset($_POST['type']) ? $_POST['type'] : null;
|
||||
$postSignature = isset($_POST['signature']);
|
||||
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = 'Could not verify request.';
|
||||
} else {
|
||||
$isEditingTopic = empty($topicInfo) || ($mode === 'edit' && $originalPostInfo->id == $postInfo->id);
|
||||
|
|
|
@ -34,7 +34,7 @@ else
|
|||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
$msz->changelog->deleteChange($changeInfo);
|
||||
|
@ -44,7 +44,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
|||
}
|
||||
|
||||
// make errors not echos lol
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$action = !empty($_POST['cl_action']) && is_scalar($_POST['cl_action']) ? trim((string)$_POST['cl_action']) : '';
|
||||
$summary = !empty($_POST['cl_summary']) && is_scalar($_POST['cl_summary']) ? trim((string)$_POST['cl_summary']) : '';
|
||||
$body = !empty($_POST['cl_body']) && is_scalar($_POST['cl_body']) ? trim((string)$_POST['cl_body']) : '';
|
||||
|
|
|
@ -23,7 +23,7 @@ else
|
|||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
$msz->changelog->deleteTag($tagInfo);
|
||||
|
@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
|||
return;
|
||||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$name = !empty($_POST['ct_name']) && is_scalar($_POST['ct_name']) ? trim((string)$_POST['ct_name']) : '';
|
||||
$description = !empty($_POST['ct_desc']) && is_scalar($_POST['ct_desc']) ? trim((string)$_POST['ct_desc']) : '';
|
||||
$archive = !empty($_POST['ct_archive']);
|
||||
|
|
|
@ -8,7 +8,7 @@ if(!$msz->authInfo->getPerms('global')->check(Perm::G_FORUM_TOPIC_REDIRS_MANAGE)
|
|||
Template::throwError(403);
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
throw new \Exception("Request verification failed.");
|
||||
|
||||
$rTopicId = !empty($_POST['topic_redir_id']) && is_scalar($_POST['topic_redir_id']) ? trim((string)$_POST['topic_redir_id']) : '';
|
||||
|
@ -21,7 +21,7 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
}
|
||||
|
||||
if(!empty($_GET['m']) && $_GET['m'] === 'explode') {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
throw new \Exception("Request verification failed.");
|
||||
|
||||
$rTopicId = !empty($_GET['t']) && is_scalar($_GET['t']) ? (string)$_GET['t'] : '';
|
||||
|
|
|
@ -26,7 +26,7 @@ else
|
|||
}
|
||||
|
||||
// make errors not echos lol
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$order = !empty($_POST['em_order']) && is_scalar($_POST['em_order']) ? (int)$_POST['em_order'] : '';
|
||||
$minRank = !empty($_POST['em_minrank']) && is_scalar($_POST['em_minrank']) ? (int)$_POST['em_minrank'] : '';
|
||||
$url = !empty($_POST['em_url']) && is_scalar($_POST['em_url']) ? trim((string)$_POST['em_url']) : '';
|
||||
|
|
|
@ -9,7 +9,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
|
|||
if(!$msz->authInfo->getPerms('global')->check(Perm::G_EMOTES_MANAGE))
|
||||
Template::throwError(403);
|
||||
|
||||
if(CSRF::validateRequest() && !empty($_GET['emote'])) {
|
||||
if($msz->csrfCtx->verifyLegacy() && !empty($_GET['emote'])) {
|
||||
$emoteId = !empty($_GET['emote']) && is_scalar($_GET['emote']) ? (string)$_GET['emote'] : '';
|
||||
|
||||
try {
|
||||
|
|
|
@ -11,7 +11,7 @@ $valueInfo = $msz->config->getValueInfo(!empty($_GET['name']) && is_scalar($_GET
|
|||
if($valueInfo === null)
|
||||
Template::throwError(404);
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$msz->logsCtx->createAuthedLog('CONFIG_DELETE', [$valueInfo->name]);
|
||||
$msz->config->removeValues($valueInfo->name);
|
||||
Tools::redirect($msz->urls->format('manage-general-settings'));
|
||||
|
|
|
@ -25,7 +25,7 @@ if(!empty($sName)) {
|
|||
}
|
||||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
if($isNew) {
|
||||
$sName = !empty($_POST['conf_name']) && is_scalar($_POST['conf_name']) ? trim((string)$_POST['conf_name']) : '';
|
||||
if(!DbConfig::validateName($sName)) {
|
||||
|
|
|
@ -23,7 +23,7 @@ else
|
|||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
$msz->news->deleteCategory($categoryInfo);
|
||||
|
@ -32,7 +32,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
|||
return;
|
||||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$name = !empty($_POST['nc_name']) && is_scalar($_POST['nc_name']) ? trim((string)$_POST['nc_name']) : '';
|
||||
$description = !empty($_POST['nc_desc']) && is_scalar($_POST['nc_desc']) ? trim((string)$_POST['nc_desc']) : '';
|
||||
$hidden = !empty($_POST['nc_hidden']);
|
||||
|
|
|
@ -24,7 +24,7 @@ else
|
|||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
$msz->news->deletePost($postInfo);
|
||||
|
@ -33,7 +33,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
|||
return;
|
||||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$title = !empty($_POST['np_title']) && is_scalar($_POST['np_title']) ? trim((string)$_POST['np_title']) : '';
|
||||
$category = !empty($_POST['np_category']) && is_scalar($_POST['np_category']) ? trim((string)$_POST['np_category']) : '';
|
||||
$featured = !empty($_POST['np_featured']);
|
||||
|
|
|
@ -12,7 +12,7 @@ if(!$msz->authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE))
|
|||
Template::throwError(403);
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
try {
|
||||
|
@ -35,7 +35,7 @@ try {
|
|||
|
||||
$modInfo = $msz->authInfo->userInfo;
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$expires = !empty($_POST['ub_expires']) && is_scalar($_POST['ub_expires']) ? (int)$_POST['ub_expires'] : 0;
|
||||
$expiresCustom = !empty($_POST['ub_expires_custom']) && is_scalar($_POST['ub_expires_custom']) ? trim((string)$_POST['ub_expires_custom']) : '';
|
||||
$publicReason = !empty($_POST['ub_reason_pub']) && is_scalar($_POST['ub_reason_pub']) ? trim((string)$_POST['ub_reason_pub']) : '';
|
||||
|
|
|
@ -35,7 +35,7 @@ if($hasUserId) {
|
|||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
$msz->usersCtx->modNotes->deleteNotes($noteInfo);
|
||||
|
@ -48,7 +48,7 @@ if($hasUserId) {
|
|||
$authorInfo = $noteInfo->authorId !== null ? $msz->usersCtx->getUserInfo($noteInfo->authorId) : null;
|
||||
}
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$title = trim((string)($_POST['mn_title'] ?? ''));
|
||||
$body = trim((string)($_POST['mn_body'] ?? ''));
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ $canEditPerms = $viewerPerms->check(Perm::U_PERMS_MANAGE);
|
|||
$permsInfos = $roleInfo === null ? null : $msz->perms->getPermissionInfo(roleInfo: $roleInfo, categoryNames: Perm::INFO_FOR_ROLE);
|
||||
$permsLists = Perm::createList(Perm::LISTS_FOR_ROLE);
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$userRank = $msz->usersCtx->users->getUserRank($currentUser);
|
||||
|
||||
if(!$isNew && !$currentUser->super && $roleInfo->rank >= $userRank) {
|
||||
|
|
|
@ -47,7 +47,7 @@ $permsInfos = $msz->perms->getPermissionInfo(userInfo: $userInfo, categoryNames:
|
|||
$permsLists = Perm::createList(Perm::LISTS_FOR_USER);
|
||||
$permsNeedRecalc = false;
|
||||
|
||||
if(CSRF::validateRequest() && $canEdit) {
|
||||
if($msz->csrfCtx->verifyLegacy() && $canEdit) {
|
||||
if(!empty($_POST['impersonate_user'])) {
|
||||
if(!$canImpersonate) {
|
||||
$notices[] = 'You must be a super user to do this.';
|
||||
|
|
|
@ -10,7 +10,7 @@ if(!$msz->authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE))
|
|||
Template::throwError(403);
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) {
|
||||
if(!CSRF::validateRequest())
|
||||
if(!$msz->csrfCtx->verifyLegacy())
|
||||
Template::throwError(403);
|
||||
|
||||
try {
|
||||
|
@ -33,7 +33,7 @@ try {
|
|||
|
||||
$modInfo = $msz->authInfo->userInfo;
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$body = trim((string)($_POST['uw_body'] ?? ''));
|
||||
Template::set('warn_value_body', $body);
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ if($isEditing) {
|
|||
]);
|
||||
|
||||
if(!empty($_POST)) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
if(!$msz->csrfCtx->verifyLegacy()) {
|
||||
$notices[] = "Couldn't verify you, please refresh the page and retry.";
|
||||
} else {
|
||||
if(!$perms->edit_profile) {
|
||||
|
|
|
@ -16,7 +16,7 @@ $errors = [];
|
|||
$userInfo = $msz->authInfo->userInfo;
|
||||
$isRestricted = $msz->usersCtx->hasActiveBan($userInfo);
|
||||
$hasTotp = $msz->usersCtx->totps->hasUserTotp($userInfo);
|
||||
$isVerifiedRequest = CSRF::validateRequest();
|
||||
$isVerifiedRequest = $msz->csrfCtx->verifyLegacy();
|
||||
|
||||
if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
|
||||
try {
|
||||
|
|
|
@ -13,7 +13,7 @@ $errors = [];
|
|||
$currentUser = $msz->authInfo->userInfo;
|
||||
$activeSessionId = $msz->authInfo->sessionId;
|
||||
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
|
||||
while($_SERVER['REQUEST_METHOD'] === 'POST' && $msz->csrfCtx->verifyLegacy()) {
|
||||
$sessionId = !empty($_POST['session']) && is_scalar($_POST['session']) ? trim((string)$_POST['session']) : '';
|
||||
$activeSessionKilled = false;
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ use Index\MediaType;
|
|||
use Index\Http\Content\MultipartFormContent;
|
||||
use Index\Http\Content\Multipart\ValueMultipartFormData;
|
||||
use Index\Http\Routing\Router;
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\RouteInfo;
|
||||
use Misuzu\Auth\{AuthTokenBuilder,AuthTokenCookie,AuthTokenInfo};
|
||||
|
||||
require_once __DIR__ . '/../misuzu.php';
|
||||
|
||||
|
@ -28,90 +28,6 @@ if(is_file($msz->dbCtx->getMigrateLockPath())) {
|
|||
|
||||
$request = \Index\Http\HttpRequest::fromRequest();
|
||||
|
||||
$tokenPacker = $msz->authCtx->createAuthTokenPacker();
|
||||
|
||||
if(!empty($_COOKIE['msz_auth']) && is_string($_COOKIE['msz_auth']))
|
||||
$tokenInfo = $tokenPacker->unpack($_COOKIE['msz_auth']);
|
||||
elseif(!empty($_COOKIE['msz_uid']) && !empty($_COOKIE['msz_sid']) && is_string($_COOKIE['msz_uid']) && is_string($_COOKIE['msz_sid'])) {
|
||||
$tokenBuilder = new AuthTokenBuilder;
|
||||
$tokenBuilder->setUserId($_COOKIE['msz_uid']);
|
||||
$tokenBuilder->setSessionToken($_COOKIE['msz_sid']);
|
||||
$tokenInfo = $tokenBuilder->toInfo();
|
||||
$tokenBuilder = null;
|
||||
} else
|
||||
$tokenInfo = AuthTokenInfo::empty();
|
||||
|
||||
$userInfo = null;
|
||||
$sessionInfo = null;
|
||||
$userInfoReal = null;
|
||||
$remoteAddr = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
if($tokenInfo->hasUserId && $tokenInfo->hasSessionToken) {
|
||||
$tokenBuilder = new AuthTokenBuilder($tokenInfo);
|
||||
|
||||
try {
|
||||
$sessionInfo = $msz->authCtx->sessions->getSession(sessionToken: $tokenInfo->sessionToken);
|
||||
|
||||
if($sessionInfo->expired) {
|
||||
$tokenBuilder->removeUserId();
|
||||
$tokenBuilder->removeSessionToken();
|
||||
} elseif($sessionInfo->userId === $tokenInfo->userId) {
|
||||
$userInfo = $msz->usersCtx->users->getUser($tokenInfo->userId, 'id');
|
||||
|
||||
if($userInfo->deleted) {
|
||||
$tokenBuilder->removeUserId();
|
||||
$tokenBuilder->removeSessionToken();
|
||||
} else {
|
||||
$msz->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $remoteAddr);
|
||||
$msz->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $remoteAddr);
|
||||
if($sessionInfo->shouldBumpExpires)
|
||||
$tokenBuilder->setEdited();
|
||||
|
||||
if($tokenInfo->hasImpersonatedUserId) {
|
||||
$allowToImpersonate = $userInfo->super;
|
||||
$impersonatedUserId = $tokenInfo->impersonatedUserId;
|
||||
|
||||
if(!$allowToImpersonate) {
|
||||
$allowImpersonateUsers = $msz->config->getArray(sprintf('impersonate.allow.u%s', $userInfo->id));
|
||||
$allowToImpersonate = in_array((string)$impersonatedUserId, $allowImpersonateUsers, true);
|
||||
}
|
||||
|
||||
if($allowToImpersonate) {
|
||||
$userInfoReal = $userInfo;
|
||||
|
||||
try {
|
||||
$userInfo = $msz->usersCtx->users->getUser($impersonatedUserId, 'id');
|
||||
} catch(RuntimeException $ex) {
|
||||
$userInfo = $userInfoReal;
|
||||
$userInfoReal = null;
|
||||
$tokenBuilder->removeImpersonatedUserId();
|
||||
}
|
||||
} else $tokenBuilder->removeImpersonatedUserId();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(RuntimeException $ex) {
|
||||
$tokenBuilder->removeUserId();
|
||||
$tokenBuilder->removeSessionToken();
|
||||
$tokenBuilder->removeImpersonatedUserId();
|
||||
$userInfo = null;
|
||||
$sessionInfo = null;
|
||||
$userInfoReal = null;
|
||||
}
|
||||
|
||||
if($tokenBuilder->isEdited()) {
|
||||
$tokenInfo = $tokenBuilder->toInfo();
|
||||
AuthTokenCookie::apply($tokenPacker->pack($tokenInfo));
|
||||
}
|
||||
}
|
||||
|
||||
$msz->authInfo->setInfo($tokenInfo, $userInfo, $sessionInfo, $userInfoReal);
|
||||
|
||||
CSRF::init(
|
||||
$msz->config->getString('csrf.secret', 'soup'),
|
||||
($msz->authInfo->loggedIn ? $sessionInfo->token : $remoteAddr)
|
||||
);
|
||||
|
||||
// order for these two currently matters i think: it shouldn't.
|
||||
$router = $msz->createRouting($request);
|
||||
$msz->startTemplating();
|
||||
|
@ -125,16 +41,21 @@ if($msz->domainRoles->hasRole($request->getHeaderLine('Host'), 'main')) {
|
|||
$mszLegacyPathReal = realpath($mszLegacyPath);
|
||||
if($mszLegacyPath === $mszLegacyPathReal || $mszLegacyPath === $mszLegacyPathReal . '/') {
|
||||
// this is here so filters can run...
|
||||
$router->router->route(RouteInfo::exact($request->method, $request->requestTarget, function() {}));
|
||||
$router->router->route(RouteInfo::exact(
|
||||
$request->method,
|
||||
$request->requestTarget,
|
||||
#[Before('authz:cookie')]
|
||||
function() use ($msz, $mszRequestPath) {
|
||||
if(str_starts_with($mszRequestPath, 'manage') && !$msz->hasManageAccess())
|
||||
return 403;
|
||||
},
|
||||
));
|
||||
$response = $router->router->handle($request);
|
||||
if($response->getBody()->getSize() > 0) {
|
||||
Router::output($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
if(str_starts_with($mszRequestPath, 'manage') && !$msz->hasManageAccess())
|
||||
Template::throwError(403);
|
||||
|
||||
if(is_dir($mszLegacyPath))
|
||||
$mszLegacyPath .= '/index.php';
|
||||
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
use Misuzu\OAuth2\{OAuth2AccessInfoGetField,OAuth2Context};
|
||||
use Misuzu\Users\{UsersContext,UserInfo};
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Filters\PrefixFilter;
|
||||
|
||||
final class AuthApiRoutes implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
public function __construct(
|
||||
private Config $impersonateConfig,
|
||||
private UsersContext $usersCtx,
|
||||
private OAuth2Context $oauth2Ctx,
|
||||
private AuthContext $authCtx,
|
||||
private AuthInfo $authInfo,
|
||||
) {}
|
||||
|
||||
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
|
||||
if($impersonator->super)
|
||||
return true;
|
||||
|
||||
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->id));
|
||||
return in_array($targetId, $whitelist, true);
|
||||
}
|
||||
|
||||
/** @return void|array{error: string, error_description?: string} */
|
||||
#[PrefixFilter('/api/v1')]
|
||||
#[PrefixFilter('/oauth2')]
|
||||
#[PrefixFilter('/uploads')]
|
||||
public function handleAuthorization(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
if($this->authInfo->loggedIn)
|
||||
return;
|
||||
|
||||
$authz = explode(' ', $request->getHeaderLine('Authorization'), 2);
|
||||
if(count($authz) < 2)
|
||||
return;
|
||||
|
||||
[$method, $token] = $authz;
|
||||
|
||||
if(strcasecmp('misuzu', $method) === 0) {
|
||||
$tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);
|
||||
if(!$tokenInfo->isEmpty)
|
||||
$token = $tokenInfo->sessionToken;
|
||||
|
||||
try {
|
||||
$sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $token);
|
||||
} catch(RuntimeException $ex) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Misuzu error="invalid_token", error_description="Misuzu token has expired."');
|
||||
return;
|
||||
}
|
||||
|
||||
if($sessionInfo->expired) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Misuzu error="invalid_token", error_description="Misuzu token has expired."');
|
||||
$this->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $request->remoteAddress);
|
||||
|
||||
$userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
|
||||
$userInfoReal = null;
|
||||
if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
|
||||
$userInfoReal = $userInfo;
|
||||
|
||||
try {
|
||||
$userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
|
||||
} catch(RuntimeException $ex) {
|
||||
$userInfo = $userInfoReal;
|
||||
}
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
tokenInfo: $tokenInfo,
|
||||
userInfo: $userInfo,
|
||||
sessionInfo: $sessionInfo,
|
||||
realUserInfo: $userInfoReal,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(strcasecmp('basic', $method) === 0) {
|
||||
$token = base64_decode($token);
|
||||
if(empty($token)) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Basic error="invalid_token", error_description="Basic credentials are invalid."');
|
||||
return;
|
||||
}
|
||||
|
||||
$authz = explode(':', $token, 2);
|
||||
if(count($authz) < 2) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Basic error="invalid_token", error_description="Basic credentials are invalid."');
|
||||
return;
|
||||
}
|
||||
|
||||
[$clientId, $clientSecret] = $authz;
|
||||
|
||||
try {
|
||||
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(clientId: $clientId, deleted: false);
|
||||
} catch(RuntimeException $ex) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Basic error="invalid_token", error_description="Basic credentials are invalid."');
|
||||
return;
|
||||
}
|
||||
|
||||
if($appInfo->confidential) {
|
||||
// TODO: rate limiting
|
||||
|
||||
if(!$appInfo->verifyClientSecret($clientSecret)) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Basic error="invalid_token", error_description="Basic credentials are invalid."');
|
||||
return;
|
||||
}
|
||||
} elseif($clientSecret !== '') {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Basic error="invalid_token", error_description="Basic credentials are invalid."');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
appInfo: $appInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(strcasecmp('bearer', $method) === 0) {
|
||||
try {
|
||||
$accessInfo = $this->oauth2Ctx->tokens->getAccessInfo($token, OAuth2AccessInfoGetField::Token);
|
||||
} catch(RuntimeException $ex) {
|
||||
$accessInfo = null;
|
||||
}
|
||||
|
||||
if($accessInfo?->expired !== false) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Bearer error="invalid_token", error_description="Access token has expired."');
|
||||
return [
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'Access token has expired.',
|
||||
];
|
||||
}
|
||||
|
||||
$userInfo = null;
|
||||
if($accessInfo->userId !== null)
|
||||
$userInfo = $this->usersCtx->users->getUser($accessInfo->userId, 'id');
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
userInfo: $userInfo,
|
||||
accessInfo: $accessInfo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,6 +46,11 @@ class AuthInfo {
|
|||
$this->setInfo();
|
||||
}
|
||||
|
||||
public bool $hasInfo {
|
||||
get => $this->userInfo !== null
|
||||
|| $this->appInfo !== null;
|
||||
}
|
||||
|
||||
public bool $loggedIn {
|
||||
get => $this->userInfo !== null;
|
||||
}
|
||||
|
@ -55,11 +60,13 @@ class AuthInfo {
|
|||
}
|
||||
|
||||
public bool $loggedInBearer {
|
||||
get => $this->accessInfo !== null && $this->userInfo !== null;
|
||||
get => $this->accessInfo !== null
|
||||
&& $this->userInfo !== null;
|
||||
}
|
||||
|
||||
public bool $loggedInBearerClient {
|
||||
get => $this->accessInfo !== null && $this->userInfo === null;
|
||||
get => $this->accessInfo !== null
|
||||
&& $this->userInfo === null;
|
||||
}
|
||||
|
||||
public ?string $userId {
|
||||
|
|
468
src/Auth/AuthProcessors.php
Normal file
468
src/Auth/AuthProcessors.php
Normal file
|
@ -0,0 +1,468 @@
|
|||
<?php
|
||||
namespace Misuzu\Auth;
|
||||
|
||||
use RuntimeException;
|
||||
use Carbon\Carbon;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Content\FormContent;
|
||||
use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Preprocessor;
|
||||
use Misuzu\{CsrfContext,Misuzu};
|
||||
use Misuzu\OAuth2\{OAuth2AccessInfoGetField,OAuth2Context};
|
||||
use Misuzu\Users\{BansData,UsersContext,UserInfo};
|
||||
|
||||
final class AuthProcessors implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
public function __construct(
|
||||
private Config $impersonateConfig,
|
||||
private UsersContext $usersCtx,
|
||||
private OAuth2Context $oauth2Ctx,
|
||||
private CsrfContext $csrfCtx,
|
||||
private AuthContext $authCtx,
|
||||
private AuthInfo $authInfo,
|
||||
) {}
|
||||
|
||||
private function canImpersonateUserId(UserInfo $impersonator, string $targetId): bool {
|
||||
if($impersonator->super)
|
||||
return true;
|
||||
|
||||
$whitelist = $this->impersonateConfig->getArray(sprintf('allow.u%s', $impersonator->id));
|
||||
return in_array($targetId, $whitelist, true);
|
||||
}
|
||||
|
||||
/** @param array<string, string> $error */
|
||||
private static function applyErrorHeader(HttpResponseBuilder $response, string $method, array $error): void {
|
||||
$parts = [];
|
||||
foreach($error as $name => $value)
|
||||
$parts[] = sprintf('%s="%s"', rawurlencode($name), rawurlencode($value));
|
||||
|
||||
$response->addHeader('WWW-Authenticate', sprintf('%s %s', $method, implode(', ', $parts)));
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('authz:perm')]
|
||||
public function preAuthzPermissionCheck(
|
||||
HandlerContext $context,
|
||||
string $category,
|
||||
int $perm,
|
||||
string $type = '',
|
||||
) {
|
||||
if($this->authInfo->loggedIn && $this->authInfo->getPerms($category)->check($perm))
|
||||
return;
|
||||
|
||||
if($type === 'json') {
|
||||
$context->response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'permission',
|
||||
'text' => 'You are not allowed to do this.',
|
||||
],
|
||||
];
|
||||
} else return 403;
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('authz:private')]
|
||||
public function preAuthzPrivate(
|
||||
HandlerContext $context,
|
||||
string $type = '',
|
||||
string $argName = 'impersonator',
|
||||
bool $allowDebug = true,
|
||||
) {
|
||||
if($type === 'arg' && isset($context->args[$argName]))
|
||||
return;
|
||||
|
||||
$result = null;
|
||||
if((!$allowDebug || !Misuzu::debug()) && $this->authInfo->loggedIn && $this->authInfo->impersonating)
|
||||
$result = $this->authInfo->realUserInfo;
|
||||
|
||||
if($type === 'arg')
|
||||
$context->setArgument($argName, $result);
|
||||
|
||||
if($result !== null) {
|
||||
if($type === 'json') {
|
||||
$context->response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'impersonating',
|
||||
'text' => 'You are not allowed to do this while impersonating someone.',
|
||||
],
|
||||
];
|
||||
} else return 403;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('authz:banned')]
|
||||
public function preAuthzBanned(
|
||||
HandlerContext $context,
|
||||
int $minimumSeverity = BansData::SEVERITY_MIN,
|
||||
string $type = '',
|
||||
string $argName = 'banned',
|
||||
) {
|
||||
if($type === 'arg' && isset($context->args[$argName]))
|
||||
return;
|
||||
|
||||
$result = null;
|
||||
if($this->authInfo->loggedIn)
|
||||
$result = $this->usersCtx->tryGetActiveBan($this->authInfo->userInfo, $minimumSeverity);
|
||||
|
||||
if($type === 'arg')
|
||||
$context->setArgument($argName, $result);
|
||||
|
||||
if($result !== null) {
|
||||
if($type === 'json') {
|
||||
$context->response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'ban',
|
||||
'text' => 'You are banned, check your profile for more information.',
|
||||
],
|
||||
];
|
||||
} else return 403;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('authz:cookie')]
|
||||
public function preAuthzCookie(
|
||||
HttpResponseBuilder $response,
|
||||
HttpRequest $request,
|
||||
bool $required = false,
|
||||
string $type = 'html', // should just be replaced with an Accept header read?
|
||||
) {
|
||||
if($this->authInfo->hasInfo)
|
||||
return;
|
||||
|
||||
$result = (function() use ($response, $request) {
|
||||
$packer = $this->authCtx->createAuthTokenPacker();
|
||||
$builder = $tokenInfo = $userInfo = $sessionInfo = $userInfoReal = null;
|
||||
$mszAuth = $mszUserId = $mszSessionId = null;
|
||||
|
||||
try {
|
||||
$mszAuth = trim($request->getCookie('msz_auth'));
|
||||
if(!empty($mszAuth)) {
|
||||
$tokenInfo = $packer->unpack($mszAuth);
|
||||
} else {
|
||||
$mszUserId = trim($request->getCookie('msz_uid'));
|
||||
$mszSessionId = trim($request->getCookie('msz_sid'));
|
||||
|
||||
if(!empty($mszUserId) && !empty($mszSessionId))
|
||||
$tokenInfo = (function($builder) use ($mszUserId, $mszSessionId) {
|
||||
$builder->setUserId($mszUserId);
|
||||
$builder->setSessionToken($mszSessionId);
|
||||
return $builder->toInfo();
|
||||
})(new AuthTokenBuilder);
|
||||
}
|
||||
|
||||
if(empty($tokenInfo) || $tokenInfo->isEmpty || !$tokenInfo->hasUserId || !$tokenInfo->hasSessionToken)
|
||||
return false;
|
||||
|
||||
$builder = new AuthTokenBuilder($tokenInfo);
|
||||
|
||||
try {
|
||||
$sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $tokenInfo->sessionToken);
|
||||
if($sessionInfo->expired || $sessionInfo->userId !== $tokenInfo->userId) {
|
||||
$sessionInfo = null;
|
||||
$builder->removeUserId();
|
||||
$builder->removeSessionToken();
|
||||
return false;
|
||||
}
|
||||
|
||||
$userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
|
||||
if($userInfo->deleted) {
|
||||
$sessionInfo = $userInfo = null;
|
||||
$builder->removeUserId();
|
||||
$builder->removeSessionToken();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $request->remoteAddress);
|
||||
$this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $request->remoteAddress);
|
||||
if($sessionInfo->shouldBumpExpires)
|
||||
$builder->setEdited();
|
||||
|
||||
if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
|
||||
$userInfoReal = $userInfo;
|
||||
|
||||
try {
|
||||
$userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
|
||||
} catch(RuntimeException $ex) {
|
||||
$userInfo = $userInfoReal;
|
||||
$userInfoReal = null;
|
||||
$builder->removeImpersonatedUserId();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(RuntimeException $ex) {
|
||||
$builder->removeUserId();
|
||||
$builder->removeSessionToken();
|
||||
$builder->removeImpersonatedUserId();
|
||||
$sessionInfo = $userInfo = $userInfoReal = null;
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
$host = $request->getHeaderLine('Host');
|
||||
|
||||
if($mszUserId !== null)
|
||||
$response->removeCookie('msz_uid', '/', $host, $request->secure, true);
|
||||
|
||||
if($mszSessionId !== null)
|
||||
$response->removeCookie('msz_sid', '/', $host, $request->secure, true);
|
||||
|
||||
if($builder?->isEdited() === true) {
|
||||
$tokenInfo = $builder->toInfo();
|
||||
$response->addCookie(
|
||||
'msz_auth',
|
||||
$packer->pack($tokenInfo),
|
||||
Carbon::now()->addMonths(3),
|
||||
'/',
|
||||
$host,
|
||||
$request->secure,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
tokenInfo: $tokenInfo,
|
||||
userInfo: $userInfo,
|
||||
sessionInfo: $sessionInfo,
|
||||
realUserInfo: $userInfoReal,
|
||||
);
|
||||
|
||||
$this->csrfCtx->setIdentity(
|
||||
$this->authInfo->loggedIn ? $this->authInfo->sessionInfo->token : $request->remoteAddress
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
if($required && !$result) {
|
||||
if($type === 'json') {
|
||||
$response->statusCode = 401;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'authz',
|
||||
'text' => 'You must be logged in to do that.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return 401;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: string, error_description: string} */
|
||||
#[Preprocessor('authz:misuzu')]
|
||||
public function preAuthzMisuzu(
|
||||
HttpResponseBuilder $response,
|
||||
HttpRequest $request,
|
||||
bool $required = true,
|
||||
string $type = 'json',
|
||||
) {
|
||||
if($this->authInfo->hasInfo)
|
||||
return;
|
||||
|
||||
$result = (function() use ($request) {
|
||||
$authz = explode(' ', $request->getHeaderLine('Authorization'), 2);
|
||||
if(count($authz) < 2 || strcasecmp('misuzu', $authz[0]) !== 0)
|
||||
return false;
|
||||
|
||||
$token = $authz[1];
|
||||
$tokenInfo = $this->authCtx->createAuthTokenPacker()->unpack($token);
|
||||
if(!$tokenInfo->isEmpty)
|
||||
$token = $tokenInfo->sessionToken;
|
||||
|
||||
try {
|
||||
$sessionInfo = $this->authCtx->sessions->getSession(sessionToken: $token);
|
||||
} catch(RuntimeException $ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($sessionInfo->expired) {
|
||||
$this->authCtx->sessions->deleteSessions(sessionInfos: $sessionInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
$userInfo = $this->usersCtx->users->getUser($sessionInfo->userId, 'id');
|
||||
if($userInfo->deleted)
|
||||
return false;
|
||||
|
||||
$this->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $request->remoteAddress);
|
||||
$this->authCtx->sessions->recordSessionActivity(sessionInfo: $sessionInfo, remoteAddr: $request->remoteAddress);
|
||||
|
||||
$userInfoReal = null;
|
||||
if($tokenInfo->hasImpersonatedUserId && $this->canImpersonateUserId($userInfo, $tokenInfo->impersonatedUserId)) {
|
||||
$userInfoReal = $userInfo;
|
||||
|
||||
try {
|
||||
$userInfo = $this->usersCtx->users->getUser($tokenInfo->impersonatedUserId, 'id');
|
||||
} catch(RuntimeException $ex) {
|
||||
$userInfo = $userInfoReal;
|
||||
}
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
tokenInfo: $tokenInfo,
|
||||
userInfo: $userInfo,
|
||||
sessionInfo: $sessionInfo,
|
||||
realUserInfo: $userInfoReal,
|
||||
);
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
if($required && !$result) {
|
||||
$info = [
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'Misuzu token has expired.',
|
||||
];
|
||||
|
||||
self::applyErrorHeader($response, 'Misuzu', $info);
|
||||
|
||||
if($type === 'json') {
|
||||
$response->statusCode = 401;
|
||||
return $info;
|
||||
}
|
||||
|
||||
return 401;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: string, error_description: string} */
|
||||
#[Preprocessor('authz:basic')]
|
||||
public function preAuthzBasic(
|
||||
HttpResponseBuilder $response,
|
||||
HttpRequest $request,
|
||||
?FormContent $content = null,
|
||||
bool $required = true,
|
||||
string $type = 'json',
|
||||
bool $bodyId = true,
|
||||
bool $bodySecret = false,
|
||||
) {
|
||||
if($this->authInfo->hasInfo)
|
||||
return;
|
||||
|
||||
$response->setCacheControl('no-store');
|
||||
|
||||
$result = (function() use ($request, $content, $bodyId, $bodySecret) {
|
||||
if($request->hasHeader('Authorization')) {
|
||||
$token = explode(' ', $request->getHeaderLine('Authorization'), 2);
|
||||
if(count($token) < 2 || strcasecmp('basic', $token[0]) !== 0)
|
||||
return false;
|
||||
|
||||
$token = base64_decode($token[1]);
|
||||
if(empty($token))
|
||||
return false;
|
||||
|
||||
$token = explode(':', $token, 2);
|
||||
if(count($token) < 2)
|
||||
return false;
|
||||
|
||||
[$clientId, $clientSecret] = $token;
|
||||
} elseif($bodyId && $content instanceof FormContent) {
|
||||
$clientId = $content->getParam('client_id');
|
||||
$clientSecret = $bodySecret ? $content->getParam('client_secret') : '';
|
||||
} else return false;
|
||||
|
||||
try {
|
||||
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(clientId: $clientId, deleted: false);
|
||||
} catch(RuntimeException $ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($appInfo->confidential) {
|
||||
// TODO: rate limiting
|
||||
|
||||
if(!$appInfo->verifyClientSecret($clientSecret))
|
||||
return false;
|
||||
} elseif(trim($clientSecret) !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
appInfo: $appInfo,
|
||||
);
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
if($required && !$result) {
|
||||
$info = [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'Client authentication failed.',
|
||||
];
|
||||
|
||||
self::applyErrorHeader($response, 'Basic', $info);
|
||||
|
||||
if($type === 'json') {
|
||||
$response->statusCode = 401;
|
||||
return $info;
|
||||
}
|
||||
|
||||
return 401;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: string, error_description: string} */
|
||||
#[Preprocessor('authz:bearer')]
|
||||
public function preAuthzBearer(
|
||||
HttpResponseBuilder $response,
|
||||
HttpRequest $request,
|
||||
bool $required = true,
|
||||
string $type = 'json',
|
||||
) {
|
||||
if($this->authInfo->hasInfo)
|
||||
return;
|
||||
|
||||
$result = (function() use ($request) {
|
||||
$authz = explode(' ', $request->getHeaderLine('Authorization'), 2);
|
||||
if(count($authz) < 2 || strcasecmp('basic', $authz[0]) !== 0)
|
||||
return false;
|
||||
|
||||
try {
|
||||
$accessInfo = $this->oauth2Ctx->tokens->getAccessInfo($authz[1], OAuth2AccessInfoGetField::Token);
|
||||
} catch(RuntimeException $ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($accessInfo->expired)
|
||||
return false;
|
||||
|
||||
$userInfo = null;
|
||||
if($accessInfo->userId !== null) {
|
||||
$userInfo = $this->usersCtx->users->getUser($accessInfo->userId, 'id');
|
||||
if($userInfo->deleted)
|
||||
return false;
|
||||
|
||||
$this->usersCtx->users->recordUserActivity($userInfo, remoteAddr: $request->remoteAddress);
|
||||
}
|
||||
|
||||
$this->authInfo->setInfo(
|
||||
userInfo: $userInfo,
|
||||
accessInfo: $accessInfo,
|
||||
);
|
||||
|
||||
return true;
|
||||
})();
|
||||
|
||||
if($required && !$result) {
|
||||
$info = [
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'Access token has expired.',
|
||||
];
|
||||
|
||||
self::applyErrorHeader($response, 'Bearer', $info);
|
||||
|
||||
if($type === 'json') {
|
||||
$response->statusCode = 401;
|
||||
return $info;
|
||||
}
|
||||
|
||||
return 401;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,9 +10,6 @@ final class AuthTokenCookie {
|
|||
if(empty($url))
|
||||
$url = $_SERVER['HTTP_HOST'];
|
||||
|
||||
if(!filter_var($url, FILTER_VALIDATE_IP))
|
||||
$url = '.' . $url;
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
|
45
src/CSRF.php
45
src/CSRF.php
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Index\CsrfToken;
|
||||
|
||||
final class CSRF {
|
||||
private static ?CsrfToken $instance = null;
|
||||
private static string $secretKey = '';
|
||||
|
||||
public static function available(): bool {
|
||||
return self::$instance !== null;
|
||||
}
|
||||
|
||||
public static function create(string $identity, ?string $secretKey = null): CsrfToken {
|
||||
if($secretKey === null)
|
||||
$secretKey = self::$secretKey;
|
||||
else
|
||||
self::$secretKey = $secretKey;
|
||||
|
||||
return new CsrfToken($secretKey, $identity);
|
||||
}
|
||||
|
||||
public static function init(string $secretKey, string $identity): void {
|
||||
self::$instance = self::create($identity, $secretKey);
|
||||
}
|
||||
|
||||
public static function validate(string $token, int $tolerance = -1): bool {
|
||||
return self::$instance?->verifyToken($token, $tolerance) ?? false;
|
||||
}
|
||||
|
||||
public static function token(): string {
|
||||
return self::$instance?->createToken() ?? '';
|
||||
}
|
||||
|
||||
public static function validateRequest(int $tolerance = -1): bool {
|
||||
if(self::$instance === null)
|
||||
return false;
|
||||
|
||||
$token = isset($_POST['_csrf']) && is_string($_POST['_csrf']) ? $_POST['_csrf'] : '';
|
||||
if(empty($token))
|
||||
$token = isset($_GET['csrf']) && is_string($_GET['csrf']) ? $_GET['csrf'] : '';
|
||||
|
||||
return self::$instance->verifyToken($token, $tolerance);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ use ErrorException;
|
|||
use RuntimeException;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Syndication\FeedBuilder;
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
|
@ -24,6 +25,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
|
|||
) {}
|
||||
|
||||
#[ExactRoute('GET', '/changelog')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('changelog-index', '/changelog', ['date' => '<date>', 'user' => '<user>', 'tags' => '<tags>', 'p' => '<page>'])]
|
||||
public function getIndex(HttpResponseBuilder $response, HttpRequest $request): int|string {
|
||||
$filterDate = (string)$request->getParam('date');
|
||||
|
@ -98,6 +100,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('GET', '/changelog/change/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('changelog-change', '/changelog/change/<change>')]
|
||||
#[UrlFormat('changelog-change-comments', '/changelog/change/<change>', fragment: 'comments')]
|
||||
public function getChange(HttpResponseBuilder $response, HttpRequest $request, string $changeId): int|string {
|
||||
|
|
|
@ -6,11 +6,10 @@ use Index\XArray;
|
|||
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Content\FormContent;
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Filters\PrefixFilter;
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Perm};
|
||||
use Misuzu\Perm;
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Perms\{PermissionResult,IPermissionResult};
|
||||
use Misuzu\Users\{UserInfo,UsersContext,UsersData};
|
||||
|
@ -189,21 +188,6 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
];
|
||||
}
|
||||
|
||||
/** @return void|array{error: array{name: string, text: string}} */
|
||||
#[PrefixFilter('/comments')]
|
||||
public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return self::error($response, 401, 'comments:auth', 'You must be logged in to use the comments system.');
|
||||
if(!CSRF::validate($request->getHeaderLine('x-csrf-token')))
|
||||
return self::error($response, 403, 'comments:csrf', 'Request could not be verified. Please try again.');
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo))
|
||||
return self::error($response, 403, 'comments:csrf', 'You are banned, check your profile for more information.');
|
||||
}
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* category: array{
|
||||
|
@ -233,6 +217,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* }|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('GET', '/comments/categories/([A-Za-z0-9-]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getCategory(HttpResponseBuilder $response, string $categoryName): array {
|
||||
try {
|
||||
$catInfo = $this->commentsCtx->categories->getCategory(name: $categoryName);
|
||||
|
@ -295,6 +280,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* }|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('POST', '/comments/categories/([A-Za-z0-9-]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
public function patchCategory(HttpResponseBuilder $response, FormContent $content, string $categoryName): array {
|
||||
try {
|
||||
|
@ -335,6 +323,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return mixed[]|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[ExactRoute('POST', '/comments/posts')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[Before('input:multipart')]
|
||||
public function postPost(HttpResponseBuilder $response, FormContent $content): array {
|
||||
$perms = $this->getGlobalPerms();
|
||||
|
@ -397,6 +388,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return mixed[]|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('GET', '/comments/posts/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getPost(HttpResponseBuilder $response, string $commentId): array {
|
||||
try {
|
||||
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||
|
@ -422,6 +414,7 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return mixed[]|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('GET', '/comments/posts/([0-9]+)/replies')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getPostReplies(HttpResponseBuilder $response, string $commentId): array {
|
||||
try {
|
||||
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||
|
@ -446,6 +439,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* }|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('PATCH', '/comments/posts/([0-9]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[Before('input:multipart')]
|
||||
public function patchPost(HttpResponseBuilder $response, FormContent $content, string $commentId): array {
|
||||
try {
|
||||
|
@ -515,6 +511,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return string|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('DELETE', '/comments/posts/([0-9]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
public function deletePost(HttpResponseBuilder $response, string $commentId): array|string {
|
||||
try {
|
||||
$postInfo = $this->commentsCtx->posts->getPost($commentId);
|
||||
|
@ -544,6 +543,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return mixed[]|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('POST', '/comments/posts/([0-9]+)/restore')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
public function postPostRestore(HttpResponseBuilder $response, string $commentId): array {
|
||||
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
|
||||
return self::error($response, 403, 'comments:restore-not-allowed', 'You are not allowed to restore comments.');
|
||||
|
@ -569,6 +571,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* @return mixed[]|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('POST' ,'/comments/posts/([0-9]+)/nuke')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
public function postPostNuke(HttpResponseBuilder $response, string $commentId): array {
|
||||
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_DELETE_ANY))
|
||||
return self::error($response, 403, 'comments:nuke-not-allowed', 'You are not allowed to permanently delete comments.');
|
||||
|
@ -598,6 +603,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* }|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('POST', '/comments/posts/([0-9]+)/vote')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
public function postPostVote(HttpResponseBuilder $response, FormContent $content, string $commentId): array {
|
||||
$vote = (int)$content->getFilteredParam('vote', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
@ -644,6 +652,9 @@ class CommentsRoutes implements RouteHandler, UrlSource {
|
|||
* }|array{error: array{name: string, text: string}}
|
||||
*/
|
||||
#[PatternRoute('DELETE', '/comments/posts/([0-9]+)/vote')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
public function deletePostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array {
|
||||
if(!$this->getGlobalPerms()->check(Perm::G_COMMENTS_VOTE))
|
||||
return self::error($response, 403, 'comments:vote-not-allowed', 'You are not allowed to vote on comments.');
|
||||
|
|
129
src/CsrfContext.php
Normal file
129
src/CsrfContext.php
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
namespace Misuzu;
|
||||
|
||||
use Index\CsrfToken;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\Content\FormContent;
|
||||
use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Preprocessor;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
|
||||
class CsrfContext extends AbstractExtension implements RouteHandler {
|
||||
use RouteHandlerCommon;
|
||||
|
||||
private ?CsrfToken $instance = null;
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
) {}
|
||||
|
||||
public function createInstance(string $identity): CsrfToken {
|
||||
return new CsrfToken(
|
||||
$this->config->getString('secret', 'soup'),
|
||||
$identity,
|
||||
);
|
||||
}
|
||||
|
||||
public function isAvailable(): bool {
|
||||
return $this->instance !== null;
|
||||
}
|
||||
|
||||
public function setIdentity(string $identity): void {
|
||||
$this->instance = $this->createInstance($identity);
|
||||
}
|
||||
|
||||
public function createToken(): string {
|
||||
return $this->instance?->createToken() ?? '';
|
||||
}
|
||||
|
||||
public function verifyToken(string $token, int $tolerance = -1): bool {
|
||||
return $this->instance?->verifyToken($token, $tolerance) ?? false;
|
||||
}
|
||||
|
||||
public function verifyLegacy(int $tolerance = -1): bool {
|
||||
if(!$this->isAvailable())
|
||||
return false;
|
||||
|
||||
$token = isset($_POST['_csrf']) && is_string($_POST['_csrf']) ? $_POST['_csrf'] : '';
|
||||
if(empty($token))
|
||||
$token = isset($_GET['csrf']) && is_string($_GET['csrf']) ? $_GET['csrf'] : '';
|
||||
|
||||
return $this->verifyToken($token, $tolerance);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getFunctions() {
|
||||
return [
|
||||
new TwigFunction('csrf_available', $this->isAvailable(...)),
|
||||
new TwigFunction('csrf_token', $this->createToken(...)),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('csrf:header')]
|
||||
public function preVerifyHeader(
|
||||
HandlerContext $context,
|
||||
string $type = '',
|
||||
string $argName = 'csrf',
|
||||
) {
|
||||
if($type === 'arg' && isset($context->args[$argName]) && $context->args[$argName] === true)
|
||||
return;
|
||||
|
||||
$result = $this->isAvailable()
|
||||
&& $context->request->hasHeader('X-CSRF-Token')
|
||||
&& $this->verifyToken($context->request->getHeaderLine('X-CSRF-Token'));
|
||||
|
||||
if($result) {
|
||||
$context->response->setHeader('X-CSRF-Token', $this->createToken());
|
||||
if($type === 'arg')
|
||||
$context->setArgument($argName, true);
|
||||
} else {
|
||||
if($type === 'arg')
|
||||
$context->setArgument($argName, false);
|
||||
elseif($type === 'json') {
|
||||
$context->response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'csrf',
|
||||
'text' => 'Request could not be verified. Please try again.',
|
||||
],
|
||||
];
|
||||
} else return 403;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[Preprocessor('csrf:form')]
|
||||
public function preVerifyForm(
|
||||
HandlerContext $context,
|
||||
?FormContent $content,
|
||||
string $formParam = '_csrf',
|
||||
string $type = '',
|
||||
string $argName = 'csrf',
|
||||
) {
|
||||
if($type === 'arg' && isset($context->args[$argName]) && $context->args[$argName] === true)
|
||||
return;
|
||||
|
||||
$result = $this->isAvailable()
|
||||
&& $content?->hasParam($formParam) === true
|
||||
&& $this->verifyToken((string)$content->getParam($formParam));
|
||||
|
||||
if($result) {
|
||||
if($type === 'arg')
|
||||
$context->setArgument($argName, true);
|
||||
} else {
|
||||
if($type === 'arg') {
|
||||
$context->setArgument($argName, false);
|
||||
} elseif($type === 'json') {
|
||||
$context->response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'csrf',
|
||||
'text' => 'Request could not be verified. Please try again.',
|
||||
],
|
||||
];
|
||||
} else return 403;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,9 +5,10 @@ use stdClass;
|
|||
use RuntimeException;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Pagination,Perm,Template};
|
||||
use Misuzu\{Pagination,Perm,Template};
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Users\UsersContext;
|
||||
|
||||
|
@ -21,6 +22,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource {
|
|||
) {}
|
||||
|
||||
#[ExactRoute('GET', '/forum')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('forum-index', '/forum')]
|
||||
#[UrlFormat('forum-category-root', '/forum', fragment: '<forum>')]
|
||||
public function getIndex(): string {
|
||||
|
@ -172,6 +174,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('GET', '/forum/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('forum-category', '/forum/<forum>', ['page' => '<page>'])]
|
||||
public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $catId): mixed {
|
||||
try {
|
||||
|
@ -330,15 +333,10 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[ExactRoute('POST', '/forum/mark-as-read')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[UrlFormat('forum-mark-as-read', '/forum/mark-as-read', ['cat' => '<category>', 'rec' => '<recursive>'])]
|
||||
public function postMarkAsRead(HttpResponseBuilder $response, HttpRequest $request): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
$catId = (string)$request->getFilteredParam('cat', FILTER_SANITIZE_NUMBER_INT);
|
||||
$recursive = !empty($request->getParam('rec'));
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ namespace Misuzu\Forum;
|
|||
use RuntimeException;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\PatternRoute;
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Perm};
|
||||
use Misuzu\Perm;
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Logs\LogsContext;
|
||||
use Misuzu\Users\UsersContext;
|
||||
|
||||
class ForumPostsRoutes implements RouteHandler, UrlSource {
|
||||
use RouteHandlerCommon, UrlSourceCommon;
|
||||
|
@ -17,12 +17,12 @@ class ForumPostsRoutes implements RouteHandler, UrlSource {
|
|||
public function __construct(
|
||||
private UrlRegistry $urls,
|
||||
private ForumContext $forumCtx,
|
||||
private UsersContext $usersCtx,
|
||||
private LogsContext $logsCtx,
|
||||
private AuthInfo $authInfo,
|
||||
) {}
|
||||
|
||||
#[PatternRoute('GET', '/forum/posts/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('forum-post', '/forum/posts/<post>')]
|
||||
public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed {
|
||||
try {
|
||||
|
@ -56,25 +56,11 @@ class ForumPostsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('DELETE', '/forum/posts/([0-9]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-post-delete', '/forum/posts/<post>')]
|
||||
public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$post = $this->forumCtx->posts->getPost(
|
||||
postId: $postId,
|
||||
|
@ -185,25 +171,11 @@ class ForumPostsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/posts/([0-9]+)/nuke')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-post-nuke', '/forum/posts/<post>/nuke')]
|
||||
public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$post = $this->forumCtx->posts->getPost(
|
||||
postId: $postId,
|
||||
|
@ -259,25 +231,11 @@ class ForumPostsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/posts/([0-9]+)/restore')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-post-restore', '/forum/posts/<post>/restore')]
|
||||
public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$post = $this->forumCtx->posts->getPost(
|
||||
postId: $postId,
|
||||
|
|
|
@ -6,9 +6,10 @@ use RuntimeException;
|
|||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Content\FormContent;
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\PatternRoute;
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Pagination,Perm,Template};
|
||||
use Misuzu\{Pagination,Perm,Template};
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Logs\LogsContext;
|
||||
use Misuzu\Users\UsersContext;
|
||||
|
@ -24,6 +25,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
) {}
|
||||
|
||||
#[PatternRoute('GET', '/forum/topics/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('forum-topic', '/forum/topics/<topic>', ['page' => '<page>'], '<topic_fragment>')]
|
||||
public function getTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
$isNuked = $deleted = $canDeleteAny = false;
|
||||
|
@ -155,25 +157,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('DELETE', '/forum/topics/([0-9]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-delete', '/forum/topics/<topic>')]
|
||||
public function deleteTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
@ -269,25 +257,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/topics/([0-9]+)/restore')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-restore', '/forum/topics/<topic>/restore')]
|
||||
public function postTopicRestore(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
@ -343,25 +317,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/topics/([0-9]+)/nuke')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-nuke', '/forum/topics/<topic>/nuke')]
|
||||
public function postTopicNuke(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
@ -417,25 +377,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/topics/([0-9]+)/bump')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-bump', '/forum/topics/<topic>/bump')]
|
||||
public function postTopicBump(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
@ -491,25 +437,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/topics/([0-9]+)/lock')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-lock', '/forum/topics/<topic>/lock')]
|
||||
public function postTopicLock(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
@ -575,25 +507,11 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('POST', '/forum/topics/([0-9]+)/unlock')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('authz:banned', type: 'json')]
|
||||
#[UrlFormat('forum-topic-unlock', '/forum/topics/<topic>/unlock')]
|
||||
public function postTopicUnlock(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed {
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
return 403;
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
|
||||
if($this->usersCtx->hasActiveBan($this->authInfo->userInfo)) {
|
||||
$response->statusCode = 403;
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'user:banned',
|
||||
'text' => "You aren't allowed to do that while banned.",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$topic = $this->forumCtx->topics->getTopic(
|
||||
topicId: $topicId,
|
||||
|
|
|
@ -8,6 +8,7 @@ use Index\Colour\Colour;
|
|||
use Index\Db\{DbConnection,DbTools};
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\ExactRoute;
|
||||
use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{Pagination,SiteInfo,Template};
|
||||
|
@ -200,6 +201,7 @@ class HomeRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[ExactRoute('GET', '/')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('index', '/')]
|
||||
public function getIndex(): string {
|
||||
return $this->authInfo->loggedIn ? $this->getHome() : $this->getLanding();
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Misuzu\Info;
|
|||
use Index\Index;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{Misuzu,Template};
|
||||
|
@ -22,12 +23,14 @@ class InfoRoutes implements RouteHandler, UrlSource {
|
|||
];
|
||||
|
||||
#[ExactRoute('GET', '/info')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('info-index', '/info')]
|
||||
public function getIndex(): string {
|
||||
return Template::renderRaw('info.index');
|
||||
}
|
||||
|
||||
#[PatternRoute('GET', '/info/([A-Za-z0-9_]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('info', '/info/<title>')]
|
||||
#[UrlFormat('info-doc', '/info/<title>')]
|
||||
public function getDocsPage(HttpResponseBuilder $response, HttpRequest $request, string $name): string {
|
||||
|
@ -75,6 +78,7 @@ class InfoRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('GET', '/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('info-project-doc', '/info/<project>/<title>')]
|
||||
public function getProjectPage(HttpResponseBuilder $response, HttpRequest $request, string $project, string $name): int|string {
|
||||
if(!array_key_exists($project, self::PROJECT_PATHS))
|
||||
|
|
|
@ -10,11 +10,10 @@ use Index\Colour\Colour;
|
|||
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Content\FormContent;
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Filters\PrefixFilter;
|
||||
use Index\Http\Routing\Processors\{After,Before};
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Misuzu,Pagination,Perm,Template};
|
||||
use Misuzu\{Misuzu,Pagination,Perm,Template};
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Parsers\TextFormat;
|
||||
use Misuzu\Perms\PermissionsData;
|
||||
|
@ -36,39 +35,13 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
private AuthInfo $authInfo,
|
||||
private MessagesContext $msgsCtx,
|
||||
private UsersContext $usersCtx,
|
||||
private PermissionsData $perms
|
||||
private PermissionsData $perms,
|
||||
) {}
|
||||
|
||||
private bool $canSendMessages;
|
||||
|
||||
/** @return void|int|array{error: array{name: string, text: string}} */
|
||||
#[PrefixFilter('/messages')]
|
||||
public function checkAccess(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
// should probably be a permission or something too
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return 401;
|
||||
|
||||
// do not allow access to PMs when impersonating in production mode
|
||||
if(!Misuzu::debug() && $this->authInfo->impersonating)
|
||||
return 403;
|
||||
|
||||
$globalPerms = $this->authInfo->getPerms('global');
|
||||
if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
|
||||
return 403;
|
||||
|
||||
$this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND)
|
||||
&& !$this->usersCtx->hasActiveBan($this->authInfo->userInfo);
|
||||
|
||||
if($request->method === 'POST') {
|
||||
if(!CSRF::validate($request->getHeaderLine('x-csrf-token')))
|
||||
return [
|
||||
'error' => [
|
||||
'name' => 'msgs:verify',
|
||||
'text' => 'Request verification failed! Refresh the page and try again.',
|
||||
],
|
||||
];
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
private bool $canSendMessages {
|
||||
get {
|
||||
return $this->authInfo->getPerms('global')->check(Perm::G_MESSAGES_SEND)
|
||||
&& !$this->usersCtx->hasActiveBan($this->authInfo->userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +67,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[ExactRoute('GET', '/messages')]
|
||||
#[Before('authz:cookie', required: true)]
|
||||
#[Before('authz:private')]
|
||||
#[Before('authz:perm', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[UrlFormat('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
|
||||
public function getIndex(HttpRequest $request, string $folderName = ''): int|string {
|
||||
$folderName = (string)$request->getParam('folder');
|
||||
|
@ -148,6 +124,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return array{unread: int} */
|
||||
#[ExactRoute('GET', '/messages/stats')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[UrlFormat('messages-stats', '/messages/stats')]
|
||||
public function getStats(): array {
|
||||
$selfInfo = $this->authInfo->userInfo;
|
||||
|
@ -173,6 +152,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
* }
|
||||
*/
|
||||
#[ExactRoute('POST', '/messages/recipient')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
#[UrlFormat('messages-recipient', '/messages/recipient')]
|
||||
public function postRecipient(FormContent $content): int|array {
|
||||
|
@ -211,6 +194,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[ExactRoute('GET', '/messages/compose')]
|
||||
#[Before('authz:cookie', required: true)]
|
||||
#[Before('authz:private')]
|
||||
#[Before('authz:perm', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
|
||||
public function getEditor(HttpRequest $request): int|string {
|
||||
if(!$this->canSendMessages)
|
||||
|
@ -222,6 +208,9 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('GET', '/messages/([A-Za-z0-9]+)')]
|
||||
#[Before('authz:cookie', required: true)]
|
||||
#[Before('authz:private')]
|
||||
#[Before('authz:perm', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[UrlFormat('messages-view', '/messages/<message>')]
|
||||
public function getView(string $messageId): int|string {
|
||||
if(strlen($messageId) !== 8)
|
||||
|
@ -355,6 +344,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */
|
||||
#[ExactRoute('POST', '/messages/create')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:multipart')]
|
||||
#[UrlFormat('messages-create', '/messages/create')]
|
||||
public function postCreate(FormContent $content): int|array {
|
||||
|
@ -457,6 +450,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */
|
||||
#[PatternRoute('PATCH', '/messages/([A-Za-z0-9]+)')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:multipart')]
|
||||
#[UrlFormat('messages-update', '/messages/<message>')]
|
||||
public function patchUpdate(FormContent $content, string $messageId): int|array {
|
||||
|
@ -547,6 +544,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
|
||||
#[ExactRoute('POST', '/messages/mark')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
#[UrlFormat('messages-mark', '/messages/mark')]
|
||||
public function postMark(FormContent $content): int|array {
|
||||
|
@ -576,6 +577,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
|
||||
#[ExactRoute('POST', '/messages/delete')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
#[UrlFormat('messages-delete', '/messages/delete')]
|
||||
public function postDelete(FormContent $content): int|array {
|
||||
|
@ -600,6 +605,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
|
||||
#[ExactRoute('POST', '/messages/restore')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
#[UrlFormat('messages-restore', '/messages/restore')]
|
||||
public function postRestore(FormContent $content) {
|
||||
|
@ -624,6 +633,10 @@ class MessagesRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return int|array{error: array{name: string, text: string}}|scalar[] */
|
||||
#[ExactRoute('POST', '/messages/nuke')]
|
||||
#[Before('authz:cookie', type: 'json', required: true)]
|
||||
#[Before('authz:private', type: 'json')]
|
||||
#[Before('authz:perm', type: 'json', category: 'global', perm: Perm::G_MESSAGES_VIEW)]
|
||||
#[Before('csrf:header', type: 'json')]
|
||||
#[Before('input:urlencoded')]
|
||||
#[UrlFormat('messages-nuke', '/messages/nuke')]
|
||||
public function postNuke(FormContent $content) {
|
||||
|
|
|
@ -7,11 +7,13 @@ use Index\Config\Db\DbConfig;
|
|||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
|
||||
use Index\Http\HttpRequest;
|
||||
use Index\Http\Routing\RouteHandler;
|
||||
use Index\Snowflake\{BinarySnowflake,RandomSnowflake,SnowflakeGenerator};
|
||||
use Index\Templating\TplEnvironment;
|
||||
use Index\Urls\UrlRegistry;
|
||||
use Misuzu\Routing\RoutingContext;
|
||||
use Misuzu\Users\UserInfo;
|
||||
use Twig\Extension\ExtensionInterface;
|
||||
|
||||
class MisuzuContext {
|
||||
public private(set) Dependencies $deps;
|
||||
|
@ -28,6 +30,7 @@ class MisuzuContext {
|
|||
public private(set) Counters\CountersData $counters;
|
||||
public private(set) News\NewsData $news;
|
||||
|
||||
public private(set) CsrfContext $csrfCtx;
|
||||
public private(set) DatabaseContext $dbCtx;
|
||||
public private(set) Apps\AppsContext $appsCtx;
|
||||
public private(set) Auth\AuthContext $authCtx;
|
||||
|
@ -95,6 +98,10 @@ class MisuzuContext {
|
|||
));
|
||||
$this->deps->register($this->coloursCtx = $this->deps->constructLazy(Colours\ColoursContext::class));
|
||||
$this->deps->register($this->commentsCtx = $this->deps->constructLazy(Comments\CommentsContext::class));
|
||||
$this->deps->register($this->csrfCtx = $this->deps->constructLazy(
|
||||
CsrfContext::class,
|
||||
config: $this->config->scopeTo('csrf'),
|
||||
));
|
||||
$this->deps->register($this->emotesCtx = $this->deps->constructLazy(Emoticons\EmotesContext::class));
|
||||
$this->deps->register($this->forumCtx = $this->deps->constructLazy(
|
||||
Forum\ForumContext::class,
|
||||
|
@ -163,6 +170,11 @@ class MisuzuContext {
|
|||
cache: $isDebug || !$cache ? null : ['Misuzu', GitInfo::hash(true)],
|
||||
debug: $isDebug
|
||||
);
|
||||
|
||||
$exts = $this->deps->all(ExtensionInterface::class);
|
||||
foreach($exts as $ext)
|
||||
$this->templating->addExtension($ext);
|
||||
|
||||
$this->templating->addExtension($this->deps->construct(TemplatingExtension::class));
|
||||
$this->templating->addGlobal('globals', $globals);
|
||||
|
||||
|
@ -175,7 +187,10 @@ class MisuzuContext {
|
|||
|
||||
$routingCtx = $this->deps->construct(RoutingContext::class);
|
||||
$this->deps->register($this->urls = $routingCtx->urls);
|
||||
$routingCtx->register($this->logsCtx);
|
||||
|
||||
$handlers = $this->deps->all(RouteHandler::class);
|
||||
foreach($handlers as $handler)
|
||||
$routingCtx->register($handler);
|
||||
|
||||
if(in_array('main', $roles))
|
||||
$this->registerMainRoutes($routingCtx);
|
||||
|
@ -200,7 +215,7 @@ class MisuzuContext {
|
|||
$routingCtx->register($this->deps->constructLazy(WebFinger\WebFingerRoutes::class));
|
||||
|
||||
$routingCtx->register($this->deps->constructLazy(
|
||||
Auth\AuthApiRoutes::class,
|
||||
Auth\AuthProcessors::class,
|
||||
impersonateConfig: $this->config->scopeTo('impersonate')
|
||||
));
|
||||
$routingCtx->register($this->deps->constructLazy(Colours\ColoursApiRoutes::class));
|
||||
|
|
|
@ -5,6 +5,7 @@ use RuntimeException;
|
|||
use Index\Colour\Colour;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Syndication\FeedBuilder;
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
|
@ -100,6 +101,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[ExactRoute('GET', '/news')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('news-index', '/news', ['p' => '<page>'])]
|
||||
public function getIndex(HttpResponseBuilder $response, HttpRequest $request): int|string {
|
||||
$categories = $this->news->getCategories(hidden: false);
|
||||
|
@ -143,6 +145,7 @@ class NewsRoutes implements RouteHandler, UrlSource {
|
|||
}
|
||||
|
||||
#[PatternRoute('GET', '/news/post/([0-9]+)')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('news-post', '/news/post/<post>')]
|
||||
#[UrlFormat('news-post-comments', '/news/post/<post>', fragment: 'comments')]
|
||||
public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $postId): int|string {
|
||||
|
|
|
@ -193,51 +193,17 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
|
|||
*/
|
||||
#[PatternRoute('POST', '/oauth2/request-authori[sz]e')]
|
||||
#[Before('input:urlencoded', required: false)]
|
||||
#[Before('authz:basic')]
|
||||
#[UrlFormat('oauth2-request-authorise', '/oauth2/request-authorize')]
|
||||
public function postRequestAuthorise(HttpResponseBuilder $response, HttpRequest $request, ?FormContent $content): array {
|
||||
$response->setHeader('Cache-Control', 'no-store');
|
||||
|
||||
if($content === null)
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'Your request must use content type application/x-www-form-urlencoded.',
|
||||
]);
|
||||
|
||||
$authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization'));
|
||||
if(strcasecmp($authzHeader[0], 'Basic') === 0) {
|
||||
$authzHeader = explode(':', base64_decode($authzHeader[1] ?? ''));
|
||||
$clientId = $authzHeader[0];
|
||||
$clientSecret = $authzHeader[1] ?? '';
|
||||
} elseif($authzHeader[0] !== '') {
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'You must use the Basic method for Authorization parameters.',
|
||||
], authzHeader: true);
|
||||
} else {
|
||||
$clientId = (string)$content->getParam('client_id');
|
||||
$clientSecret = '';
|
||||
}
|
||||
|
||||
try {
|
||||
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(clientId: $clientId, deleted: false);
|
||||
} catch(RuntimeException $ex) {
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'No application has been registered with this client ID.',
|
||||
], authzHeader: $authzHeader[0] !== '');
|
||||
}
|
||||
|
||||
if($clientSecret !== '') {
|
||||
// TODO: rate limiting
|
||||
if(!$appInfo->verifyClientSecret($clientSecret))
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'Provided client secret is not correct for this application.',
|
||||
], authzHeader: true);
|
||||
}
|
||||
|
||||
return self::filter($response, $request, $this->oauth2Ctx->createDeviceAuthorisationRequest(
|
||||
$appInfo,
|
||||
$this->authInfo->appInfo,
|
||||
$content->hasParam('scope') ? (string)$content->getParam('scope') : null
|
||||
));
|
||||
}
|
||||
|
@ -254,81 +220,40 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
|
|||
#[AccessControl(allowHeaders: ['Authorization'], exposeHeaders: ['WWW-Authenticate'])]
|
||||
#[ExactRoute('POST', '/oauth2/token')]
|
||||
#[Before('input:urlencoded', required: false)]
|
||||
#[Before('authz:basic', bodySecret: true)]
|
||||
#[UrlFormat('oauth2-token', '/oauth2/token')]
|
||||
public function postToken(HttpResponseBuilder $response, HttpRequest $request, ?FormContent $content): array {
|
||||
$response->setHeader('Cache-Control', 'no-store');
|
||||
|
||||
if($content === null)
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'Your request must use content type application/x-www-form-urlencoded.',
|
||||
]);
|
||||
|
||||
// authz header should be the preferred method
|
||||
$authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization'));
|
||||
if(strcasecmp($authzHeader[0], 'Basic') === 0) {
|
||||
$authzHeader = explode(':', base64_decode($authzHeader[1] ?? ''));
|
||||
$clientId = $authzHeader[0];
|
||||
$clientSecret = $authzHeader[1] ?? '';
|
||||
} elseif($authzHeader[0] !== '') {
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'You must either use the Basic method for Authorization or use the client_id and client_secret parameters.',
|
||||
], authzHeader: true);
|
||||
} else {
|
||||
$clientId = (string)$content->getParam('client_id');
|
||||
$clientSecret = (string)$content->getParam('client_secret');
|
||||
}
|
||||
|
||||
try {
|
||||
$appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo(clientId: $clientId, deleted: false);
|
||||
} catch(RuntimeException $ex) {
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'No application has been registered with this client ID.',
|
||||
], authzHeader: $authzHeader[0] !== '');
|
||||
}
|
||||
|
||||
$isAuthed = false;
|
||||
if($clientSecret !== '') {
|
||||
// TODO: rate limiting
|
||||
$isAuthed = $appInfo->verifyClientSecret($clientSecret);
|
||||
if(!$isAuthed)
|
||||
return self::filter($response, $request, [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'Provided client secret is not correct for this application.',
|
||||
], authzHeader: $authzHeader[0] !== '');
|
||||
}
|
||||
|
||||
$type = (string)$content->getParam('grant_type');
|
||||
|
||||
if($type === 'authorization_code')
|
||||
return self::filter($response, $request, $this->oauth2Ctx->redeemAuthorisationCode(
|
||||
$appInfo,
|
||||
$isAuthed,
|
||||
$this->authInfo->appInfo,
|
||||
(string)$content->getParam('code'),
|
||||
(string)$content->getParam('code_verifier')
|
||||
));
|
||||
|
||||
if($type === 'refresh_token')
|
||||
return self::filter($response, $request, $this->oauth2Ctx->redeemRefreshToken(
|
||||
$appInfo,
|
||||
$isAuthed,
|
||||
$this->authInfo->appInfo,
|
||||
(string)$content->getParam('refresh_token'),
|
||||
$content->hasParam('scope') ? (string)$content->getParam('scope') : null
|
||||
));
|
||||
|
||||
if($type === 'client_credentials')
|
||||
return self::filter($response, $request, $this->oauth2Ctx->redeemClientCredentials(
|
||||
$appInfo,
|
||||
$isAuthed,
|
||||
$this->authInfo->appInfo,
|
||||
$content->hasParam('scope') ? (string)$content->getParam('scope') : null
|
||||
));
|
||||
|
||||
if($type === 'urn:ietf:params:oauth:grant-type:device_code' || $type === 'device_code')
|
||||
return self::filter($response, $request, $this->oauth2Ctx->redeemDeviceCode(
|
||||
$appInfo,
|
||||
$isAuthed,
|
||||
$this->authInfo->appInfo,
|
||||
(string)$content->getParam('device_code')
|
||||
));
|
||||
|
||||
|
@ -362,17 +287,9 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource {
|
|||
*/
|
||||
#[AccessControl(allowHeaders: ['Authorization'], exposeHeaders: ['WWW-Authenticate'])]
|
||||
#[ExactRoute('GET', '/oauth2/userinfo')]
|
||||
#[Before('authz:bearer')]
|
||||
#[UrlFormat('oauth2-openid-userinfo', '/oauth2/userinfo')]
|
||||
public function getUserInfo(HttpResponseBuilder $response, HttpRequest $request): array {
|
||||
if(!$this->authInfo->loggedInBearer) {
|
||||
$response->statusCode = 401;
|
||||
$response->setHeader('WWW-Authenticate', 'Bearer error="invalid_token", error_description="Bearer authentication must be used."');
|
||||
return [
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'Bearer authentication must be used.',
|
||||
];
|
||||
}
|
||||
|
||||
if(!$this->authInfo->hasScope('openid')) {
|
||||
$response->statusCode = 403;
|
||||
$response->setHeader('WWW-Authenticate', 'Bearer error="insufficient_scope", error_description="openid scope is required for this endpoint."');
|
||||
|
|
|
@ -241,7 +241,7 @@ class OAuth2Context {
|
|||
* refresh_token?: string,
|
||||
* }|array{ error: string, error_description: string }
|
||||
*/
|
||||
public function redeemAuthorisationCode(AppInfo $appInfo, bool $isAuthed, string $code, string $codeVerifier): array {
|
||||
public function redeemAuthorisationCode(AppInfo $appInfo, string $code, string $codeVerifier): array {
|
||||
try {
|
||||
$authsInfo = $this->authorisations->getAuthorisationInfo(
|
||||
appInfo: $appInfo,
|
||||
|
@ -294,7 +294,7 @@ class OAuth2Context {
|
|||
* refresh_token?: string,
|
||||
* }|array{ error: string, error_description: string }
|
||||
*/
|
||||
public function redeemRefreshToken(AppInfo $appInfo, bool $isAuthed, string $refreshToken, ?string $scope = null): array {
|
||||
public function redeemRefreshToken(AppInfo $appInfo, string $refreshToken, ?string $scope = null): array {
|
||||
try {
|
||||
$refreshInfo = $this->tokens->getRefreshInfo($refreshToken, OAuth2RefreshInfoGetField::Token);
|
||||
} catch(RuntimeException $ex) {
|
||||
|
@ -355,17 +355,12 @@ class OAuth2Context {
|
|||
* refresh_token?: string,
|
||||
* }|array{ error: string, error_description: string }
|
||||
*/
|
||||
public function redeemClientCredentials(AppInfo $appInfo, bool $isAuthed, ?string $scope = null): array {
|
||||
public function redeemClientCredentials(AppInfo $appInfo, ?string $scope = null): array {
|
||||
if(!$appInfo->confidential)
|
||||
return [
|
||||
'error' => 'unauthorized_client',
|
||||
'error_description' => 'This application is not allowed to use this grant type.',
|
||||
];
|
||||
if(!$isAuthed)
|
||||
return [
|
||||
'error' => 'invalid_client',
|
||||
'error_description' => 'Application must authenticate with client secret in order to use this grant type.',
|
||||
];
|
||||
|
||||
$requestedScope = $scope;
|
||||
$scope = $this->checkAndBuildScopeString($appInfo, $requestedScope, true);
|
||||
|
@ -392,7 +387,7 @@ class OAuth2Context {
|
|||
* refresh_token?: string,
|
||||
* }|array{ error: string, error_description: string }
|
||||
*/
|
||||
public function redeemDeviceCode(AppInfo $appInfo, bool $isAuthed, string $deviceCode): array {
|
||||
public function redeemDeviceCode(AppInfo $appInfo, string $deviceCode): array {
|
||||
try {
|
||||
$deviceInfo = $this->devices->getDeviceInfo(
|
||||
appInfo: $appInfo,
|
||||
|
|
|
@ -10,7 +10,7 @@ use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
|||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{CSRF,Template};
|
||||
use Misuzu\{CsrfContext,Template};
|
||||
use Misuzu\Auth\AuthInfo;
|
||||
use Misuzu\Users\UsersContext;
|
||||
|
||||
|
@ -24,6 +24,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
private OAuth2Context $oauth2Ctx,
|
||||
private UsersContext $usersCtx,
|
||||
private UrlRegistry $urls,
|
||||
private CsrfContext $csrfCtx,
|
||||
private AuthInfo $authInfo,
|
||||
) {}
|
||||
|
||||
|
@ -48,10 +49,10 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
public function postAuthorise(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array {
|
||||
// TODO: RATE LIMITING
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
|
||||
return ['error' => 'csrf'];
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
$response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());
|
||||
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return ['error' => 'auth'];
|
||||
|
@ -174,10 +175,10 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
public function getResolveAuthorise(HttpResponseBuilder $response, HttpRequest $request): array {
|
||||
// TODO: RATE LIMITING
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
|
||||
return ['error' => 'csrf'];
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
$response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());
|
||||
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return ['error' => 'auth'];
|
||||
|
@ -236,7 +237,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
'name' => $this->authInfo->realUserInfo->name,
|
||||
'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->realUserInfo),
|
||||
'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->realUserInfo->id]),
|
||||
'revert_uri' => $this->urls->format('auth-revert', ['csrf' => CSRF::token()]),
|
||||
'revert_uri' => $this->urls->format('auth-revert', ['csrf' => $this->csrfCtx->createToken()]),
|
||||
'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->realUserInfo->id, 'res' => 60]),
|
||||
];
|
||||
|
||||
|
@ -263,10 +264,10 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
public function postVerify(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array {
|
||||
// TODO: RATE LIMITING
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
|
||||
return ['error' => 'csrf'];
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
$response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());
|
||||
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return ['error' => 'auth'];
|
||||
|
@ -360,10 +361,10 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
public function getResolveVerify(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
// TODO: RATE LIMITING
|
||||
|
||||
if(!CSRF::validate($request->getHeaderLine('X-CSRF-token')))
|
||||
if(!$this->csrfCtx->verifyToken($request->getHeaderLine('X-CSRF-token')))
|
||||
return ['error' => 'csrf'];
|
||||
|
||||
$response->setHeader('X-CSRF-Token', CSRF::token());
|
||||
$response->setHeader('X-CSRF-Token', $this->csrfCtx->createToken());
|
||||
|
||||
if(!$this->authInfo->loggedIn)
|
||||
return ['error' => 'auth'];
|
||||
|
@ -424,7 +425,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource {
|
|||
'name' => $this->authInfo->realUserInfo->name,
|
||||
'colour' => (string)$this->usersCtx->getUserColour($this->authInfo->realUserInfo),
|
||||
'profile_uri' => $this->urls->format('user-profile', ['user' => $this->authInfo->realUserInfo->id]),
|
||||
'revert_uri' => $this->urls->format('auth-revert', ['csrf' => CSRF::token()]),
|
||||
'revert_uri' => $this->urls->format('auth-revert', ['csrf' => $this->csrfCtx->createToken()]),
|
||||
'avatar_uri' => $this->urls->format('user-avatar', ['user' => $this->authInfo->realUserInfo->id, 'res' => 60]),
|
||||
];
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ final class SharpChatRoutes implements RouteHandler {
|
|||
}
|
||||
|
||||
#[ExactRoute('GET', '/_sockchat/login')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getLogin(HttpResponseBuilder $response, HttpRequest $request): void {
|
||||
if(!$this->authInfo->loggedIn) {
|
||||
$response->redirect($this->urls->format('auth-login'));
|
||||
|
@ -87,6 +88,7 @@ final class SharpChatRoutes implements RouteHandler {
|
|||
/** @return array{ok: false, err: string}|array{ok: true, usr: int, tkn: string} */
|
||||
#[AccessControl(credentials: true)]
|
||||
#[ExactRoute('GET', '/_sockchat/token')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getToken(HttpRequest $request): array {
|
||||
$tokenInfo = $this->authInfo->tokenInfo;
|
||||
if(!$tokenInfo->hasSessionToken)
|
||||
|
|
|
@ -133,8 +133,11 @@ class UploadsLegacyRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return array<string, ?scalar> */
|
||||
#[AccessControl(credentials: true, allowHeaders: ['Authorization'], exposeHeaders: ['X-EEPROM-Max-Size'])]
|
||||
#[Before('input:multipart')]
|
||||
#[ExactRoute('POST', '/uploads')]
|
||||
#[Before('authz:cookie', required: false)]
|
||||
#[Before('authz:bearer', required: false)]
|
||||
#[Before('authz:misuzu', required: false)]
|
||||
#[Before('input:multipart')]
|
||||
public function postUpload(HttpResponseBuilder $response, HttpRequest $request, MultipartFormContent $content) {
|
||||
if(!$this->authInfo->loggedIn) {
|
||||
$response->statusCode = 401;
|
||||
|
@ -302,6 +305,9 @@ class UploadsLegacyRoutes implements RouteHandler, UrlSource {
|
|||
/** @return int|array{error: string, english: string} */
|
||||
#[AccessControl(credentials: true, allowHeaders: ['Authorization'])]
|
||||
#[PatternRoute('DELETE', '/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
|
||||
#[Before('authz:cookie', required: false)]
|
||||
#[Before('authz:bearer', required: false)]
|
||||
#[Before('authz:misuzu', required: false)]
|
||||
public function deleteUpload(HttpResponseBuilder $response, HttpRequest $request, string $uploadId) {
|
||||
if(!$this->authInfo->loggedIn) {
|
||||
$response->statusCode = 401;
|
||||
|
|
|
@ -26,8 +26,6 @@ final class TemplatingExtension extends AbstractExtension {
|
|||
return [
|
||||
new TwigFunction('asset', $this->assetInfo->getAssetUrl(...)),
|
||||
new TwigFunction('url', $this->ctx->urls->format(...)),
|
||||
new TwigFunction('csrf_available', CSRF::available(...)),
|
||||
new TwigFunction('csrf_token', CSRF::token(...)),
|
||||
new TwigFunction('git_commit_hash', GitInfo::hash(...)),
|
||||
new TwigFunction('git_tag', GitInfo::tag(...)),
|
||||
new TwigFunction('git_branch', GitInfo::branch(...)),
|
||||
|
@ -190,7 +188,7 @@ final class TemplatingExtension extends AbstractExtension {
|
|||
|
||||
$menu[] = [
|
||||
'title' => 'Log out',
|
||||
'url' => $this->ctx->urls->format('auth-logout', ['csrf' => CSRF::token()]),
|
||||
'url' => $this->ctx->urls->format('auth-logout', ['csrf' => $this->ctx->csrfCtx->createToken()]),
|
||||
'icon' => 'fas fa-sign-out-alt fa-fw',
|
||||
];
|
||||
} else {
|
||||
|
|
|
@ -5,6 +5,7 @@ use InvalidArgumentException;
|
|||
use RuntimeException;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
|
||||
use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon};
|
||||
use Misuzu\{Misuzu,Perm};
|
||||
|
@ -33,6 +34,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
|
|||
/** @return void */
|
||||
#[ExactRoute('GET', '/assets/avatar')]
|
||||
#[PatternRoute('GET', '/assets/avatar/([0-9]+)(?:\.[a-z]+)?')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('user-avatar', '/assets/avatar/<user>', ['res' => '<res>'])]
|
||||
public function getAvatar(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') {
|
||||
$assetInfo = new StaticUserImageAsset(Misuzu::PATH_PUBLIC . '/images/no-avatar.png', Misuzu::PATH_PUBLIC);
|
||||
|
@ -56,6 +58,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
|
|||
/** @return string|void */
|
||||
#[ExactRoute('GET', '/assets/profile-background')]
|
||||
#[PatternRoute('GET', '/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')]
|
||||
#[Before('authz:cookie')]
|
||||
#[UrlFormat('user-background', '/assets/profile-background/<user>')]
|
||||
public function getProfileBackground(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') {
|
||||
try {
|
||||
|
@ -80,6 +83,7 @@ class AssetsRoutes implements RouteHandler, UrlSource {
|
|||
|
||||
/** @return string|void */
|
||||
#[ExactRoute('GET', '/user-assets.php')]
|
||||
#[Before('authz:cookie')]
|
||||
public function getUserAssets(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
$userId = (string)$request->getFilteredParam('u', FILTER_SANITIZE_NUMBER_INT);
|
||||
$mode = (string)$request->getParam('m');
|
||||
|
|
|
@ -7,6 +7,7 @@ use Index\Colour\{Colour,ColourRgb};
|
|||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
|
||||
use Index\Http\Routing\AccessControl\AccessControl;
|
||||
use Index\Http\Routing\Processors\Before;
|
||||
use Index\Http\Routing\Routes\ExactRoute;
|
||||
use Index\Urls\UrlRegistry;
|
||||
use Misuzu\{FieldTransformer,SiteInfo};
|
||||
|
@ -126,6 +127,9 @@ final class UsersApiRoutes implements RouteHandler {
|
|||
/** @return int|mixed[] */
|
||||
#[AccessControl]
|
||||
#[ExactRoute('GET', '/api/v1/me')]
|
||||
#[Before('authz:cookie', required: false)]
|
||||
#[Before('authz:bearer', required: false)]
|
||||
#[Before('authz:misuzu', required: false)]
|
||||
public function getMe(HttpResponseBuilder $response, HttpRequest $request): int|array {
|
||||
$response->setHeader('Cache-Control', 'no-store');
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue