From 5ba8b30047c8d0d43db3443a6816882c8a9e3cae Mon Sep 17 00:00:00 2001 From: flashwave <me@flash.moe> Date: Mon, 24 Mar 2025 00:20:41 +0000 Subject: [PATCH] Updated to latest Index as well as some minor bug fixes. --- assets/misuzu.js/comments/api.js | 14 +- assets/misuzu.js/messages/messages.js | 2 +- assets/oauth2.js/authorise.js | 2 +- composer.json | 8 +- composer.lock | 135 ++++++++---- public-legacy/auth/login.php | 29 +-- public-legacy/auth/password.php | 30 +-- public-legacy/auth/register.php | 27 ++- public-legacy/auth/twofactor.php | 13 +- public-legacy/forum/leaderboard.php | 6 +- public-legacy/forum/posting.php | 24 +-- public-legacy/manage/changelog/change.php | 14 +- public-legacy/manage/changelog/tag.php | 6 +- public-legacy/manage/forum/index.php | 7 +- public-legacy/manage/forum/redirs.php | 8 +- public-legacy/manage/general/emoticon.php | 10 +- public-legacy/manage/general/emoticons.php | 6 +- .../manage/general/setting-delete.php | 2 +- public-legacy/manage/general/setting.php | 12 +- public-legacy/manage/news/category.php | 6 +- public-legacy/manage/news/post.php | 8 +- public-legacy/manage/users/ban.php | 16 +- public-legacy/manage/users/bans.php | 6 +- public-legacy/manage/users/note.php | 14 +- public-legacy/manage/users/notes.php | 6 +- public-legacy/manage/users/role.php | 28 ++- public-legacy/manage/users/user.php | 10 +- public-legacy/manage/users/warning.php | 8 +- public-legacy/manage/users/warnings.php | 6 +- public-legacy/members.php | 6 +- public-legacy/profile.php | 201 ++++++++---------- public-legacy/settings/sessions.php | 2 +- public/index.php | 62 +++++- src/Auth/AuthTokenCookie.php | 4 +- src/CSRF.php | 4 +- src/Changelog/ChangelogRoutes.php | 11 +- src/Comments/CommentsRoutes.php | 96 ++++----- src/DatabaseContext.php | 7 +- src/Forum/ForumCategoriesRoutes.php | 11 +- src/Forum/ForumPostsRoutes.php | 11 +- src/Forum/ForumTopicsRoutes.php | 18 +- src/Home/HomeRoutes.php | 15 +- src/Info/InfoRoutes.php | 9 +- src/LegacyRoutes.php | 90 ++++---- src/Messages/MessagesRoutes.php | 113 +++++----- src/MisuzuContext.php | 28 +-- src/News/NewsRoutes.php | 11 +- src/OAuth2/OAuth2ApiRoutes.php | 116 ++++------ src/OAuth2/OAuth2WebRoutes.php | 58 +++-- src/Pagination.php | 4 +- src/Perm.php | 20 +- src/Redirects/AliasRedirectsRoutes.php | 13 +- src/Redirects/IncrementalRedirectsRoutes.php | 25 +-- src/Redirects/LandingRedirectsRoutes.php | 5 +- src/Redirects/NamedRedirectsRoutes.php | 6 +- src/Redirects/SocialRedirectsRoutes.php | 13 +- src/Routing/BackedRoutingContext.php | 39 ---- src/Routing/RoutingContext.php | 33 ++- src/Routing/RoutingErrorHandler.php | 25 ++- src/Routing/ScopedRoutingContext.php | 27 --- src/Satori/SatoriRoutes.php | 33 +-- src/SharpChat/SharpChatRoutes.php | 145 +++++-------- src/Tools.php | 4 +- src/Users/Assets/AssetsRoutes.php | 19 +- src/WebFinger/WebFingerRoutes.php | 12 +- templates/auth/login.twig | 6 +- templates/auth/password_forgot.twig | 2 +- templates/auth/password_reset.twig | 10 +- templates/auth/register.twig | 10 +- templates/auth/twofactor.twig | 6 +- templates/forum/posting.twig | 18 +- templates/manage/users/ban.twig | 2 +- templates/manage/users/note.twig | 2 +- templates/manage/users/warning.twig | 2 +- templates/profile/_layout/header.twig | 2 +- templates/profile/index.twig | 10 +- tools/render-tpl | 8 +- 77 files changed, 900 insertions(+), 937 deletions(-) delete mode 100644 src/Routing/BackedRoutingContext.php delete mode 100644 src/Routing/ScopedRoutingContext.php diff --git a/assets/misuzu.js/comments/api.js b/assets/misuzu.js/comments/api.js index 5bb680d6..27c3908f 100644 --- a/assets/misuzu.js/comments/api.js +++ b/assets/misuzu.js/comments/api.js @@ -1,4 +1,12 @@ const MszCommentsApi = (() => { + const argsToFormData = args => { + const formData = new FormData; + for(const name in args) + formData.append(name, args[name]); + + return formData; + }; + return { getCategory: async name => { if(typeof name !== 'string' || name.trim() === '') @@ -62,7 +70,7 @@ const MszCommentsApi = (() => { const { status, body } = await $xhr.post( '/comments/posts', { csrf: true, type: 'json' }, - args + argsToFormData(args) ); if(status !== 201) throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' }); @@ -75,10 +83,10 @@ const MszCommentsApi = (() => { if(typeof args !== 'object' || args === null) throw new Error('args must be a non-null object'); - const { status, body } = await $xhr.post( + const { status, body } = await $xhr.patch( `/comments/posts/${post}`, { csrf: true, type: 'json' }, - args + argsToFormData(args) ); if(status !== 200) throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' }); diff --git a/assets/misuzu.js/messages/messages.js b/assets/misuzu.js/messages/messages.js index a58a7ce0..f03c72c7 100644 --- a/assets/misuzu.js/messages/messages.js +++ b/assets/misuzu.js/messages/messages.js @@ -52,7 +52,7 @@ const MszMessages = () => { formData.append('format', format); formData.append('draft', draft); - const { body } = await $xhr.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json', csrf: true }, formData); + const { body } = await $xhr.patch(`/messages/${encodeURIComponent(messageId)}`, { type: 'json', csrf: true }, formData); if(body.error !== undefined) throw body.error; diff --git a/assets/oauth2.js/authorise.js b/assets/oauth2.js/authorise.js index 219de4f0..490ff014 100644 --- a/assets/oauth2.js/authorise.js +++ b/assets/oauth2.js/authorise.js @@ -188,7 +188,7 @@ const MszOAuth2Authorise = async () => { params.scope = scope; try { - const { body } = await $xhr.post('/oauth2/authorise', { authed: true, csrf: true, type: 'json' }, params); + const { body } = await $xhr.post('/oauth2/authorize', { authed: true, csrf: true, type: 'json' }, params); if(!body) throw 'authorisation failed'; if(typeof body.error === 'string') diff --git a/composer.json b/composer.json index 264cd34e..b8128202 100644 --- a/composer.json +++ b/composer.json @@ -2,8 +2,8 @@ "require": { "php": ">=8.4", "ext-mbstring": "*", - "flashwave/index": "^0.2501", - "flashii/rpcii": "~4.0", + "flashwave/index": "^0.2503", + "flashii/rpcii": "~5.0", "erusev/parsedown": "~1.7", "chillerlan/php-qrcode": "~5.0", "symfony/mailer": "~7.2", @@ -11,9 +11,9 @@ "sentry/sdk": "~4.0", "nesbot/carbon": "~3.8", "vlucas/phpdotenv": "~5.6", - "filp/whoops": "~2.17", + "filp/whoops": "~2.18", "phpseclib/phpseclib": "~3.0", - "guzzlehttp/guzzle": "~7.0" + "guzzlehttp/guzzle": "~7.9" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index a9ec0a30..87e16339 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1045c8f605203fae19361d659fb64c54", + "content-hash": "baa15cf491eb4500360fa9c20fd42a4d", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -313,16 +313,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -368,7 +368,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -376,7 +376,7 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "erusev/parsedown", @@ -430,16 +430,16 @@ }, { "name": "filp/whoops", - "version": "2.17.0", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e" + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e", + "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", "shasum": "" }, "require": { @@ -489,7 +489,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.17.0" + "source": "https://github.com/filp/whoops/tree/2.18.0" }, "funding": [ { @@ -497,24 +497,26 @@ "type": "github" } ], - "time": "2025-01-25T12:00:00+00:00" + "time": "2025-03-15T12:00:00+00:00" }, { "name": "flashii/rpcii", - "version": "v4.0.0", + "version": "v5.0.1", "source": { "type": "git", "url": "https://patchii.net/flashii/rpcii-php.git", - "reference": "7849b2effd7ac878e7cf174eef7e27914799e60d" + "reference": "28c25e0a342173524f8894d03158663842e03252" }, "require": { "ext-msgpack": ">=2.2", - "flashwave/index": "^0.2501", - "php": ">=8.4" + "flashwave/index": "^0.2503", + "guzzlehttp/guzzle": "~7.0", + "php": ">=8.4", + "psr/http-client": "^1.0" }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5" + "phpunit/phpunit": "^12.0" }, "type": "library", "autoload": { @@ -536,25 +538,27 @@ ], "description": "HTTP RPC client/server library.", "homepage": "https://railgun.sh/rpcii", - "time": "2025-01-29T22:00:17+00:00" + "time": "2025-03-21T19:38:29+00:00" }, { "name": "flashwave/index", - "version": "v0.2501.221237", + "version": "v0.2503.230355", "source": { "type": "git", "url": "https://patchii.net/flash/index.git", - "reference": "fee9a65e3bca341be7401fe2e21b795630f15f2a" + "reference": "2372a113d26380176994f64ab99c42aaf2e9d98e" }, "require": { "ext-mbstring": "*", "php": ">=8.4", - "twig/html-extra": "^3.18", - "twig/twig": "^3.18" + "psr/http-message": "^2.0", + "psr/http-server-handler": "^1.0", + "twig/html-extra": "^3.20", + "twig/twig": "^3.20" }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).", @@ -591,7 +595,7 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2025-01-22T12:38:11+00:00" + "time": "2025-03-23T03:45:47+00:00" }, { "name": "graham-campbell/result-type", @@ -982,16 +986,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -1001,8 +1005,9 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", @@ -1035,9 +1040,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-11-18T16:19:46+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { "name": "matomo/device-detector", @@ -1882,6 +1887,62 @@ }, "time": "2023-04-04T09:54:51+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -3618,16 +3679,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "2.1.6", + "version": "2.1.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c" + "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", + "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", "shasum": "" }, "require": { @@ -3672,7 +3733,7 @@ "type": "github" } ], - "time": "2025-02-19T15:46:42+00:00" + "time": "2025-03-23T14:57:55+00:00" } ], "aliases": [], diff --git a/public-legacy/auth/login.php b/public-legacy/auth/login.php index 6c0f4eca..96cbe8f4 100644 --- a/public-legacy/auth/login.php +++ b/public-legacy/auth/login.php @@ -15,10 +15,15 @@ if($msz->authInfo->loggedIn) { if(!empty($_GET['resolve'])) { header('Content-Type: application/json; charset=utf-8'); - try { - // Only works for usernames, this is by design - $userInfo = $msz->usersCtx->users->getUser((string)filter_input(INPUT_GET, 'name'), 'name'); - } catch(Exception $ex) { + if(!empty($_GET['name']) && is_scalar($_GET['name'])) + try { + // Only works for usernames, this is by design + $userInfo = $msz->usersCtx->users->getUser((string)$_GET['name'], 'name'); + } catch(Exception $ex) { + unset($userInfo); + } + + if(empty($userInfo)) { echo json_encode([ 'id' => 0, 'name' => '', @@ -62,16 +67,16 @@ if($siteIsPrivate) { $canResetPassword = true; } -while(!empty($_POST['login']) && is_array($_POST['login'])) { +while($_SERVER['REQUEST_METHOD'] === 'POST') { if(!CSRF::validateRequest()) { $notices[] = 'Was unable to verify the request, please try again!'; break; } - $loginRedirect = empty($_POST['login']['redirect']) || !is_string($_POST['login']['redirect']) ? '' : $_POST['login']['redirect']; + $loginRedirect = empty($_POST['redirect']) || !is_string($_POST['redirect']) ? '' : $_POST['redirect']; - if(empty($_POST['login']['username']) || empty($_POST['login']['password']) - || !is_string($_POST['login']['username']) || !is_string($_POST['login']['password'])) { + if(empty($_POST['username']) || empty($_POST['password']) + || !is_string($_POST['username']) || !is_string($_POST['password'])) { $notices[] = "You didn't fill in a username and/or password."; break; } @@ -91,7 +96,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) { $loginFailedError = "Invalid username or password, {$attemptsRemainingError}."; try { - $userInfo = $msz->usersCtx->users->getUser($_POST['login']['username'], 'login'); + $userInfo = $msz->usersCtx->users->getUser($_POST['username'], 'login'); } catch(Exception $ex) { $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo); $notices[] = $loginFailedError; @@ -110,14 +115,14 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) { break; } - if(!$pwInfo->verifyPassword($_POST['login']['password'])) { + if(!$pwInfo->verifyPassword($_POST['password'])) { $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo); $notices[] = $loginFailedError; break; } if($pwInfo->needsRehash) - $msz->usersCtx->passwords->updateUserPassword($userInfo, $_POST['login']['password']); + $msz->usersCtx->passwords->updateUserPassword($userInfo, $_POST['password']); if(!empty($loginPermCat) && $loginPermVal > 0 && !$msz->perms->checkPermissions($loginPermCat, $loginPermVal, $userInfo)) { $notices[] = "Login succeeded, but you're not allowed to browse the site right now."; @@ -157,7 +162,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) { $welcomeMode = !empty($_GET['welcome']); $oauth2Mode = !empty($_GET['oauth2']); -$loginUsername = !empty($_POST['login']['username']) && is_string($_POST['login']['username']) ? $_POST['login']['username'] : ( +$loginUsername = !empty($_POST['username']) && is_string($_POST['username']) ? $_POST['username'] : ( !empty($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : '' ); $loginRedirect = $welcomeMode ? $msz->urls->format('index') : (!empty($_GET['redirect']) && is_string($_GET['redirect']) ? $_GET['redirect'] : null) ?? $_SERVER['HTTP_REFERER'] ?? $msz->urls->format('index'); diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php index a59b8f7e..7090bbe8 100644 --- a/public-legacy/auth/password.php +++ b/public-legacy/auth/password.php @@ -12,11 +12,10 @@ if($msz->authInfo->loggedIn) { return; } -$reset = !empty($_POST['reset']) && is_array($_POST['reset']) ? $_POST['reset'] : []; -$forgot = !empty($_POST['forgot']) && is_array($_POST['forgot']) ? $_POST['forgot'] : []; -$userId = !empty($reset['user']) ? (int)$reset['user'] : ( - !empty($_GET['user']) ? (int)$_GET['user'] : 0 -); +$userId = !empty($_POST['user']) && is_scalar($_POST['user']) + ? (int)$_POST['user'] : ( + !empty($_GET['user']) && is_scalar($_GET['user']) ? (int)$_GET['user'] : 0 + ); if($userId > 0) try { @@ -30,17 +29,16 @@ $notices = []; $ipAddress = $_SERVER['REMOTE_ADDR']; $siteIsPrivate = $msz->config->getBoolean('private.enable'); $canResetPassword = $siteIsPrivate ? $msz->config->getBoolean('private.allow_password_reset', true) : true; - $remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress); while($canResetPassword) { - if(!empty($reset) && $userId > 0) { + if(!empty($_POST['verification']) && is_scalar($_POST['verification']) && !empty($userInfo)) { if(!CSRF::validateRequest()) { $notices[] = 'Was unable to verify the request, please try again!'; break; } - $verifyCode = !empty($reset['verification']) && is_string($reset['verification']) ? $reset['verification'] : ''; + $verifyCode = (string)$_POST['verification']; try { $tokenInfo = $msz->authCtx->recoveryTokens->getToken(verifyCode: $verifyCode); @@ -53,9 +51,8 @@ while($canResetPassword) { break; } - $password = !empty($reset['password']) && is_array($reset['password']) ? $reset['password'] : []; - $passwordNew = !empty($password['new']) && is_string($password['new']) ? $password['new'] : ''; - $passwordConfirm = !empty($password['confirm']) && is_string($password['confirm']) ? $password['confirm'] : ''; + $passwordNew = !empty($_POST['password_new']) && is_scalar($_POST['password_new']) ? $_POST['password_new'] : ''; + $passwordConfirm = !empty($_POST['password_confirm']) && is_scalar($_POST['password_confirm']) ? $_POST['password_confirm'] : ''; if(empty($passwordNew) || empty($passwordConfirm) || $passwordNew !== $passwordConfirm) { @@ -82,24 +79,19 @@ while($canResetPassword) { return; } - if(!empty($forgot)) { + if(!empty($_POST['email']) && is_scalar($_POST['email'])) { if(!CSRF::validateRequest()) { $notices[] = 'Was unable to verify the request, please try again!'; break; } - if(empty($forgot['email']) || !is_string($forgot['email'])) { - $notices[] = "You didn't supply an e-mail address."; - break; - } - if($remainingAttempts < 1) { $notices[] = "There are too many failed login attempts from your IP address, please try again later."; break; } try { - $forgotUser = $msz->usersCtx->users->getUser($forgot['email'], 'email'); + $forgotUser = $msz->usersCtx->users->getUser((string)$_POST['email'], 'email'); } catch(RuntimeException $ex) { unset($forgotUser); } @@ -142,7 +134,7 @@ while($canResetPassword) { Template::render(isset($userInfo) ? 'auth.password_reset' : 'auth.password_forgot', [ 'password_notices' => $notices, - 'password_email' => !empty($forget['email']) && is_string($forget['email']) ? $forget['email'] : '', + 'password_email' => !empty($_POST['email']) && is_scalar($_POST['email']) ? (string)$_POST['email'] : '', 'password_attempts_remaining' => $remainingAttempts, 'password_user' => $userInfo ?? null, 'password_verification' => $verifyCode ?? '', diff --git a/public-legacy/auth/register.php b/public-legacy/auth/register.php index 0b58127c..44ce50ee 100644 --- a/public-legacy/auth/register.php +++ b/public-legacy/auth/register.php @@ -12,14 +12,13 @@ if($msz->authInfo->loggedIn) { return; } -$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : []; $notices = []; $ipAddress = $_SERVER['REMOTE_ADDR']; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress); -while(!empty($register)) { +while($_SERVER['REQUEST_METHOD'] === 'POST') { if(!CSRF::validateRequest()) { $notices[] = 'Was unable to verify the request, please try again!'; break; @@ -30,13 +29,13 @@ while(!empty($register)) { break; } - if(empty($register['username']) || empty($register['password']) || empty($register['email']) || empty($register['question']) - || !is_string($register['username']) || !is_string($register['password']) || !is_string($register['email']) || !is_string($register['question'])) { + if(empty($_POST['username']) || empty($_POST['password']) || empty($_POST['email']) || empty($_POST['question']) + || !is_scalar($_POST['username']) || !is_scalar($_POST['password']) || !is_scalar($_POST['email']) || !is_scalar($_POST['question'])) { $notices[] = "You haven't filled in all fields."; break; } - $checkSpamBot = mb_strtolower($register['question']); + $checkSpamBot = mb_strtolower($_POST['question']); $spamBotValid = [ '21', 'twentyone', 'twenty-one', 'twenty one', ]; @@ -52,18 +51,18 @@ while(!empty($register)) { break; } - $usernameValidation = $msz->usersCtx->users->validateName($register['username']); + $usernameValidation = $msz->usersCtx->users->validateName($_POST['username']); if($usernameValidation !== '') $notices[] = $msz->usersCtx->users->validateNameText($usernameValidation); - $emailValidation = $msz->usersCtx->users->validateEMailAddress($register['email']); + $emailValidation = $msz->usersCtx->users->validateEMailAddress($_POST['email']); if($emailValidation !== '') $notices[] = $msz->usersCtx->users->validateEMailAddressText($emailValidation); - if($register['password_confirm'] !== $register['password']) + if($_POST['password_confirm'] !== $_POST['password']) $notices[] = "The given passwords don't match."; - $passwordValidation = UserPasswordsData::validateUserPassword($register['password']); + $passwordValidation = UserPasswordsData::validateUserPassword($_POST['password']); if($passwordValidation !== '') $notices[] = UserPasswordsData::validateUserPasswordText($passwordValidation); @@ -74,13 +73,13 @@ while(!empty($register)) { try { $userInfo = $msz->usersCtx->users->createUser( - $register['username'], - $register['email'], + $_POST['username'], + $_POST['email'], $ipAddress, $countryCode, $defaultRoleInfo ); - $msz->usersCtx->passwords->updateUserPassword($userInfo, $register['password']); + $msz->usersCtx->passwords->updateUserPassword($userInfo, $_POST['password']); } catch(RuntimeException $ex) { $notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!'; break; @@ -99,7 +98,7 @@ while(!empty($register)) { Template::render('auth.register', [ 'register_notices' => $notices, - 'register_username' => !empty($register['username']) && is_string($register['username']) ? $register['username'] : '', - 'register_email' => !empty($register['email']) && is_string($register['email']) ? $register['email'] : '', + 'register_username' => !empty($_POST['username']) && is_scalar($_POST['username']) ? (string)$_POST['username'] : '', + 'register_email' => !empty($_POST['email']) && is_scalar($_POST['email']) ? (string)$_POST['email'] : '', 'register_restricted' => '', ]); diff --git a/public-legacy/auth/twofactor.php b/public-legacy/auth/twofactor.php index f8a272e1..84083c89 100644 --- a/public-legacy/auth/twofactor.php +++ b/public-legacy/auth/twofactor.php @@ -16,13 +16,12 @@ if($msz->authInfo->loggedIn) { $ipAddress = $_SERVER['REMOTE_ADDR']; $countryCode = $_SERVER['COUNTRY_CODE'] ?? 'XX'; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; -$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : []; $notices = []; $remainingAttempts = $msz->authCtx->loginAttempts->countRemainingAttempts($ipAddress); -$tokenString = !empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : ( - !empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : '' +$tokenString = !empty($_GET['token']) && is_scalar($_GET['token']) ? (string)$_GET['token'] : ( + !empty($_POST['token']) && is_scalar($_POST['token']) ? (string)$_POST['token'] : '' ); $tokenUserId = $msz->authCtx->tfaSessions->getTokenUserId($tokenString); @@ -37,16 +36,16 @@ if($totpInfo === null) { return; } -while(!empty($twofactor)) { +while($_SERVER['REQUEST_METHOD'] === 'POST') { if(!CSRF::validateRequest()) { $notices[] = 'Was unable to verify the request, please try again!'; break; } $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; - $redirect = !empty($twofactor['redirect']) && is_string($twofactor['redirect']) ? $twofactor['redirect'] : ''; + $redirect = !empty($_POST['redirect']) && is_scalar($_POST['redirect']) ? (string)$_POST['redirect'] : ''; - if(empty($twofactor['code']) || !is_string($twofactor['code'])) { + if(empty($_POST['code']) || !is_string($_POST['code'])) { $notices[] = 'Code field was empty.'; break; } @@ -59,7 +58,7 @@ while(!empty($twofactor)) { $clientInfo = ClientInfo::fromRequest(); $generator = $totpInfo->createGenerator(); - if(!in_array($twofactor['code'], $generator->generateRange())) { + if(!in_array($_POST['code'], $generator->generateRange())) { $notices[] = sprintf( "Invalid two factor code, %d attempt%s remaining", $remainingAttempts - 1, diff --git a/public-legacy/forum/leaderboard.php b/public-legacy/forum/leaderboard.php index c6818e32..68236fc1 100644 --- a/public-legacy/forum/leaderboard.php +++ b/public-legacy/forum/leaderboard.php @@ -16,8 +16,8 @@ $config = $msz->config->getValues([ 'forum_leader.unranked.topic:a', ]); -$mode = (string)filter_input(INPUT_GET, 'mode'); -$yearMonth = (string)filter_input(INPUT_GET, 'id'); +$mode = isset($_GET['mode']) && is_scalar($_GET['mode']) ? (string)$_GET['mode'] : ''; +$yearMonth = isset($_GET['id']) && is_scalar($_GET['id']) ? (string)$_GET['id'] : ''; $year = $month = 0; $currentYear = (int)date('Y'); @@ -39,7 +39,7 @@ if(!empty($yearMonth)) { } } -if(filter_has_var(INPUT_GET, 'allow_unranked')) { +if(isset($_GET['allow_unranked'])) { $unrankedForums = $unrankedTopics = []; } else { $unrankedForums = $config['forum_leader.unranked.forum']; diff --git a/public-legacy/forum/posting.php b/public-legacy/forum/posting.php index 531bb8cc..601c2f0a 100644 --- a/public-legacy/forum/posting.php +++ b/public-legacy/forum/posting.php @@ -19,11 +19,11 @@ $currentUserId = $currentUser->id; if($msz->usersCtx->hasActiveBan($currentUser)) Template::throwError(403); -if(filter_has_var(INPUT_POST, 'preview')) { +if(!empty($_POST['preview'])) { header('Content-Type: text/plain; charset=utf-8'); - $text = (string)filter_input(INPUT_POST, 'text'); - $format = TextFormat::tryFrom((string)filter_input(INPUT_POST, 'format')); + $text = isset($_POST['text']) && is_scalar($_POST['text']) ? (string)$_POST['text'] : ''; + $format = TextFormat::tryFrom(isset($_POST['format']) && is_scalar($_POST['format']) ? (string)$_POST['format'] : ''); if($format === null) { http_response_code(400); return; @@ -39,10 +39,10 @@ $forumPostingModes = [ ]; if(!empty($_POST)) { - $mode = !empty($_POST['post']['mode']) && is_string($_POST['post']['mode']) ? $_POST['post']['mode'] : 'create'; - $postId = !empty($_POST['post']['id']) && is_string($_POST['post']['id']) ? (int)$_POST['post']['id'] : 0; - $topicId = !empty($_POST['post']['topic']) && is_string($_POST['post']['topic']) ? (int)$_POST['post']['topic'] : 0; - $forumId = !empty($_POST['post']['forum']) && is_string($_POST['post']['forum']) ? (int)$_POST['post']['forum'] : 0; + $mode = !empty($_POST['mode']) && is_string($_POST['mode']) ? $_POST['mode'] : 'create'; + $postId = !empty($_POST['id']) && is_string($_POST['id']) ? (int)$_POST['id'] : 0; + $topicId = !empty($_POST['topic']) && is_string($_POST['topic']) ? (int)$_POST['topic'] : 0; + $forumId = !empty($_POST['forum']) && is_string($_POST['forum']) ? (int)$_POST['forum'] : 0; } else { $mode = !empty($_GET['m']) && is_string($_GET['m']) ? $_GET['m'] : 'create'; $postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0; @@ -141,11 +141,11 @@ if($mode === 'edit') { $notices = []; if(!empty($_POST)) { - $topicTitle = $_POST['post']['title'] ?? ''; - $postText = $_POST['post']['text'] ?? ''; - $postParser = TextFormat::tryFrom((string)($_POST['post']['parser'] ?? '')) ?? TextFormat::BBCode; - $topicType = isset($_POST['post']['type']) ? $_POST['post']['type'] : null; - $postSignature = isset($_POST['post']['signature']); + $topicTitle = $_POST['title'] ?? ''; + $postText = $_POST['text'] ?? ''; + $postParser = TextFormat::tryFrom((string)($_POST['parser'] ?? '')) ?? TextFormat::BBCode; + $topicType = isset($_POST['type']) ? $_POST['type'] : null; + $postSignature = isset($_POST['signature']); if(!CSRF::validateRequest()) { $notices[] = 'Could not verify request.'; diff --git a/public-legacy/manage/changelog/change.php b/public-legacy/manage/changelog/change.php index fa9622a4..1b31f76a 100644 --- a/public-legacy/manage/changelog/change.php +++ b/public-legacy/manage/changelog/change.php @@ -17,7 +17,7 @@ $changeActions = []; foreach(ChangelogData::ACTIONS as $action) $changeActions[$action] = ChangelogData::actionText($action); -$changeId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); +$changeId = !empty($_GET['c']) && is_scalar($_GET['c']) ? (string)$_GET['c'] : ''; $changeInfo = null; $changeTagIds = []; $tagInfos = $msz->changelog->getTags(); @@ -45,12 +45,12 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { // make errors not echos lol while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $action = trim((string)filter_input(INPUT_POST, 'cl_action')); - $summary = trim((string)filter_input(INPUT_POST, 'cl_summary')); - $body = trim((string)filter_input(INPUT_POST, 'cl_body')); - $userId = (int)filter_input(INPUT_POST, 'cl_user', FILTER_SANITIZE_NUMBER_INT); - $createdAt = trim((string)filter_input(INPUT_POST, 'cl_created')); - $tags = filter_input(INPUT_POST, 'cl_tags', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); + $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']) : ''; + $userId = !empty($_POST['cl_user']) && is_scalar($_POST['cl_user']) ? (int)$_POST['cl_user'] : 0; + $createdAt = !empty($_POST['cl_created']) && is_scalar($_POST['cl_created']) ? trim((string)$_POST['cl_created']) : ''; + $tags = !empty($_POST['cl_tags']) && is_array($_POST['cl_tags']) ? $_POST['cl_tags'] : []; if($userId < 1) $userId = null; else $userId = (string)$userId; diff --git a/public-legacy/manage/changelog/tag.php b/public-legacy/manage/changelog/tag.php index 5c1fa5bb..1361e9de 100644 --- a/public-legacy/manage/changelog/tag.php +++ b/public-legacy/manage/changelog/tag.php @@ -9,7 +9,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('global')->check(Perm::G_CL_TAGS_MANAGE)) Template::throwError(403); -$tagId = (string)filter_input(INPUT_GET, 't', FILTER_SANITIZE_NUMBER_INT); +$tagId = !empty($_GET['t']) && is_scalar($_GET['t']) ? (string)$_GET['t'] : ''; $loadTagInfo = fn() => $msz->changelog->getTag($tagId); if(empty($tagId)) @@ -33,8 +33,8 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { } while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $name = trim((string)filter_input(INPUT_POST, 'ct_name')); - $description = trim((string)filter_input(INPUT_POST, 'ct_desc')); + $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']); if($isNew) { diff --git a/public-legacy/manage/forum/index.php b/public-legacy/manage/forum/index.php index 5d3e53e2..abf1af83 100644 --- a/public-legacy/manage/forum/index.php +++ b/public-legacy/manage/forum/index.php @@ -12,11 +12,8 @@ if(!$msz->authInfo->getPerms('global')->check(Perm::G_FORUM_CATEGORIES_MANAGE)) $permsInfos = $msz->perms->getPermissionInfo(categoryNames: Perm::INFO_FOR_FORUM_CATEGORY); $permsLists = Perm::createList(Perm::LISTS_FOR_FORUM_CATEGORY); -if(filter_has_var(INPUT_POST, 'perms')) - Template::set('calculated_perms', Perm::convertSubmission( - filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY), - Perm::INFO_FOR_FORUM_CATEGORY - )); +if(!empty($_POST)) + Template::set('calculated_perms', Perm::convertSubmission($_POST, Perm::INFO_FOR_FORUM_CATEGORY)); Template::render('manage.forum.listing', [ 'perms_lists' => $permsLists, diff --git a/public-legacy/manage/forum/redirs.php b/public-legacy/manage/forum/redirs.php index da97b0d4..6e6f12cd 100644 --- a/public-legacy/manage/forum/redirs.php +++ b/public-legacy/manage/forum/redirs.php @@ -11,8 +11,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') { if(!CSRF::validateRequest()) throw new \Exception("Request verification failed."); - $rTopicId = (string)filter_input(INPUT_POST, 'topic_redir_id'); - $rTopicURL = trim((string)filter_input(INPUT_POST, 'topic_redir_url')); + $rTopicId = !empty($_POST['topic_redir_id']) && is_scalar($_POST['topic_redir_id']) ? trim((string)$_POST['topic_redir_id']) : ''; + $rTopicURL = !empty($_POST['topic_redir_url']) && is_scalar($_POST['topic_redir_url']) ? trim((string)$_POST['topic_redir_url']) : ''; $msz->createAuditLog('FORUM_TOPIC_REDIR_CREATE', [$rTopicId]); $msz->forumCtx->topicRedirects->createTopicRedirect($rTopicId, $msz->authInfo->userInfo, $rTopicURL); @@ -20,11 +20,11 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') { return; } -if(filter_input(INPUT_GET, 'm') === 'explode') { +if(!empty($_GET['m']) && $_GET['m'] === 'explode') { if(!CSRF::validateRequest()) throw new \Exception("Request verification failed."); - $rTopicId = (string)filter_input(INPUT_GET, 't'); + $rTopicId = !empty($_GET['t']) && is_scalar($_GET['t']) ? (string)$_GET['t'] : ''; $msz->createAuditLog('FORUM_TOPIC_REDIR_REMOVE', [$rTopicId]); $msz->forumCtx->topicRedirects->deleteTopicRedirect($rTopicId); Tools::redirect($msz->urls->format('manage-forum-topic-redirs')); diff --git a/public-legacy/manage/general/emoticon.php b/public-legacy/manage/general/emoticon.php index 6ecb10cf..2993c0fe 100644 --- a/public-legacy/manage/general/emoticon.php +++ b/public-legacy/manage/general/emoticon.php @@ -10,7 +10,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('global')->check(Perm::G_EMOTES_MANAGE)) Template::throwError(403); -$emoteId = (string)filter_input(INPUT_GET, 'e', FILTER_SANITIZE_NUMBER_INT); +$emoteId = !empty($_GET['e']) && is_scalar($_GET['e']) ? (string)$_GET['e'] : ''; $emoteInfo = []; $emoteStrings = []; @@ -27,10 +27,10 @@ else // make errors not echos lol while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $order = (int)filter_input(INPUT_POST, 'em_order', FILTER_SANITIZE_NUMBER_INT); - $minRank = (int)filter_input(INPUT_POST, 'em_minrank', FILTER_SANITIZE_NUMBER_INT); - $url = trim((string)filter_input(INPUT_POST, 'em_url')); - $strings = explode(' ', trim((string)filter_input(INPUT_POST, 'em_strings'))); + $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']) : ''; + $strings = explode(' ', !empty($_POST['em_strings']) && is_scalar($_POST['em_strings']) ? trim((string)$_POST['em_strings']) : ''); if($isNew || $url !== $emoteInfo->url) { $checkUrl = $msz->emotes->checkEmoteUrl($url); diff --git a/public-legacy/manage/general/emoticons.php b/public-legacy/manage/general/emoticons.php index 90987fdc..5b1e60f2 100644 --- a/public-legacy/manage/general/emoticons.php +++ b/public-legacy/manage/general/emoticons.php @@ -10,7 +10,7 @@ if(!$msz->authInfo->getPerms('global')->check(Perm::G_EMOTES_MANAGE)) Template::throwError(403); if(CSRF::validateRequest() && !empty($_GET['emote'])) { - $emoteId = (string)filter_input(INPUT_GET, 'emote', FILTER_SANITIZE_NUMBER_INT); + $emoteId = !empty($_GET['emote']) && is_scalar($_GET['emote']) ? (string)$_GET['emote'] : ''; try { $emoteInfo = $msz->emotes->getEmote($emoteId); @@ -23,14 +23,14 @@ if(CSRF::validateRequest() && !empty($_GET['emote'])) { $msz->createAuditLog('EMOTICON_DELETE', [$emoteInfo->id]); } else { if(isset($_GET['order'])) { - $order = filter_input(INPUT_GET, 'order'); + $order = !empty($_GET['order']) && is_scalar($_GET['order']) ? (string)$_GET['order'] : ''; $offset = $order === 'i' ? 10 : ($order === 'd' ? -10 : 0); $msz->emotes->updateEmoteOrderOffset($emoteInfo, $offset); $msz->createAuditLog('EMOTICON_ORDER', [$emoteInfo->id]); } if(isset($_GET['alias'])) { - $alias = (string)filter_input(INPUT_GET, 'alias'); + $alias = !empty($_GET['alias']) && is_scalar($_GET['alias']) ? (string)$_GET['alias'] : ''; if($msz->emotes->checkEmoteString($alias) === '') { $msz->emotes->addEmoteString($emoteInfo, $alias); $msz->createAuditLog('EMOTICON_ALIAS', [$emoteInfo->id, $alias]); diff --git a/public-legacy/manage/general/setting-delete.php b/public-legacy/manage/general/setting-delete.php index e614271b..0a5a8205 100644 --- a/public-legacy/manage/general/setting-delete.php +++ b/public-legacy/manage/general/setting-delete.php @@ -7,7 +7,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('global')->check(Perm::G_CONFIG_MANAGE)) Template::throwError(403); -$valueInfo = $msz->config->getValueInfo((string)filter_input(INPUT_GET, 'name')); +$valueInfo = $msz->config->getValueInfo(!empty($_GET['name']) && is_scalar($_GET['name']) ? (string)$_GET['name'] : ''); if($valueInfo === null) Template::throwError(404); diff --git a/public-legacy/manage/general/setting.php b/public-legacy/manage/general/setting.php index c98c5878..15aca438 100644 --- a/public-legacy/manage/general/setting.php +++ b/public-legacy/manage/general/setting.php @@ -10,8 +10,8 @@ if(!$msz->authInfo->getPerms('global')->check(Perm::G_CONFIG_MANAGE)) Template::throwError(403); $isNew = true; -$sName = (string)filter_input(INPUT_GET, 'name'); -$sType = (string)filter_input(INPUT_GET, 'type'); +$sName = !empty($_GET['name']) && is_scalar($_GET['name']) ? (string)$_GET['name'] : ''; +$sType = !empty($_GET['type']) && is_scalar($_GET['type']) ? (string)$_GET['type'] : ''; $sValue = null; $loadValueInfo = fn() => $msz->config->getValueInfo($sName); @@ -27,13 +27,13 @@ if(!empty($sName)) { while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { if($isNew) { - $sName = trim((string)filter_input(INPUT_POST, 'conf_name')); + $sName = !empty($_POST['conf_name']) && is_scalar($_POST['conf_name']) ? trim((string)$_POST['conf_name']) : ''; if(!DbConfig::validateName($sName)) { echo 'Name contains invalid characters.'; break; } - $sType = trim((string)filter_input(INPUT_POST, 'conf_type')); + $sType = !empty($_POST['conf_type']) && is_scalar($_POST['conf_type']) ? trim((string)$_POST['conf_type']) : ''; if(!in_array($sType, ['string', 'int', 'float', 'bool', 'array'])) { echo 'Invalid type specified.'; break; @@ -43,7 +43,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { if($sType === 'array') { $applyFunc = $msz->config->setArray(...); $sValue = []; - $sRaw = filter_input(INPUT_POST, 'conf_value', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); + $sRaw = !empty($_POST['conf_value']) && is_array($_POST['conf_value']) ? $_POST['conf_value'] : []; foreach($sRaw as $rValue) { if(strpos($rValue, ':') === 1) { $rType = $rValue[0]; @@ -63,7 +63,7 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { $sValue = !empty($_POST['conf_value']); $applyFunc = $msz->config->setBoolean(...); } else { - $sValue = filter_input(INPUT_POST, 'conf_value'); + $sValue = !empty($_POST['conf_value']) && is_scalar($_POST['conf_value']) ? trim((string)$_POST['conf_value']) : ''; if($sType === 'int') { $applyFunc = $msz->config->setInteger(...); $sValue = (int)$sValue; diff --git a/public-legacy/manage/news/category.php b/public-legacy/manage/news/category.php index dde4ebb6..8a64f33d 100644 --- a/public-legacy/manage/news/category.php +++ b/public-legacy/manage/news/category.php @@ -9,7 +9,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_CATEGORIES_MANAGE)) Template::throwError(403); -$categoryId = (string)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT); +$categoryId = !empty($_GET['c']) && is_scalar($_GET['c']) ? (string)$_GET['c'] : ''; $loadCategoryInfo = fn() => $msz->news->getCategory(categoryId: $categoryId); if(empty($categoryId)) @@ -33,8 +33,8 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { } while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $name = trim((string)filter_input(INPUT_POST, 'nc_name')); - $description = trim((string)filter_input(INPUT_POST, 'nc_desc')); + $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']); if($isNew) { diff --git a/public-legacy/manage/news/post.php b/public-legacy/manage/news/post.php index 6673f69e..bea405af 100644 --- a/public-legacy/manage/news/post.php +++ b/public-legacy/manage/news/post.php @@ -10,7 +10,7 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('global')->check(Perm::G_NEWS_POSTS_MANAGE)) Template::throwError(403); -$postId = (string)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT); +$postId = !empty($_GET['p']) && is_scalar($_GET['p']) ? (string)$_GET['p'] : ''; $loadPostInfo = fn() => $msz->news->getPost($postId); if(empty($postId)) @@ -34,10 +34,10 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { } while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $title = trim((string)filter_input(INPUT_POST, 'np_title')); - $category = (string)filter_input(INPUT_POST, 'np_category', FILTER_SANITIZE_NUMBER_INT); + $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']); - $body = trim((string)filter_input(INPUT_POST, 'np_body')); + $body = !empty($_POST['np_body']) && is_scalar($_POST['np_body']) ? trim((string)$_POST['np_body']) : ''; if($isNew) { $postInfo = $msz->news->createPost($category, $title, $body, $featured, $msz->authInfo->userInfo); diff --git a/public-legacy/manage/users/ban.php b/public-legacy/manage/users/ban.php index 0304783e..1a2d05a5 100644 --- a/public-legacy/manage/users/ban.php +++ b/public-legacy/manage/users/ban.php @@ -11,12 +11,12 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE)) Template::throwError(403); -if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(!CSRF::validateRequest()) Template::throwError(403); try { - $banInfo = $msz->usersCtx->bans->getBan((string)filter_input(INPUT_GET, 'b')); + $banInfo = $msz->usersCtx->bans->getBan(!empty($_GET['b']) && is_scalar($_GET['b']) ? (string)$_GET['b'] : ''); } catch(RuntimeException $ex) { Template::throwError(404); } @@ -28,7 +28,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) } try { - $userInfo = $msz->usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id'); + $userInfo = $msz->usersCtx->getUserInfo(!empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : '', 'id'); } catch(RuntimeException $ex) { Template::throwError(404); } @@ -36,11 +36,11 @@ try { $modInfo = $msz->authInfo->userInfo; while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $expires = (int)filter_input(INPUT_POST, 'ub_expires', FILTER_SANITIZE_NUMBER_INT); - $expiresCustom = (string)filter_input(INPUT_POST, 'ub_expires_custom'); - $publicReason = trim((string)filter_input(INPUT_POST, 'ub_reason_pub')); - $privateReason = trim((string)filter_input(INPUT_POST, 'ub_reason_priv')); - $severity = (int)filter_input(INPUT_POST, 'ub_severity', FILTER_SANITIZE_NUMBER_INT); + $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']) : ''; + $privateReason = !empty($_POST['ub_reason_priv']) && is_scalar($_POST['ub_reason_priv']) ? trim((string)$_POST['ub_reason_priv']) : ''; + $severity = !empty($_POST['ub_severity']) && is_scalar($_POST['ub_severity']) ? (int)$_POST['ub_severity'] : 0; Template::set([ 'ban_value_expires' => $expires, diff --git a/public-legacy/manage/users/bans.php b/public-legacy/manage/users/bans.php index b2b985af..04f44ef5 100644 --- a/public-legacy/manage/users/bans.php +++ b/public-legacy/manage/users/bans.php @@ -10,8 +10,8 @@ if(!$msz->authInfo->getPerms('user')->check(Perm::U_BANS_MANAGE)) Template::throwError(403); $filterUser = null; -if(filter_has_var(INPUT_GET, 'u')) { - $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); +if(!empty($_GET['u'])) { + $filterUserId = !empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : ''; try { $filterUser = $msz->usersCtx->getUserInfo($filterUserId); } catch(RuntimeException $ex) { @@ -20,7 +20,7 @@ if(filter_has_var(INPUT_GET, 'u')) { } $pagination = Pagination::fromInput($msz->usersCtx->bans->countBans(userInfo: $filterUser), 10); -if(!$pagination->validOffset) +if(!$pagination->validOffset && $pagination->count > 0) Template::throwError(404); $banList = []; diff --git a/public-legacy/manage/users/note.php b/public-legacy/manage/users/note.php index dc122da2..0f8a3009 100644 --- a/public-legacy/manage/users/note.php +++ b/public-legacy/manage/users/note.php @@ -9,8 +9,8 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE)) Template::throwError(403); -$hasNoteId = filter_has_var(INPUT_GET, 'n'); -$hasUserId = filter_has_var(INPUT_GET, 'u'); +$hasNoteId = !empty($_GET['n']); +$hasUserId = !empty($_GET['u']); if((!$hasNoteId && !$hasUserId) || ($hasNoteId && $hasUserId)) Template::throwError(400); @@ -19,7 +19,7 @@ if($hasUserId) { $isNew = true; try { - $userInfo = $msz->usersCtx->getUserInfo(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT)); + $userInfo = $msz->usersCtx->getUserInfo(!empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : ''); } catch(RuntimeException $ex) { Template::throwError(404); } @@ -29,12 +29,12 @@ if($hasUserId) { $isNew = false; try { - $noteInfo = $msz->usersCtx->modNotes->getNote((string)filter_input(INPUT_GET, 'n', FILTER_SANITIZE_NUMBER_INT)); + $noteInfo = $msz->usersCtx->modNotes->getNote(!empty($_GET['n']) && is_scalar($_GET['n']) ? (string)$_GET['n'] : ''); } catch(RuntimeException $ex) { Template::throwError(404); } - if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { + if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(!CSRF::validateRequest()) Template::throwError(403); @@ -49,8 +49,8 @@ if($hasUserId) { } while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $title = trim((string)filter_input(INPUT_POST, 'mn_title')); - $body = trim((string)filter_input(INPUT_POST, 'mn_body')); + $title = trim((string)($_POST['mn_title'] ?? '')); + $body = trim((string)($_POST['mn_body'] ?? '')); if($isNew) { $noteInfo = $msz->usersCtx->modNotes->createNote($userInfo, $title, $body, $authorInfo); diff --git a/public-legacy/manage/users/notes.php b/public-legacy/manage/users/notes.php index 1489b4a7..e734bba1 100644 --- a/public-legacy/manage/users/notes.php +++ b/public-legacy/manage/users/notes.php @@ -10,8 +10,8 @@ if(!$msz->authInfo->getPerms('user')->check(Perm::U_NOTES_MANAGE)) Template::throwError(403); $filterUser = null; -if(filter_has_var(INPUT_GET, 'u')) { - $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); +if(!empty($_GET['u'])) { + $filterUserId = !empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : ''; try { $filterUser = $msz->usersCtx->getUserInfo($filterUserId); } catch(RuntimeException $ex) { @@ -20,7 +20,7 @@ if(filter_has_var(INPUT_GET, 'u')) { } $pagination = Pagination::fromInput($msz->usersCtx->modNotes->countNotes(userInfo: $filterUser), 10); -if(!$pagination->validOffset) +if(!$pagination->validOffset && $pagination->count > 0) Template::throwError(404); $notes = []; diff --git a/public-legacy/manage/users/role.php b/public-legacy/manage/users/role.php index 5a10a11d..28e7bf39 100644 --- a/public-legacy/manage/users/role.php +++ b/public-legacy/manage/users/role.php @@ -15,8 +15,8 @@ if(!$viewerPerms->check(Perm::U_ROLES_MANAGE)) $roleInfo = null; -if(filter_has_var(INPUT_GET, 'r')) { - $roleId = (string)filter_input(INPUT_GET, 'r', FILTER_SANITIZE_NUMBER_INT); +if(!empty($_GET['r'])) { + $roleId = !empty($_GET['r']) && is_scalar($_GET['r']) ? (string)$_GET['r'] : ''; try { $isNew = false; @@ -40,17 +40,17 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { break; } - $roleString = (string)filter_input(INPUT_POST, 'ur_string'); - $roleName = (string)filter_input(INPUT_POST, 'ur_name'); + $roleString = !empty($_POST['ur_string']) && is_scalar($_POST['ur_string']) ? trim((string)$_POST['ur_string']) : ''; + $roleName = !empty($_POST['ur_name']) && is_scalar($_POST['ur_name']) ? trim((string)$_POST['ur_name']) : ''; $roleHide = !empty($_POST['ur_hidden']); $roleLeavable = !empty($_POST['ur_leavable']); - $roleRank = (int)filter_input(INPUT_POST, 'ur_rank', FILTER_SANITIZE_NUMBER_INT); - $roleTitle = (string)filter_input(INPUT_POST, 'ur_title'); - $roleDesc = (string)filter_input(INPUT_POST, 'ur_desc'); + $roleRank = !empty($_POST['ur_rank']) && is_scalar($_POST['ur_rank']) ? (int)$_POST['ur_rank'] : 0; + $roleTitle = !empty($_POST['ur_title']) && is_scalar($_POST['ur_title']) ? trim((string)$_POST['ur_title']) : ''; + $roleDesc = !empty($_POST['ur_desc']) && is_scalar($_POST['ur_desc']) ? trim((string)$_POST['ur_desc']) : ''; $colourInherit = !empty($_POST['ur_col_inherit']); - $colourRed = (int)filter_input(INPUT_POST, 'ur_col_red', FILTER_SANITIZE_NUMBER_INT); - $colourGreen = (int)filter_input(INPUT_POST, 'ur_col_green', FILTER_SANITIZE_NUMBER_INT); - $colourBlue = (int)filter_input(INPUT_POST, 'ur_col_blue', FILTER_SANITIZE_NUMBER_INT); + $colourRed = !empty($_POST['ur_col_red']) && is_scalar($_POST['ur_col_red']) ? (int)$_POST['ur_col_red'] : 0; + $colourGreen = !empty($_POST['ur_col_green']) && is_scalar($_POST['ur_col_green']) ? (int)$_POST['ur_col_green'] : 0; + $colourBlue = !empty($_POST['ur_col_blue']) && is_scalar($_POST['ur_col_blue']) ? (int)$_POST['ur_col_blue'] : 0; Template::set([ 'role_ur_string' => $roleString, @@ -153,12 +153,8 @@ while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { [$roleInfo->id] ); - if($canEditPerms && filter_has_var(INPUT_POST, 'perms')) { - $permsApply = Perm::convertSubmission( - filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY), - Perm::INFO_FOR_ROLE - ); - + if($canEditPerms) { + $permsApply = Perm::convertSubmission($_POST, Perm::INFO_FOR_ROLE); foreach($permsApply as $categoryName => $values) $msz->perms->setPermissions($categoryName, $values['allow'], $values['deny'], roleInfo: $roleInfo); diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php index 992d9de2..b6a7a90e 100644 --- a/public-legacy/manage/users/user.php +++ b/public-legacy/manage/users/user.php @@ -29,7 +29,7 @@ if(!$hasAccess) Template::throwError(403); $notices = []; -$userId = (string)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); +$userId = !empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : ''; try { $userInfo = $msz->usersCtx->users->getUser($userId, 'id'); @@ -201,12 +201,8 @@ if(CSRF::validateRequest() && $canEdit) { } } - if($canEditPerms && filter_has_var(INPUT_POST, 'perms')) { - $permsApply = Perm::convertSubmission( - filter_input(INPUT_POST, 'perms', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY), - Perm::INFO_FOR_USER - ); - + if($canEditPerms) { + $permsApply = Perm::convertSubmission($_POST, Perm::INFO_FOR_USER); foreach($permsApply as $categoryName => $values) $msz->perms->setPermissions($categoryName, $values['allow'], $values['deny'], userInfo: $userInfo); diff --git a/public-legacy/manage/users/warning.php b/public-legacy/manage/users/warning.php index 55cba84d..dfac1df1 100644 --- a/public-legacy/manage/users/warning.php +++ b/public-legacy/manage/users/warning.php @@ -9,12 +9,12 @@ if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext)) if(!$msz->authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE)) Template::throwError(403); -if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) { +if($_SERVER['REQUEST_METHOD'] === 'GET' && !empty($_GET['delete'])) { if(!CSRF::validateRequest()) Template::throwError(403); try { - $warnInfo = $msz->usersCtx->warnings->getWarning((string)filter_input(INPUT_GET, 'w')); + $warnInfo = $msz->usersCtx->warnings->getWarning(!empty($_GET['w']) && is_scalar($_GET['w']) ? (string)$_GET['w'] : ''); } catch(RuntimeException $ex) { Template::throwError(404); } @@ -26,7 +26,7 @@ if($_SERVER['REQUEST_METHOD'] === 'GET' && filter_has_var(INPUT_GET, 'delete')) } try { - $userInfo = $msz->usersCtx->users->getUser(filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT), 'id'); + $userInfo = $msz->usersCtx->users->getUser(!empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : '', 'id'); } catch(RuntimeException $ex) { Template::throwError(404); } @@ -34,7 +34,7 @@ try { $modInfo = $msz->authInfo->userInfo; while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $body = trim((string)filter_input(INPUT_POST, 'uw_body')); + $body = trim((string)($_POST['uw_body'] ?? '')); Template::set('warn_value_body', $body); $warnInfo = $msz->usersCtx->warnings->createWarning( diff --git a/public-legacy/manage/users/warnings.php b/public-legacy/manage/users/warnings.php index a35af7e4..7f960966 100644 --- a/public-legacy/manage/users/warnings.php +++ b/public-legacy/manage/users/warnings.php @@ -10,8 +10,8 @@ if(!$msz->authInfo->getPerms('user')->check(Perm::U_WARNINGS_MANAGE)) Template::throwError(403); $filterUser = null; -if(filter_has_var(INPUT_GET, 'u')) { - $filterUserId = filter_input(INPUT_GET, 'u', FILTER_SANITIZE_NUMBER_INT); +if(!empty($_GET['u'])) { + $filterUserId = !empty($_GET['u']) && is_scalar($_GET['u']) ? (string)$_GET['u'] : ''; try { $filterUser = $msz->usersCtx->getUserInfo($filterUserId); } catch(RuntimeException $ex) { @@ -20,7 +20,7 @@ if(filter_has_var(INPUT_GET, 'u')) { } $pagination = Pagination::fromInput($msz->usersCtx->warnings->countWarnings(userInfo: $filterUser), 10); -if(!$pagination->validOffset) +if(!$pagination->validOffset && $pagination->count > 0) Template::throwError(404); $warnList = []; diff --git a/public-legacy/members.php b/public-legacy/members.php index ab541612..03925af6 100644 --- a/public-legacy/members.php +++ b/public-legacy/members.php @@ -11,9 +11,9 @@ if(!$msz->authInfo->loggedIn) // TODO: restore forum-topics and forum-posts orderings -$roleId = filter_has_var(INPUT_GET, 'r') ? (string)filter_input(INPUT_GET, 'r') : null; -$orderBy = strtolower((string)filter_input(INPUT_GET, 'ss')); -$orderDir = strtolower((string)filter_input(INPUT_GET, 'sd')); +$roleId = !empty($_GET['r']) && is_string($_GET['r']) ? $_GET['r'] : null; +$orderBy = strtolower(!empty($_GET['ss']) && is_string($_GET['ss']) ? $_GET['ss'] : ''); +$orderDir = strtolower(!empty($_GET['sd']) && is_string($_GET['sd']) ? $_GET['sd'] : ''); $orderDirs = [ 'asc' => 'In Order', diff --git a/public-legacy/profile.php b/public-legacy/profile.php index 60d478f2..25a4ebab 100644 --- a/public-legacy/profile.php +++ b/public-legacy/profile.php @@ -5,6 +5,8 @@ use stdClass; use InvalidArgumentException; use RuntimeException; use Index\ByteFormat; +use Index\Http\Content\MultipartFormContent; +use Index\Http\Content\Multipart\FileMultipartFormData; use Misuzu\Forum\ForumSignaturesData; use Misuzu\Parsers\TextFormat; use Misuzu\Profile\{ProfileAboutData,ProfileBackgroundAttach}; @@ -111,52 +113,49 @@ if($isEditing) { if(!CSRF::validateRequest()) { $notices[] = "Couldn't verify you, please refresh the page and retry."; } else { - $profileFieldsSubmit = filter_input(INPUT_POST, 'profile', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); + if(!$perms->edit_profile) { + $notices[] = "You're not allowed to edit your profile."; + } else { + $profileFieldInfos = iterator_to_array($msz->profileCtx->fields->getFields()); + $profileFieldsSetInfos = []; + $profileFieldsSetValues = []; + $profileFieldsRemove = []; - if(!empty($profileFieldsSubmit)) { - if(!$perms->edit_profile) { - $notices[] = "You're not allowed to edit your profile."; - } else { - $profileFieldInfos = iterator_to_array($msz->profileCtx->fields->getFields()); - $profileFieldsSetInfos = []; - $profileFieldsSetValues = []; - $profileFieldsRemove = []; + foreach($profileFieldInfos as $fieldInfo) { + $fieldName = sprintf('profile_%s', $fieldInfo->name); + $fieldValue = empty($_POST[$fieldName]) || !is_scalar($_POST[$fieldName]) + ? '' : (string)filter_var($_POST[$fieldName]); - foreach($profileFieldInfos as $fieldInfo) { - $fieldName = $fieldInfo->name; - $fieldValue = empty($profileFieldsSubmit[$fieldName]) ? '' : (string)filter_var($profileFieldsSubmit[$fieldName]); - - if(empty($profileFieldsSubmit[$fieldName])) { - $profileFieldsRemove[] = $fieldInfo; - continue; - } - - if($fieldInfo->checkValue($fieldValue)) { - $profileFieldsSetInfos[] = $fieldInfo; - $profileFieldsSetValues[] = $fieldValue; - } else - $notices[] = sprintf("%s isn't properly formatted.", $fieldInfo->title); - - unset($fieldName, $fieldValue, $fieldInfo); + if(empty($_POST[$fieldName])) { + $profileFieldsRemove[] = $fieldInfo; + continue; } - if(!empty($profileFieldsRemove)) - $msz->profileCtx->fields->removeFieldValues($userInfo, $profileFieldsRemove); - if(!empty($profileFieldsSetInfos)) - $msz->profileCtx->fields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues); + if($fieldInfo->checkValue($fieldValue)) { + $profileFieldsSetInfos[] = $fieldInfo; + $profileFieldsSetValues[] = $fieldValue; + } else + $notices[] = sprintf("%s isn't properly formatted.", $fieldInfo->title); + + unset($fieldName, $fieldValue, $fieldInfo); } + + if(!empty($profileFieldsRemove)) + $msz->profileCtx->fields->removeFieldValues($userInfo, $profileFieldsRemove); + if(!empty($profileFieldsSetInfos)) + $msz->profileCtx->fields->setFieldValues($userInfo, $profileFieldsSetInfos, $profileFieldsSetValues); } - if(filter_has_var(INPUT_POST, 'about_body')) { + if(isset($_POST['about_body']) && is_scalar($_POST['about_body'])) { if(!$perms->edit_about) { $notices[] = "You're not allowed to edit your about page."; } else { - $aboutBody = (string)filter_input(INPUT_POST, 'about_body'); + $aboutBody = (string)$_POST['about_body']; if(trim($aboutBody) === '') { $msz->profileCtx->about->deleteProfileAbout($userInfo); $aboutInfo = null; } else { - $aboutFormat = TextFormat::tryFrom(filter_input(INPUT_POST, 'about_format')); + $aboutFormat = TextFormat::tryFrom(isset($_POST['about_format']) && is_scalar($_POST['about_format']) ? (string)$_POST['about_format'] : ''); $aboutValid = ProfileAboutData::validateProfileAbout($aboutFormat, $aboutBody); if($aboutValid === '') $aboutInfo = $msz->profileCtx->about->updateProfileAbout($userInfo, $aboutBody, $aboutFormat); @@ -166,16 +165,16 @@ if($isEditing) { } } - if(filter_has_var(INPUT_POST, 'sig_body')) { + if(isset($_POST['sig_body']) && is_scalar($_POST['sig_body'])) { if(!$perms->edit_signature) { $notices[] = "You're not allowed to edit your forum signature."; } else { - $sigBody = (string)filter_input(INPUT_POST, 'sig_body'); + $sigBody = (string)$_POST['sig_body']; if(trim($sigBody) === '') { $msz->forumCtx->signatures->deleteSignature($userInfo); $sigInfo = null; } else { - $sigFormat = TextFormat::tryFrom(filter_input(INPUT_POST, 'sig_format')); + $sigFormat = TextFormat::tryFrom(isset($_POST['sig_format']) && is_scalar($_POST['sig_format']) ? (string)$_POST['sig_format'] : ''); $sigValid = ForumSignaturesData::validateSignature($sigFormat, $sigBody); if($sigValid === '') $sigInfo = $msz->forumCtx->signatures->updateSignature($userInfo, $sigBody, $sigFormat); @@ -185,13 +184,13 @@ if($isEditing) { } } - if(!empty($_POST['birthdate']) && is_array($_POST['birthdate'])) { + if(!empty($_POST['birth_day']) && !empty($_POST['birth_month'])) { if(!$perms->edit_birthdate) { $notices[] = "You aren't allow to change your birthdate."; } else { - $birthYear = (int)($_POST['birthdate']['year'] ?? 0); - $birthMonth = (int)($_POST['birthdate']['month'] ?? 0); - $birthDay = (int)($_POST['birthdate']['day'] ?? 0); + $birthYear = (int)($_POST['birth_year'] ?? 0); + $birthMonth = (int)$_POST['birth_month']; + $birthDay = (int)$_POST['birth_day']; $birthValid = UserBirthdatesData::validateBirthdate($birthYear, $birthMonth, $birthDay); if($birthValid === '') { @@ -204,53 +203,39 @@ if($isEditing) { } } - if(!empty($_FILES['avatar'])) { - if(!empty($_POST['avatar']['delete'])) { - $avatarAsset->delete(); - } else { + if(!empty($_POST['avatar_delete'])) { + $avatarAsset->delete(); + } elseif(isset($mszRequestContent) && $mszRequestContent instanceof MultipartFormContent) { + $avatarInfo = $mszRequestContent->getParamData('avatar_file'); + if($avatarInfo instanceof FileMultipartFormData) { if(!$perms->edit_avatar) { $notices[] = "You aren't allow to change your avatar."; - } elseif(!empty($_FILES['avatar']) - && is_array($_FILES['avatar']) - && !empty($_FILES['avatar']['name']['file'])) { - if($_FILES['avatar']['error']['file'] !== UPLOAD_ERR_OK) { - switch($_FILES['avatar']['error']['file']) { - case UPLOAD_ERR_NO_FILE: - $notices[] = 'Select a file before hitting upload!'; - break; - case UPLOAD_ERR_PARTIAL: - $notices[] = 'The upload was interrupted, please try again!'; - break; - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - $notices[] = sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())); - break; - default: - $notices[] = 'Unable to save your avatar, contact an administator!'; - break; - } - } else { - try { - $avatarAsset->setFromPath($_FILES['avatar']['tmp_name']['file']); - } catch(InvalidArgumentException $ex) { - $exMessage = $ex->getMessage(); - $notices[] = match($exMessage) { - '$path is not a valid image.' => 'The file you uploaded was not an image!', - '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', - 'Dimensions of $path are too large.' => sprintf("Your avatar can't be larger than %dx%d!", $avatarAsset->getMaxWidth(), $avatarAsset->getMaxHeight()), - 'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())), - default => $exMessage, - }; - } catch(RuntimeException $ex) { - $notices[] = 'Unable to save your avatar, contact an administator!'; - } + } elseif($avatarInfo->getSize() > 0) { + $avatarTemp = tempnam(sys_get_temp_dir(), 'msz-legacy-avatar-'); + try { + $avatarInfo->moveTo($avatarTemp); + $avatarAsset->setFromPath($avatarTemp); + } catch(InvalidArgumentException $ex) { + $exMessage = $ex->getMessage(); + $notices[] = match($exMessage) { + '$path is not a valid image.' => 'The file you uploaded was not an image!', + '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', + 'Dimensions of $path are too large.' => sprintf("Your avatar can't be larger than %dx%d!", $avatarAsset->getMaxWidth(), $avatarAsset->getMaxHeight()), + 'File size of $path is too large.' => sprintf('Your avatar is not allowed to be larger in file size than %s!', ByteFormat::format($avatarAsset->getMaxBytes())), + default => $exMessage, + }; + } catch(RuntimeException $ex) { + $notices[] = 'Unable to save your avatar, contact an administator!'; + } finally { + if(is_file($avatarTemp)) + unlink($avatarTemp); } } } } - if(filter_has_var(INPUT_POST, 'bg_attach')) { - $bgFormat = ProfileBackgroundAttach::tryFrom((string)filter_input(INPUT_POST, 'bg_attach')); + if(isset($_POST['bg_attach']) && is_scalar($_POST['bg_attach'])) { + $bgFormat = ProfileBackgroundAttach::tryFrom((string)$_POST['bg_attach']); if($bgFormat === null) { $backgroundAsset->delete(); @@ -259,47 +244,35 @@ if($isEditing) { } else { if(!$perms->edit_background) { $notices[] = "You aren't allow to change your background."; - } elseif(!empty($_FILES['bg_file']) && is_array($_FILES['bg_file'])) { - if(!empty($_FILES['bg_file']['name'])) { - if($_FILES['bg_file']['error'] !== UPLOAD_ERR_OK) { - switch($_FILES['bg_file']['error']) { - case UPLOAD_ERR_NO_FILE: - $notices[] = 'Select a file before hitting upload!'; - break; - case UPLOAD_ERR_PARTIAL: - $notices[] = 'The upload was interrupted, please try again!'; - break; - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - $notices[] = sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format(isset($backgroundProps) && is_array($backgroundProps) ? $backgroundProps['max_size'] : 0)); - break; - default: - $notices[] = 'Unable to save your background, contact an administator!'; - break; - } - } else { - try { - $backgroundAsset->setFromPath($_FILES['bg_file']['tmp_name']); - } catch(InvalidArgumentException $ex) { - $exMessage = $ex->getMessage(); - $notices[] = match($exMessage) { - '$path is not a valid image.' => 'The file you uploaded was not an image!', - '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', - 'Dimensions of $path are too large.' => sprintf("Your background can't be larger than %dx%d!", $backgroundAsset->getMaxWidth(), $backgroundAsset->getMaxHeight()), - 'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundAsset->getMaxBytes())), - default => $exMessage, - }; - } catch(RuntimeException $ex) { - $notices[] = 'Unable to save your background, contact an administator!'; - } + } elseif(isset($mszRequestContent) && $mszRequestContent instanceof MultipartFormContent) { + $bgInfo = $mszRequestContent->getParamData('bg_file'); + if($bgInfo instanceof FileMultipartFormData && $bgInfo->getSize() > 0) { + $bgTemp = tempnam(sys_get_temp_dir(), 'msz-legacy-profile-background-'); + try { + $bgInfo->moveTo($bgTemp); + $backgroundAsset->setFromPath($bgTemp); + } catch(InvalidArgumentException $ex) { + $exMessage = $ex->getMessage(); + $notices[] = match($exMessage) { + '$path is not a valid image.' => 'The file you uploaded was not an image!', + '$path is not an allowed image file.' => 'This type of image is not supported, keep to PNG, JPG or GIF!', + 'Dimensions of $path are too large.' => sprintf("Your background can't be larger than %dx%d!", $backgroundAsset->getMaxWidth(), $backgroundAsset->getMaxHeight()), + 'File size of $path is too large.' => sprintf('Your background is not allowed to be larger in file size than %s!', ByteFormat::format($backgroundAsset->getMaxBytes())), + default => $exMessage, + }; + } catch(RuntimeException $ex) { + $notices[] = 'Unable to save your background, contact an administator!'; + } finally { + if(is_file($bgTemp)) + unlink($bgTemp); } } $backgroundInfo = $msz->profileCtx->backgrounds->updateProfileBackground( $userInfo, $bgFormat, - filter_has_var(INPUT_POST, 'bg_blend'), - filter_has_var(INPUT_POST, 'bg_slide') + !empty($_POST['bg_blend']), + !empty($_POST['bg_slide']) ); } } diff --git a/public-legacy/settings/sessions.php b/public-legacy/settings/sessions.php index b1e65883..90be2cd1 100644 --- a/public-legacy/settings/sessions.php +++ b/public-legacy/settings/sessions.php @@ -14,7 +14,7 @@ $currentUser = $msz->authInfo->userInfo; $activeSessionId = $msz->authInfo->sessionId; while($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) { - $sessionId = (string)filter_input(INPUT_POST, 'session'); + $sessionId = !empty($_POST['session']) && is_scalar($_POST['session']) ? trim((string)$_POST['session']) : ''; $activeSessionKilled = false; if($sessionId === 'all') { diff --git a/public/index.php b/public/index.php index 0085336d..6afe51d8 100644 --- a/public/index.php +++ b/public/index.php @@ -2,6 +2,9 @@ namespace Misuzu; use RuntimeException; +use Index\MediaType; +use Index\Http\Content\MultipartFormContent; +use Index\Http\Content\Multipart\ValueMultipartFormData; use Misuzu\Auth\{AuthTokenBuilder,AuthTokenCookie,AuthTokenInfo}; require_once __DIR__ . '/../misuzu.php'; @@ -25,12 +28,12 @@ $request = \Index\Http\HttpRequest::fromRequest(); $tokenPacker = $msz->authCtx->createAuthTokenPacker(); -if(filter_has_var(INPUT_COOKIE, 'msz_auth')) - $tokenInfo = $tokenPacker->unpack(filter_input(INPUT_COOKIE, 'msz_auth')); -elseif(filter_has_var(INPUT_COOKIE, 'msz_uid') && filter_has_var(INPUT_COOKIE, 'msz_sid')) { +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((string)filter_input(INPUT_COOKIE, 'msz_uid', FILTER_SANITIZE_NUMBER_INT)); - $tokenBuilder->setSessionToken((string)filter_input(INPUT_COOKIE, 'msz_sid')); + $tokenBuilder->setUserId($_COOKIE['msz_uid']); + $tokenBuilder->setSessionToken($_COOKIE['msz_sid']); $tokenInfo = $tokenBuilder->toInfo(); $tokenBuilder = null; } else @@ -39,7 +42,7 @@ elseif(filter_has_var(INPUT_COOKIE, 'msz_uid') && filter_has_var(INPUT_COOKIE, ' $userInfo = null; $sessionInfo = null; $userInfoReal = null; -$remoteAddr = (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'); +$remoteAddr = $_SERVER['REMOTE_ADDR']; if($tokenInfo->hasUserId && $tokenInfo->hasSessionToken) { $tokenBuilder = new AuthTokenBuilder($tokenInfo); @@ -112,7 +115,52 @@ $router = $msz->createRouting($request); $msz->startTemplating(); if($msz->domainRoles->hasRole($request->getHeaderLine('Host'), 'main')) { - $mszRequestPath = substr($request->path, 1); + // Reconstruct $_POST since PHP no longer makes it for us + if($request->getBody()->isReadable() && empty($_POST)) { + $mszRequestContent = (function($contentType, $stream) { + if($contentType->equals('application/x-www-form-urlencoded')) { + parse_str((string)$stream, $postVars); + return $postVars; + } + + if($contentType->equals('multipart/form-data')) + try { + return MultipartFormContent::parseStream($stream, $contentType->boundary); + } catch(RuntimeException $ex) {} + + return null; + })(MediaType::parse($request->getHeaderLine('Content-Type')), $request->getBody()); + + $_POST = (function($requestContent) { + if(is_array($requestContent)) + return $requestContent; + + if($requestContent instanceof MultipartFormContent) { + $postVars = []; + foreach($requestContent->params as $name => $values) { + if(count($values) === 0) + $postVars[$name] = ''; + elseif(count($values) === 1) + $postVars[$name] = $values[0] instanceof ValueMultipartFormData ? (string)$values[0] : ''; + else { + $postVar = []; + foreach($values as $value) + $postVars[] = $value instanceof ValueMultipartFormData ? (string)$value : ''; + + $postVars[$name] = $postVar; + } + } + + return $postVars; + } + + return []; + })($mszRequestContent); + + $_REQUEST = array_merge($_GET, $_POST); + } + + $mszRequestPath = substr($request->requestTarget, 1); $mszLegacyPathPrefix = Misuzu::PATH_PUBLIC_LEGACY . '/'; $mszLegacyPath = $mszLegacyPathPrefix . $mszRequestPath; diff --git a/src/Auth/AuthTokenCookie.php b/src/Auth/AuthTokenCookie.php index 21c32663..dd2ceba0 100644 --- a/src/Auth/AuthTokenCookie.php +++ b/src/Auth/AuthTokenCookie.php @@ -26,7 +26,7 @@ final class AuthTokenCookie { $threeMonths->format('D, d M Y H:i:s e'), $threeMonths->getTimestamp() - $now->getTimestamp(), self::domain(), - filter_has_var(INPUT_SERVER, 'HTTPS') ? ' Secure' : '' + !empty($_SERVER['HTTPS']) ? ' Secure' : '' )); } @@ -34,7 +34,7 @@ final class AuthTokenCookie { header(sprintf( 'Set-Cookie: msz_auth=; Expires=Wed, 31 Dec 1969 21:29:59 UTC; Max-Age=-9001; Domain=%s; Path=/; SameSite=Lax; HttpOnly;%s', self::domain(), - filter_has_var(INPUT_SERVER, 'HTTPS') ? ' Secure' : '' + !empty($_SERVER['HTTPS']) ? ' Secure' : '' )); } } diff --git a/src/CSRF.php b/src/CSRF.php index 96f542fb..8a7d76d8 100644 --- a/src/CSRF.php +++ b/src/CSRF.php @@ -36,9 +36,9 @@ final class CSRF { if(self::$instance === null) return false; - $token = (string)filter_input(INPUT_POST, '_csrf'); + $token = isset($_POST['_csrf']) && is_string($_POST['_csrf']) ? $_POST['_csrf'] : ''; if(empty($token)) - $token = (string)filter_input(INPUT_GET, 'csrf'); + $token = isset($_GET['csrf']) && is_string($_GET['csrf']) ? $_GET['csrf'] : ''; return self::$instance->verifyToken($token, $tolerance); } diff --git a/src/Changelog/ChangelogRoutes.php b/src/Changelog/ChangelogRoutes.php index 4ba4d941..b5356f1d 100644 --- a/src/Changelog/ChangelogRoutes.php +++ b/src/Changelog/ChangelogRoutes.php @@ -4,7 +4,8 @@ namespace Misuzu\Changelog; use ErrorException; use RuntimeException; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Syndication\FeedBuilder; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\{Pagination,SiteInfo,Template}; @@ -22,11 +23,11 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { private CommentsContext $commentsCtx, ) {} - #[HttpGet('/changelog')] + #[ExactRoute('GET', '/changelog')] #[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'); - $filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); + $filterUser = (string)$request->getFilteredParam('user', FILTER_SANITIZE_NUMBER_INT); $filterTags = (string)$request->getParam('tags'); if(empty($filterDate)) @@ -96,7 +97,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/changelog/change/([0-9]+)')] + #[PatternRoute('GET', '/changelog/change/([0-9]+)')] #[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 { @@ -120,7 +121,7 @@ final class ChangelogRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/changelog.(xml|rss|atom)')] + #[PatternRoute('GET', '/changelog.(xml|rss|atom)')] #[UrlFormat('changelog-feed', '/changelog.xml')] public function getFeed(HttpResponseBuilder $response): string { $response->setContentType('application/rss+xml; charset=utf-8'); diff --git a/src/Comments/CommentsRoutes.php b/src/Comments/CommentsRoutes.php index dc1b0976..791e61cc 100644 --- a/src/Comments/CommentsRoutes.php +++ b/src/Comments/CommentsRoutes.php @@ -4,7 +4,11 @@ namespace Misuzu\Comments; use RuntimeException; use Index\XArray; use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpDelete,HttpGet,HttpMiddleware,HttpPatch,HttpPost,RouteHandler,RouteHandlerCommon}; +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\Auth\AuthInfo; @@ -186,7 +190,7 @@ class CommentsRoutes implements RouteHandler, UrlSource { } /** @return void|array{error: array{name: string, text: string}} */ - #[HttpMiddleware('/comments')] + #[PrefixFilter('/comments')] public function checkCsrf(HttpResponseBuilder $response, HttpRequest $request) { if(in_array($request->method, ['DELETE', 'PATCH', 'POST'])) { if(!$this->authInfo->loggedIn) @@ -228,8 +232,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { * posts: mixed[] * }|array{error: array{name: string, text: string}} */ - #[HttpGet('/comments/categories/([A-Za-z0-9-]+)')] - public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array { + #[PatternRoute('GET', '/comments/categories/([A-Za-z0-9-]+)')] + public function getCategory(HttpResponseBuilder $response, string $categoryName): array { try { $catInfo = $this->commentsCtx->categories->getCategory(name: $categoryName); } catch(RuntimeException $ex) { @@ -290,11 +294,9 @@ class CommentsRoutes implements RouteHandler, UrlSource { * locked?: string|false, * }|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/categories/([A-Za-z0-9-]+)')] - public function patchCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryName): array { - if(!($request->content instanceof FormHttpContent)) - return self::error($response, 400, 'comments:content', 'Provided content could not be understood.'); - + #[PatternRoute('POST', '/comments/categories/([A-Za-z0-9-]+)')] + #[Before('input:urlencoded')] + public function patchCategory(HttpResponseBuilder $response, FormContent $content, string $categoryName): array { try { $catInfo = $this->commentsCtx->categories->getCategory(name: $categoryName); } catch(RuntimeException $ex) { @@ -304,11 +306,11 @@ class CommentsRoutes implements RouteHandler, UrlSource { $perms = $this->getGlobalPerms(); $locked = null; - if($request->content->hasParam('lock')) { + if($content->hasParam('lock')) { if(!$perms->check(Perm::G_COMMENTS_LOCK)) return self::error($response, 403, 'comments:lock-not-allowed', 'You are not allowed to lock this comment section.'); - $locked = !empty($request->content->getParam('lock')); + $locked = !empty($content->getParam('lock')); } $this->commentsCtx->categories->updateCategory( @@ -332,34 +334,32 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return mixed[]|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/posts')] - public function postPost(HttpResponseBuilder $response, HttpRequest $request): array { - if(!($request->content instanceof FormHttpContent)) - return self::error($response, 400, 'comments:content', 'Provided content could not be understood.'); - + #[ExactRoute('POST', '/comments/posts')] + #[Before('input:multipart')] + public function postPost(HttpResponseBuilder $response, FormContent $content): array { $perms = $this->getGlobalPerms(); if(!$perms->check(Perm::G_COMMENTS_CREATE)) return self::error($response, 403, 'comments:create-not-allowed', 'You are not allowed to post comments.'); - if(!$request->content->hasParam('category') || !$request->content->hasParam('body')) + if(!$content->hasParam('category') || !$content->hasParam('body')) return self::error($response, 400, 'comments:missing-fields', 'Required fields are not specified.'); $pinned = false; - $body = preg_replace("/[\r\n]{2,}/", "\n", (string)$request->content->getParam('body')); + $body = preg_replace("/[\r\n]{2,}/", "\n", (string)$content->getParam('body')); if(mb_strlen(mb_trim($body)) < 1) return self::error($response, 400, 'comments:body-too-short', 'Your comment must be longer.'); if(mb_strlen($body) > 5000) return self::error($response, 400, 'comments:body-too-long', 'Your comment is too long.'); try { - $catInfo = $this->commentsCtx->categories->getCategory(name: (string)$request->content->getParam('category')); + $catInfo = $this->commentsCtx->categories->getCategory(name: (string)$content->getParam('category')); } catch(RuntimeException $ex) { return self::error($response, 404, 'comments:category-not-found', 'No comment section with that name exists.'); } - if($request->content->hasParam('reply_to')) { + if($content->hasParam('reply_to')) { try { - $replyToInfo = $this->commentsCtx->posts->getPost((string)$request->content->getParam('reply_to')); + $replyToInfo = $this->commentsCtx->posts->getPost((string)$content->getParam('reply_to')); if($replyToInfo->deleted) return self::error($response, 404, 'comments:parent-not-found', 'The comment you are trying to reply to does not exist.'); } catch(RuntimeException $ex) { @@ -368,13 +368,13 @@ class CommentsRoutes implements RouteHandler, UrlSource { } else $replyToInfo = null; - if($request->content->hasParam('pin')) { + if($content->hasParam('pin')) { if(!$perms->check(Perm::G_COMMENTS_PIN) && $catInfo->ownerId !== $this->authInfo->userId) return self::error($response, 403, 'comments:pin-not-allowed', 'You are not allowed to pin comments.'); if($replyToInfo !== null) return self::error($response, 400, 'comments:post-not-root', 'Replies cannot be pinned.'); - $pinned = !empty($request->content->getParam('pin')); + $pinned = !empty($content->getParam('pin')); } try { @@ -396,8 +396,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return mixed[]|array{error: array{name: string, text: string}} */ - #[HttpGet('/comments/posts/([0-9]+)')] - public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { + #[PatternRoute('GET', '/comments/posts/([0-9]+)')] + public function getPost(HttpResponseBuilder $response, string $commentId): array { try { $postInfo = $this->commentsCtx->posts->getPost($commentId); $catInfo = $this->commentsCtx->categories->getCategory(postInfo: $postInfo); @@ -421,8 +421,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return mixed[]|array{error: array{name: string, text: string}} */ - #[HttpGet('/comments/posts/([0-9]+)/replies')] - public function getPostReplies(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { + #[PatternRoute('GET', '/comments/posts/([0-9]+)/replies')] + public function getPostReplies(HttpResponseBuilder $response, string $commentId): array { try { $postInfo = $this->commentsCtx->posts->getPost($commentId); $catInfo = $this->commentsCtx->categories->getCategory(postInfo: $postInfo); @@ -445,13 +445,9 @@ class CommentsRoutes implements RouteHandler, UrlSource { * edited?: string, * }|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/posts/([0-9]+)')] - // this should be HttpPatch but PHP doesn't parse into $_POST for PATCH... - // fix this in the v3 router for index by just ignoring PHP's parsing altogether - public function patchPost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { - if(!($request->content instanceof FormHttpContent)) - return self::error($response, 400, 'comments:content', 'Provided content could not be understood.'); - + #[PatternRoute('PATCH', '/comments/posts/([0-9]+)')] + #[Before('input:multipart')] + public function patchPost(HttpResponseBuilder $response, FormContent $content, string $commentId): array { try { $postInfo = $this->commentsCtx->posts->getPost($commentId); $catInfo = $this->commentsCtx->categories->getCategory(postInfo: $postInfo); @@ -467,20 +463,20 @@ class CommentsRoutes implements RouteHandler, UrlSource { $pinned = null; $edited = false; - if($request->content->hasParam('pin')) { + if($content->hasParam('pin')) { if(!$perms->check(Perm::G_COMMENTS_PIN) && $catInfo->ownerId !== $this->authInfo->userId) return self::error($response, 403, 'comments:pin-not-allowed', 'You are not allowed to pin comments.'); if($postInfo->reply) return self::error($response, 400, 'comments:post-not-root', 'Replies cannot be pinned.'); - $pinned = !empty($request->content->getParam('pin')); + $pinned = !empty($content->getParam('pin')); } - if($request->content->hasParam('body')) { + if($content->hasParam('body')) { if(!$perms->check(Perm::G_COMMENTS_EDIT_ANY) && !($perms->check(Perm::G_COMMENTS_EDIT_OWN) && $this->authInfo->userId === $postInfo->userId)) return self::error($response, 403, 'comments:edit-not-allowed', 'You are not allowed to edit comments.'); - $body = preg_replace("/[\r\n]{2,}/", "\n", (string)$request->content->getParam('body')); + $body = preg_replace("/[\r\n]{2,}/", "\n", (string)$content->getParam('body')); if(mb_strlen(mb_trim($body)) < 1) return self::error($response, 400, 'comments:body-too-short', 'Your comment must be longer.'); if(mb_strlen($body) > 5000) @@ -518,8 +514,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return string|array{error: array{name: string, text: string}} */ - #[HttpDelete('/comments/posts/([0-9]+)')] - public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array|string { + #[PatternRoute('DELETE', '/comments/posts/([0-9]+)')] + public function deletePost(HttpResponseBuilder $response, string $commentId): array|string { try { $postInfo = $this->commentsCtx->posts->getPost($commentId); if($postInfo->deleted) @@ -547,8 +543,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return mixed[]|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/posts/([0-9]+)/restore')] - public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { + #[PatternRoute('POST', '/comments/posts/([0-9]+)/restore')] + 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.'); @@ -572,8 +568,8 @@ class CommentsRoutes implements RouteHandler, UrlSource { /** * @return mixed[]|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/posts/([0-9]+)/nuke')] - public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { + #[PatternRoute('POST' ,'/comments/posts/([0-9]+)/nuke')] + 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.'); @@ -601,12 +597,10 @@ class CommentsRoutes implements RouteHandler, UrlSource { * negative: int, * }|array{error: array{name: string, text: string}} */ - #[HttpPost('/comments/posts/([0-9]+)/vote')] - public function postPostVote(HttpResponseBuilder $response, HttpRequest $request, string $commentId): array { - if(!($request->content instanceof FormHttpContent)) - return self::error($response, 400, 'comments:content', 'Provided content could not be understood.'); - - $vote = (int)$request->content->getParam('vote', FILTER_SANITIZE_NUMBER_INT); + #[PatternRoute('POST', '/comments/posts/([0-9]+)/vote')] + #[Before('input:urlencoded')] + public function postPostVote(HttpResponseBuilder $response, FormContent $content, string $commentId): array { + $vote = (int)$content->getFilteredParam('vote', FILTER_SANITIZE_NUMBER_INT); if($vote === 0) return self::error($response, 400, 'comments:vote', 'Could not process vote.'); @@ -649,7 +643,7 @@ class CommentsRoutes implements RouteHandler, UrlSource { * negative: int, * }|array{error: array{name: string, text: string}} */ - #[HttpDelete('/comments/posts/([0-9]+)/vote')] + #[PatternRoute('DELETE', '/comments/posts/([0-9]+)/vote')] 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.'); diff --git a/src/DatabaseContext.php b/src/DatabaseContext.php index 83db6ff3..75420785 100644 --- a/src/DatabaseContext.php +++ b/src/DatabaseContext.php @@ -4,7 +4,8 @@ namespace Misuzu; use Index\Config\Config; use Index\Db\{DbBackends,DbConnection}; use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo}; -use Index\Http\Routing\{HttpMiddleware,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Filters\PrefixFilter; class DatabaseContext implements RouteHandler { use RouteHandlerCommon; @@ -40,8 +41,8 @@ class DatabaseContext implements RouteHandler { } /** @return void|int */ - #[HttpMiddleware('/')] - public function middleware() { + #[PrefixFilter('/')] + public function filterMigrate() { if(is_file($this->getMigrateLockPath())) return 503; } diff --git a/src/Forum/ForumCategoriesRoutes.php b/src/Forum/ForumCategoriesRoutes.php index d83c80db..ab7de7c9 100644 --- a/src/Forum/ForumCategoriesRoutes.php +++ b/src/Forum/ForumCategoriesRoutes.php @@ -4,7 +4,8 @@ namespace Misuzu\Forum; use stdClass; use RuntimeException; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon}; use Misuzu\{CSRF,Pagination,Perm,Template}; use Misuzu\Auth\AuthInfo; @@ -19,7 +20,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource { private AuthInfo $authInfo, ) {} - #[HttpGet('/forum')] + #[ExactRoute('GET', '/forum')] #[UrlFormat('forum-index', '/forum')] #[UrlFormat('forum-category-root', '/forum', fragment: '<forum>')] public function getIndex(): string { @@ -170,7 +171,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/forum/([0-9]+)')] + #[PatternRoute('GET', '/forum/([0-9]+)')] #[UrlFormat('forum-category', '/forum/<forum>', ['page' => '<page>'])] public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $catId): mixed { try { @@ -328,7 +329,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource { ]); } - #[HttpPost('/forum/mark-as-read')] + #[ExactRoute('POST', '/forum/mark-as-read')] #[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) @@ -338,7 +339,7 @@ class ForumCategoriesRoutes implements RouteHandler, UrlSource { return 403; $response->setHeader('X-CSRF-Token', CSRF::token()); - $catId = (string)$request->getParam('cat', FILTER_SANITIZE_NUMBER_INT); + $catId = (string)$request->getFilteredParam('cat', FILTER_SANITIZE_NUMBER_INT); $recursive = !empty($request->getParam('rec')); if($catId === '') { diff --git a/src/Forum/ForumPostsRoutes.php b/src/Forum/ForumPostsRoutes.php index 8a64eb6b..077db87e 100644 --- a/src/Forum/ForumPostsRoutes.php +++ b/src/Forum/ForumPostsRoutes.php @@ -3,7 +3,8 @@ namespace Misuzu\Forum; use RuntimeException; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpDelete,HttpGet,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\PatternRoute; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\{CSRF,Perm}; use Misuzu\AuditLog\AuditLogData; @@ -21,7 +22,7 @@ class ForumPostsRoutes implements RouteHandler, UrlSource { private AuthInfo $authInfo, ) {} - #[HttpGet('/forum/posts/([0-9]+)')] + #[PatternRoute('GET', '/forum/posts/([0-9]+)')] #[UrlFormat('forum-post', '/forum/posts/<post>')] public function getPost(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed { try { @@ -54,7 +55,7 @@ class ForumPostsRoutes implements RouteHandler, UrlSource { return 302; } - #[HttpDelete('/forum/posts/([0-9]+)')] + #[PatternRoute('DELETE', '/forum/posts/([0-9]+)')] #[UrlFormat('forum-post-delete', '/forum/posts/<post>')] public function deletePost(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed { if(!$this->authInfo->loggedIn) @@ -189,7 +190,7 @@ class ForumPostsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/posts/([0-9]+)/nuke')] + #[PatternRoute('POST', '/forum/posts/([0-9]+)/nuke')] #[UrlFormat('forum-post-nuke', '/forum/posts/<post>/nuke')] public function postPostNuke(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed { if(!$this->authInfo->loggedIn) @@ -269,7 +270,7 @@ class ForumPostsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/posts/([0-9]+)/restore')] + #[PatternRoute('POST', '/forum/posts/([0-9]+)/restore')] #[UrlFormat('forum-post-restore', '/forum/posts/<post>/restore')] public function postPostRestore(HttpResponseBuilder $response, HttpRequest $request, string $postId): mixed { if(!$this->authInfo->loggedIn) diff --git a/src/Forum/ForumTopicsRoutes.php b/src/Forum/ForumTopicsRoutes.php index cf616d78..1643a144 100644 --- a/src/Forum/ForumTopicsRoutes.php +++ b/src/Forum/ForumTopicsRoutes.php @@ -4,7 +4,9 @@ namespace Misuzu\Forum; use stdClass; use RuntimeException; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpDelete,HttpGet,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\Content\FormContent; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\PatternRoute; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\{CSRF,Pagination,Perm,Template}; use Misuzu\AuditLog\AuditLogData; @@ -21,7 +23,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { private AuthInfo $authInfo, ) {} - #[HttpGet('/forum/topics/([0-9]+)')] + #[PatternRoute('GET', '/forum/topics/([0-9]+)')] #[UrlFormat('forum-topic', '/forum/topics/<topic>', ['page' => '<page>'], '<topic_fragment>')] public function getTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { $isNuked = $deleted = $canDeleteAny = false; @@ -152,7 +154,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { ]); } - #[HttpDelete('/forum/topics/([0-9]+)')] + #[PatternRoute('DELETE', '/forum/topics/([0-9]+)')] #[UrlFormat('forum-topic-delete', '/forum/topics/<topic>')] public function deleteTopic(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) @@ -272,7 +274,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/topics/([0-9]+)/restore')] + #[PatternRoute('POST', '/forum/topics/([0-9]+)/restore')] #[UrlFormat('forum-topic-restore', '/forum/topics/<topic>/restore')] public function postTopicRestore(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) @@ -352,7 +354,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/topics/([0-9]+)/nuke')] + #[PatternRoute('POST', '/forum/topics/([0-9]+)/nuke')] #[UrlFormat('forum-topic-nuke', '/forum/topics/<topic>/nuke')] public function postTopicNuke(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) @@ -432,7 +434,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/topics/([0-9]+)/bump')] + #[PatternRoute('POST', '/forum/topics/([0-9]+)/bump')] #[UrlFormat('forum-topic-bump', '/forum/topics/<topic>/bump')] public function postTopicBump(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) @@ -512,7 +514,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/topics/([0-9]+)/lock')] + #[PatternRoute('POST', '/forum/topics/([0-9]+)/lock')] #[UrlFormat('forum-topic-lock', '/forum/topics/<topic>/lock')] public function postTopicLock(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) @@ -602,7 +604,7 @@ class ForumTopicsRoutes implements RouteHandler, UrlSource { return 204; } - #[HttpPost('/forum/topics/([0-9]+)/unlock')] + #[PatternRoute('POST', '/forum/topics/([0-9]+)/unlock')] #[UrlFormat('forum-topic-unlock', '/forum/topics/<topic>/unlock')] public function postTopicUnlock(HttpResponseBuilder $response, HttpRequest $request, string $topicId): mixed { if(!$this->authInfo->loggedIn) diff --git a/src/Home/HomeRoutes.php b/src/Home/HomeRoutes.php index 9b9cdbf8..df374802 100644 --- a/src/Home/HomeRoutes.php +++ b/src/Home/HomeRoutes.php @@ -7,7 +7,8 @@ use Index\Config\Config; use Index\Colour\Colour; use Index\Db\{DbConnection,DbTools}; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\ExactRoute; use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon}; use Misuzu\{Pagination,SiteInfo,Template}; use Misuzu\Auth\AuthInfo; @@ -198,12 +199,10 @@ class HomeRoutes implements RouteHandler, UrlSource { return $topics; } - #[HttpGet('/')] + #[ExactRoute('GET', '/')] #[UrlFormat('index', '/')] - public function getIndex(HttpResponseBuilder $response, HttpRequest $request): string { - return $this->authInfo->loggedIn - ? $this->getHome() - : $this->getLanding($response, $request); + public function getIndex(): string { + return $this->authInfo->loggedIn ? $this->getHome() : $this->getLanding(); } public function getHome(): string { @@ -246,8 +245,8 @@ class HomeRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/_landing')] - public function getLanding(HttpResponseBuilder $response, HttpRequest $request): string { + #[ExactRoute('GET', '/_landing')] + public function getLanding(): string { $config = $this->config->getValues([ ['social.embed_linked:b'], ['landing.forum_categories:a'], diff --git a/src/Info/InfoRoutes.php b/src/Info/InfoRoutes.php index e191bbc0..002f68fe 100644 --- a/src/Info/InfoRoutes.php +++ b/src/Info/InfoRoutes.php @@ -3,7 +3,8 @@ namespace Misuzu\Info; use Index\Index; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Urls\{UrlFormat,UrlSource,UrlSourceCommon}; use Misuzu\{Misuzu,Template}; use Misuzu\Parsers\{Parsers,TextFormat}; @@ -23,13 +24,13 @@ class InfoRoutes implements RouteHandler, UrlSource { 'rpcii' => 'RPCii » %s', ]; - #[HttpGet('/info')] + #[ExactRoute('GET', '/info')] #[UrlFormat('info-index', '/info')] public function getIndex(): string { return Template::renderRaw('info.index'); } - #[HttpGet('/info/([A-Za-z0-9_]+)')] + #[PatternRoute('GET', '/info/([A-Za-z0-9_]+)')] #[UrlFormat('info', '/info/<title>')] #[UrlFormat('info-doc', '/info/<title>')] public function getDocsPage(HttpResponseBuilder $response, HttpRequest $request, string $name): string { @@ -76,7 +77,7 @@ class InfoRoutes implements RouteHandler, UrlSource { return ''; } - #[HttpGet('/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] + #[PatternRoute('GET', '/info/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] #[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)) diff --git a/src/LegacyRoutes.php b/src/LegacyRoutes.php index 1f901915..0c4d4b9c 100644 --- a/src/LegacyRoutes.php +++ b/src/LegacyRoutes.php @@ -3,6 +3,7 @@ namespace Misuzu; use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource}; class LegacyRoutes implements RouteHandler, UrlSource { @@ -110,126 +111,123 @@ class LegacyRoutes implements RouteHandler, UrlSource { $urls->register('manage-role', '/manage/users/role.php', ['r' => '<role>']); } - #[HttpGet('/index.php')] + #[ExactRoute('GET', '/index.php')] public function getIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('index'), true); } - #[HttpGet('/info.php')] + #[ExactRoute('GET', '/info.php')] public function getInfoPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('info'), true); } - #[HttpGet('/info.php/([A-Za-z0-9_]+)')] + #[PatternRoute('GET', '/info.php/([A-Za-z0-9_]+)')] public function getInfoDocsPHP(HttpResponseBuilder $response, HttpRequest $request, string $name): void { $response->redirect($this->urls->format('info', ['title' => $name]), true); } - #[HttpGet('/info.php/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] + #[PatternRoute('GET', '/info.php/([A-Za-z0-9_]+)/([A-Za-z0-9_]+)')] public function getInfoProjectPHP(HttpResponseBuilder $response, HttpRequest $request, string $project, string $name): void { $response->redirect($this->urls->format('info', ['title' => $project . '/' . $name]), true); } - #[HttpGet('/news.php')] + #[ExactRoute('GET', '/news.php')] public function getNewsPHP(HttpResponseBuilder $response, HttpRequest $request): void { - $postId = (int)($request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT)); + $postId = (int)($request->getFilteredParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT)); if($postId > 0) $location = $this->urls->format('news-post', ['post' => $postId]); else { - $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT); - $pageId = $request->getParam('page', FILTER_SANITIZE_NUMBER_INT); + $catId = (int)$request->getFilteredParam('c', FILTER_SANITIZE_NUMBER_INT); + $pageId = $request->getFilteredParam('page', FILTER_SANITIZE_NUMBER_INT); $location = $this->urls->format($catId > 0 ? 'news-category' : 'news-index', ['category' => $catId, 'page' => $pageId]); } $response->redirect($location, true); } - #[HttpGet('/news/index.php')] + #[ExactRoute('GET', '/news/index.php')] public function getNewsIndexPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('news-index', [ - 'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT), + 'page' => $request->getFilteredParam('page', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/news/category.php')] + #[ExactRoute('GET', '/news/category.php')] public function getNewsCategoryPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('news-category', [ - 'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT), - 'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), + 'category' => $request->getFilteredParam('c', FILTER_SANITIZE_NUMBER_INT), + 'page' => $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/news/post.php')] + #[ExactRoute('GET', '/news/post.php')] public function getNewsPostPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('news-post', [ - 'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), + 'post' => $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/news.php/rss')] - #[HttpGet('/news.php/atom')] - #[HttpGet('/news/feed.php')] - #[HttpGet('/news/feed.php/rss')] - #[HttpGet('/news/feed.php/atom')] + #[PatternRoute('GET', '/news.php/(?:atom|rss)')] + #[PatternRoute('GET', '/news/feed.php(?:/(?:atom|rss)?)?')] public function getNewsFeedPHP(HttpResponseBuilder $response, HttpRequest $request): void { - $catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT); + $catId = (int)$request->getFilteredParam('c', FILTER_SANITIZE_NUMBER_INT); $response->redirect($this->urls->format( $catId > 0 ? 'news-category-feed' : 'news-feed', ['category' => $catId] ), true); } - #[HttpGet('/forum/index.php')] + #[ExactRoute('GET', '/forum/index.php')] public function getForumIndexPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('forum-index'), true); } - #[HttpGet('/forum/forum.php')] + #[ExactRoute('GET', '/forum/forum.php')] public function getForumForumPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('forum-category', [ - 'forum' => $request->getParam('f', FILTER_SANITIZE_NUMBER_INT), - 'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), + 'forum' => $request->getFilteredParam('f', FILTER_SANITIZE_NUMBER_INT), + 'page' => $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/forum/topic.php')] + #[ExactRoute('GET', '/forum/topic.php')] public function getForumTopicPHP(HttpResponseBuilder $response, HttpRequest $request): void { if($request->hasParam('p')) $response->redirect($this->urls->format('forum-post', [ - 'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), + 'post' => $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); else $response->redirect($this->urls->format('forum-topic', [ - 'topic' => $request->getParam('t', FILTER_SANITIZE_NUMBER_INT), - 'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT), + 'topic' => $request->getFilteredParam('t', FILTER_SANITIZE_NUMBER_INT), + 'page' => $request->getFilteredParam('page', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/forum/post.php')] + #[ExactRoute('GET', '/forum/post.php')] public function getForumPostPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('forum-post', [ - 'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT), + 'post' => $request->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/changelog.php')] + #[ExactRoute('GET', '/changelog.php')] public function getChangelogPHP(HttpResponseBuilder $response, HttpRequest $request): void { - $changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT); + $changeId = $request->getFilteredParam('c', FILTER_SANITIZE_NUMBER_INT); if($changeId) { $response->redirect($this->urls->format('changelog-change', ['change' => $changeId]), true); return; } $response->redirect($this->urls->format('changelog-index', [ - 'date' => $request->getParam('d'), - 'user' => $request->getParam('u', FILTER_SANITIZE_NUMBER_INT), + 'date' => $request->getFilteredParam('d'), + 'user' => $request->getFilteredParam('u', FILTER_SANITIZE_NUMBER_INT), ]), true); } - #[HttpGet('/auth.php')] + #[ExactRoute('GET', '/auth.php')] public function getAuthPHP(HttpResponseBuilder $response, HttpRequest $request): void { - $response->redirect($this->urls->format(match($request->getParam('m')) { + $response->redirect($this->urls->format(match($request->getFilteredParam('m')) { 'logout' => 'auth-logout', 'reset' => 'auth-reset', 'forgot' => 'auth-forgot', @@ -238,44 +236,44 @@ class LegacyRoutes implements RouteHandler, UrlSource { }), true); } - #[HttpGet('/auth')] - #[HttpGet('/auth/index.php')] + #[ExactRoute('GET', '/auth')] + #[ExactRoute('GET', '/auth/index.php')] public function getAuthIndexPHP(HttpResponseBuilder $response, HttpRequest $request): void { $response->redirect($this->urls->format('auth-login'), true); } - #[HttpGet('/settings.php')] + #[ExactRoute('GET', '/settings.php')] public function getSettingsPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-index'), true); } - #[HttpGet('/settings')] + #[ExactRoute('GET', '/settings')] public function getSettingsIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-account')); } - #[HttpGet('/settings/index.php')] + #[ExactRoute('GET', '/settings/index.php')] public function getSettingsIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('settings-account'), true); } - #[HttpGet('/manage')] + #[ExactRoute('GET', '/manage')] #[UrlFormat('manage-index', '/manage')] public function getManageIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-general-overview')); } - #[HttpGet('/manage/index.php')] + #[ExactRoute('GET', '/manage/index.php')] public function getManageIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-general-overview'), true); } - #[HttpGet('/manage/news')] + #[ExactRoute('GET', '/manage/news')] public function getManageNewsIndex(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-news-categories')); } - #[HttpGet('/manage/news/index.php')] + #[ExactRoute('GET', '/manage/news/index.php')] public function getManageNewsIndexPHP(HttpResponseBuilder $response): void { $response->redirect($this->urls->format('manage-news-categories'), true); } diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php index be47d717..6adc5d60 100644 --- a/src/Messages/MessagesRoutes.php +++ b/src/Messages/MessagesRoutes.php @@ -8,7 +8,11 @@ use Index\XString; use Index\Config\Config; use Index\Colour\Colour; use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerCommon}; +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\Auth\AuthInfo; @@ -38,7 +42,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { private bool $canSendMessages; /** @return void|int|array{error: array{name: string, text: string}} */ - #[HttpMiddleware('/messages')] + #[PrefixFilter('/messages')] public function checkAccess(HttpResponseBuilder $response, HttpRequest $request) { // should probably be a permission or something too if(!$this->authInfo->loggedIn) @@ -56,9 +60,6 @@ class MessagesRoutes implements RouteHandler, UrlSource { && !$this->usersCtx->hasActiveBan($this->authInfo->userInfo); if($request->method === 'POST') { - if(!($request->content instanceof FormHttpContent)) - return 400; - if(!CSRF::validate($request->getHeaderLine('x-csrf-token'))) return [ 'error' => [ @@ -92,9 +93,9 @@ class MessagesRoutes implements RouteHandler, UrlSource { return $message; } - #[HttpGet('/messages')] + #[ExactRoute('GET', '/messages')] #[UrlFormat('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])] - public function getIndex(HttpResponseBuilder $response, HttpRequest $request, string $folderName = ''): int|string { + public function getIndex(HttpRequest $request, string $folderName = ''): int|string { $folderName = (string)$request->getParam('folder'); if($folderName === '') $folderName = 'inbox'; @@ -146,7 +147,7 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return array{unread: int} */ - #[HttpGet('/messages/stats')] + #[ExactRoute('GET', '/messages/stats')] #[UrlFormat('messages-stats', '/messages/stats')] public function getStats(): array { $selfInfo = $this->authInfo->userInfo; @@ -171,16 +172,14 @@ class MessagesRoutes implements RouteHandler, UrlSource { * avatar: string * } */ - #[HttpPost('/messages/recipient')] + #[ExactRoute('POST', '/messages/recipient')] + #[Before('input:urlencoded')] #[UrlFormat('messages-recipient', '/messages/recipient')] - public function postRecipient(HttpResponseBuilder $response, HttpRequest $request): int|array { + public function postRecipient(FormContent $content): int|array { if(!$this->canSendMessages) return 403; - if(!($request->content instanceof FormHttpContent)) - return 400; - - $name = trim((string)$request->content->getParam('name')); + $name = trim((string)$content->getParam('name')); // flappy hacks if(str_starts_with(mb_strtolower($name), 'flappyzor')) @@ -211,9 +210,9 @@ class MessagesRoutes implements RouteHandler, UrlSource { ]; } - #[HttpGet('/messages/compose')] + #[ExactRoute('GET', '/messages/compose')] #[UrlFormat('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])] - public function getEditor(HttpResponseBuilder $response, HttpRequest $request): int|string { + public function getEditor(HttpRequest $request): int|string { if(!$this->canSendMessages) return 403; @@ -222,9 +221,9 @@ class MessagesRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/messages/([A-Za-z0-9]+)')] + #[PatternRoute('GET', '/messages/([A-Za-z0-9]+)')] #[UrlFormat('messages-view', '/messages/<message>')] - public function getView(HttpResponseBuilder $response, HttpRequest $request, string $messageId): int|string { + public function getView(string $messageId): int|string { if(strlen($messageId) !== 8) return 404; @@ -355,21 +354,19 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */ - #[HttpPost('/messages/create')] + #[ExactRoute('POST', '/messages/create')] + #[Before('input:multipart')] #[UrlFormat('messages-create', '/messages/create')] - public function postCreate(HttpResponseBuilder $response, HttpRequest $request): int|array { + public function postCreate(FormContent $content): int|array { if(!$this->canSendMessages) return 403; - if(!($request->content instanceof FormHttpContent)) - return 400; - - $recipient = (string)$request->content->getParam('recipient'); - $replyTo = (string)$request->content->getParam('reply'); - $title = (string)$request->content->getParam('title'); - $body = (string)$request->content->getParam('body'); - $format = TextFormat::tryFrom((string)$request->content->getParam('format')); - $draft = !empty($request->content->getParam('draft')); + $recipient = (string)$content->getParam('recipient'); + $replyTo = (string)$content->getParam('reply'); + $title = (string)$content->getParam('title'); + $body = (string)$content->getParam('body'); + $format = TextFormat::tryFrom((string)$content->getParam('format')); + $draft = !empty($content->getParam('draft')); $error = $this->checkMessageFields($title, $body, $format); if($error !== null) @@ -459,19 +456,17 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|array{id: string, url: string} */ - #[HttpPost('/messages/([A-Za-z0-9]+)')] + #[PatternRoute('PATCH', '/messages/([A-Za-z0-9]+)')] + #[Before('input:multipart')] #[UrlFormat('messages-update', '/messages/<message>')] - public function postUpdate(HttpResponseBuilder $response, HttpRequest $request, string $messageId): int|array { + public function patchUpdate(FormContent $content, string $messageId): int|array { if(!$this->canSendMessages) return 403; - if(!($request->content instanceof FormHttpContent)) - return 400; - - $title = (string)$request->content->getParam('title'); - $body = (string)$request->content->getParam('body'); - $format = TextFormat::tryFrom((string)$request->content->getParam('format')); - $draft = !empty($request->content->getParam('draft')); + $title = (string)$content->getParam('title'); + $body = (string)$content->getParam('body'); + $format = TextFormat::tryFrom((string)$content->getParam('format')); + $draft = !empty($content->getParam('draft')); $error = $this->checkMessageFields($title, $body, $format); if($error !== null) @@ -551,14 +546,12 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|scalar[] */ - #[HttpPost('/messages/mark')] + #[ExactRoute('POST', '/messages/mark')] + #[Before('input:urlencoded')] #[UrlFormat('messages-mark', '/messages/mark')] - public function postMark(HttpResponseBuilder $response, HttpRequest $request): int|array { - if(!($request->content instanceof FormHttpContent)) - return 400; - - $type = (string)$request->content->getParam('type'); - $messages = explode(',', (string)$request->content->getParam('messages')); + public function postMark(FormContent $content): int|array { + $type = (string)$content->getParam('type'); + $messages = explode(',', (string)$content->getParam('messages')); if($type !== 'read' && $type !== 'unread') return [ @@ -582,13 +575,11 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|scalar[] */ - #[HttpPost('/messages/delete')] + #[ExactRoute('POST', '/messages/delete')] + #[Before('input:urlencoded')] #[UrlFormat('messages-delete', '/messages/delete')] - public function postDelete(HttpResponseBuilder $response, HttpRequest $request): int|array { - if(!($request->content instanceof FormHttpContent)) - return 400; - - $messages = (string)$request->content->getParam('messages'); + public function postDelete(FormContent $content): int|array { + $messages = (string)$content->getParam('messages'); if($messages === '') return [ 'error' => [ @@ -608,13 +599,11 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|scalar[] */ - #[HttpPost('/messages/restore')] + #[ExactRoute('POST', '/messages/restore')] + #[Before('input:urlencoded')] #[UrlFormat('messages-restore', '/messages/restore')] - public function postRestore(HttpResponseBuilder $response, HttpRequest $request) { - if(!($request->content instanceof FormHttpContent)) - return 400; - - $messages = (string)$request->content->getParam('messages'); + public function postRestore(FormContent $content) { + $messages = (string)$content->getParam('messages'); if($messages === '') return [ 'error' => [ @@ -634,13 +623,11 @@ class MessagesRoutes implements RouteHandler, UrlSource { } /** @return int|array{error: array{name: string, text: string}}|scalar[] */ - #[HttpPost('/messages/nuke')] + #[ExactRoute('POST', '/messages/nuke')] + #[Before('input:urlencoded')] #[UrlFormat('messages-nuke', '/messages/nuke')] - public function postNuke(HttpResponseBuilder $response, HttpRequest $request) { - if(!($request->content instanceof FormHttpContent)) - return 400; - - $messages = (string)$request->content->getParam('messages'); + public function postNuke(FormContent $content) { + $messages = (string)$content->getParam('messages'); if($messages === '') return [ 'error' => [ diff --git a/src/MisuzuContext.php b/src/MisuzuContext.php index 3d5e6a79..ed374e37 100644 --- a/src/MisuzuContext.php +++ b/src/MisuzuContext.php @@ -9,7 +9,7 @@ use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo}; use Index\Http\HttpRequest; use Index\Templating\TplEnvironment; use Index\Urls\UrlRegistry; -use Misuzu\Routing\{BackedRoutingContext,RoutingContext}; +use Misuzu\Routing\RoutingContext; use Misuzu\Users\UserInfo; use RPCii\HmacVerificationProvider; use RPCii\Server\{HttpRpcServer,RpcServer}; @@ -139,36 +139,30 @@ class MisuzuContext { Template::init($this->templating); } - public function createRouting(HttpRequest $request): BackedRoutingContext { + public function createRouting(HttpRequest $request): RoutingContext { $host = $request->getHeaderLine('Host'); $roles = $this->domainRoles->getRoles($host); - $routingCtx = new BackedRoutingContext; + $routingCtx = new RoutingContext; $this->deps->register($this->urls = $routingCtx->urls); - $rpcServer = new HttpRpcServer; - $routingCtx->register($rpcServer->createRouteHandler( - new HmacVerificationProvider(fn() => $this->config->getString('aleister.secret')) - )); - if(in_array('main', $roles)) - $this->registerMainRoutes( - $routingCtx->scopeTo($this->domainRoles->getPrefix($host, 'main')), - $rpcServer - ); + $this->registerMainRoutes($routingCtx); if(in_array('redirect', $roles)) - $this->registerRedirectorRoutes( - $routingCtx->scopeTo($this->domainRoles->getPrefix($host, 'redirect')) - ); + $this->registerRedirectorRoutes($routingCtx); return $routingCtx; } public function registerMainRoutes( - RoutingContext $routingCtx, - RpcServer $rpcServer + RoutingContext $routingCtx ): void { + $rpcServer = new HttpRpcServer; + $routingCtx->register($rpcServer->createRouteHandler( + new HmacVerificationProvider(fn() => $this->config->getString('aleister.secret')) + )); + $this->deps->register($wf = new WebFinger\WebFingerRegistry); $wf->register($this->deps->constructLazy( Users\UsersWebFingerResolver::class, diff --git a/src/News/NewsRoutes.php b/src/News/NewsRoutes.php index 3448383f..f4805bee 100644 --- a/src/News/NewsRoutes.php +++ b/src/News/NewsRoutes.php @@ -4,7 +4,8 @@ namespace Misuzu\News; use RuntimeException; use Index\Colour\Colour; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Syndication\FeedBuilder; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\{Pagination,SiteInfo,Template}; @@ -98,7 +99,7 @@ class NewsRoutes implements RouteHandler, UrlSource { return $posts; } - #[HttpGet('/news')] + #[ExactRoute('GET', '/news')] #[UrlFormat('news-index', '/news', ['p' => '<page>'])] public function getIndex(HttpResponseBuilder $response, HttpRequest $request): int|string { $categories = $this->news->getCategories(hidden: false); @@ -116,7 +117,7 @@ class NewsRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/news/([0-9]+)(?:\.(xml|rss|atom))?')] + #[PatternRoute('GET', '/news/([0-9]+)(?:\.(xml|rss|atom))?')] #[UrlFormat('news-category', '/news/<category>', ['p' => '<page>'])] public function getCategory(HttpResponseBuilder $response, HttpRequest $request, string $categoryId, string $type = ''): int|string { try { @@ -141,7 +142,7 @@ class NewsRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/news/post/([0-9]+)')] + #[PatternRoute('GET', '/news/post/([0-9]+)')] #[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 { @@ -168,7 +169,7 @@ class NewsRoutes implements RouteHandler, UrlSource { ]); } - #[HttpGet('/news.(?:xml|rss|atom)')] + #[PatternRoute('GET', '/news.(?:xml|rss|atom)')] #[UrlFormat('news-feed', '/news.xml')] #[UrlFormat('news-category-feed', '/news/<category>.xml')] public function getFeed(HttpResponseBuilder $response, HttpRequest $request, ?NewsCategoryInfo $categoryInfo = null): string { diff --git a/src/OAuth2/OAuth2ApiRoutes.php b/src/OAuth2/OAuth2ApiRoutes.php index 141eb2f5..3d10765e 100644 --- a/src/OAuth2/OAuth2ApiRoutes.php +++ b/src/OAuth2/OAuth2ApiRoutes.php @@ -4,8 +4,12 @@ namespace Misuzu\OAuth2; use RuntimeException; use Index\XArray; use Index\Colour\{Colour,ColourRgb}; -use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest}; -use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\{HttpResponseBuilder,HttpRequest}; +use Index\Http\Content\FormContent; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\AccessControl\AccessControl; +use Index\Http\Routing\Processors\Before; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\SiteInfo; use Misuzu\Apps\AppsContext; @@ -52,20 +56,16 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { } /** - * @return int|array{ + * @return array{ * resource: string, * authorization_servers: string[], * scopes_supported: string[], * bearer_methods_supported: string[], * } */ - #[HttpOptions('/.well-known/oauth-protected-resource')] - #[HttpGet('/.well-known/oauth-protected-resource')] - public function getWellKnownProtectedResource(HttpResponseBuilder $response, HttpRequest $request): array|int { - $response->setHeader('Access-Control-Allow-Origin', '*'); - if($request->method === 'OPTIONS') - return 204; - + #[AccessControl] + #[ExactRoute('GET', '/.well-known/oauth-protected-resource')] + public function getWellKnownProtectedResource(HttpResponseBuilder $response, HttpRequest $request): array { return [ 'resource' => $this->siteInfo->url, 'authorization_servers' => [$this->siteInfo->url], @@ -75,7 +75,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { } /** - * @return int|array{ + * @return array{ * issuer: string, * authorization_endpoint: string, * token_endpoint: string, @@ -92,13 +92,9 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { * code_challenge_methods_supported: string[], * } */ - #[HttpOptions('/.well-known/oauth-authorization-server')] - #[HttpGet('/.well-known/oauth-authorization-server')] - public function getWellKnownAuthorizationServer(HttpResponseBuilder $response, HttpRequest $request): array|int { - $response->setHeader('Access-Control-Allow-Origin', '*'); - if($request->method === 'OPTIONS') - return 204; - + #[AccessControl] + #[ExactRoute('GET', '/.well-known/oauth-authorization-server')] + public function getWellKnownAuthorizationServer(HttpResponseBuilder $response, HttpRequest $request): array { return [ 'issuer' => $this->siteInfo->url, 'authorization_endpoint' => sprintf('%s%s', $this->siteInfo->url, $this->urls->format('oauth2-authorise')), @@ -134,7 +130,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { } /** - * @return int|array{ + * @return array{ * issuer: string, * authorization_endpoint: string, * token_endpoint: string, @@ -148,13 +144,9 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { * token_endpoint_auth_methods_supported: string[], * } */ - #[HttpOptions('/.well-known/openid-configuration')] - #[HttpGet('/.well-known/openid-configuration')] - public function getWellKnown(HttpResponseBuilder $response, HttpRequest $request): array|int { - $response->setHeader('Access-Control-Allow-Origin', '*'); - if($request->method === 'OPTIONS') - return 204; - + #[AccessControl] + #[ExactRoute('GET', '/.well-known/openid-configuration')] + public function getWellKnown(HttpResponseBuilder $response, HttpRequest $request): array { $signingAlgs = array_values(array_unique(XArray::select( $this->oauth2Ctx->keys->getPublicKeySet()->keys, fn($key) => $key->algo @@ -185,11 +177,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { #[HttpOptions('/oauth2/jwks.json')] #[HttpGet('/oauth2/jwks.json')] #[UrlFormat('oauth2-jwks', '/oauth2/jwks.json')] - public function getJwks(HttpResponseBuilder $response, HttpRequest $request): int|array { - $response->setHeader('Access-Control-Allow-Origin', '*'); - if($request->method === 'OPTIONS') - return 204; - + public function getJwks(HttpResponseBuilder $response, HttpRequest $request): array { return $this->oauth2Ctx->keys->getPublicKeysForJson(); } @@ -203,13 +191,13 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { * interval?: int * }|array{ error: string, error_description: string } */ - #[HttpPost('/oauth2/request-authorise')] - #[HttpPost('/oauth2/request-authorize')] + #[PatternRoute('POST', '/oauth2/request-authori[sz]e')] + #[Before('input:urlencoded', required: false)] #[UrlFormat('oauth2-request-authorise', '/oauth2/request-authorize')] - public function postRequestAuthorise(HttpResponseBuilder $response, HttpRequest $request): array { + public function postRequestAuthorise(HttpResponseBuilder $response, HttpRequest $request, ?FormContent $content): array { $response->setHeader('Cache-Control', 'no-store'); - if(!($request->content instanceof FormHttpContent)) + if($content === null) return self::filter($response, $request, [ 'error' => 'invalid_request', 'error_description' => 'Your request must use content type application/x-www-form-urlencoded.', @@ -226,7 +214,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { 'error_description' => 'You must use the Basic method for Authorization parameters.', ], authzHeader: true); } else { - $clientId = (string)$request->content->getParam('client_id'); + $clientId = (string)$content->getParam('client_id'); $clientSecret = ''; } @@ -250,12 +238,12 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { return self::filter($response, $request, $this->oauth2Ctx->createDeviceAuthorisationRequest( $appInfo, - $request->content->hasParam('scope') ? (string)$request->content->getParam('scope') : null + $content->hasParam('scope') ? (string)$content->getParam('scope') : null )); } /** - * @return int|array{ + * @return array{ * access_token: string, * token_type: 'Bearer', * expires_in?: int, @@ -263,36 +251,14 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { * refresh_token?: string, * }|array{ error: string, error_description: string } */ - #[HttpOptions('/oauth2/token')] - #[HttpPost('/oauth2/token')] + #[AccessControl(allowHeaders: ['Authorization'], exposeHeaders: ['WWW-Authenticate'])] + #[ExactRoute('POST', '/oauth2/token')] + #[Before('input:urlencoded', required: false)] #[UrlFormat('oauth2-token', '/oauth2/token')] - public function postToken(HttpResponseBuilder $response, HttpRequest $request): array|int { + public function postToken(HttpResponseBuilder $response, HttpRequest $request, ?FormContent $content): array { $response->setHeader('Cache-Control', 'no-store'); - $originHeaders = ['Origin', 'X-Origin', 'Referer']; - $origins = []; - foreach($originHeaders as $originHeader) { - $originHeader = $request->getHeaderFirstLine($originHeader); - if($originHeader !== '' && !in_array($originHeader, $origins)) - $origins[] = $originHeader; - } - - if(!empty($origins)) { - // TODO: check if none of the provided origins is on a blocklist or something - // different origins being specified for each header should probably also be considered suspect... - - $response->setHeader('Access-Control-Allow-Origin', $origins[0]); - $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST'); - $response->setHeader('Access-Control-Allow-Headers', 'Authorization'); - $response->setHeader('Access-Control-Expose-Headers', 'Vary'); - foreach($originHeaders as $originHeader) - $response->setHeader('Vary', $originHeader); - } - - if($request->method === 'OPTIONS') - return 204; - - if(!($request->content instanceof FormHttpContent)) + if($content === null) return self::filter($response, $request, [ 'error' => 'invalid_request', 'error_description' => 'Your request must use content type application/x-www-form-urlencoded.', @@ -310,8 +276,8 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { '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)$request->content->getParam('client_id'); - $clientSecret = (string)$request->content->getParam('client_secret'); + $clientId = (string)$content->getParam('client_id'); + $clientSecret = (string)$content->getParam('client_secret'); } try { @@ -334,36 +300,36 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { ], authzHeader: $authzHeader[0] !== ''); } - $type = (string)$request->content->getParam('grant_type'); + $type = (string)$content->getParam('grant_type'); if($type === 'authorization_code') return self::filter($response, $request, $this->oauth2Ctx->redeemAuthorisationCode( $appInfo, $isAuthed, - (string)$request->content->getParam('code'), - (string)$request->content->getParam('code_verifier') + (string)$content->getParam('code'), + (string)$content->getParam('code_verifier') )); if($type === 'refresh_token') return self::filter($response, $request, $this->oauth2Ctx->redeemRefreshToken( $appInfo, $isAuthed, - (string)$request->content->getParam('refresh_token'), - $request->content->hasParam('scope') ? (string)$request->content->getParam('scope') : null + (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, - $request->content->hasParam('scope') ? (string)$request->content->getParam('scope') : null + $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, - (string)$request->content->getParam('device_code') + (string)$content->getParam('device_code') )); return self::filter($response, $request, [ @@ -373,7 +339,7 @@ final class OAuth2ApiRoutes implements RouteHandler, UrlSource { } /** - * @return int|array{ + * @return array{ * sub: string, * name?: string, * nickname?: string, diff --git a/src/OAuth2/OAuth2WebRoutes.php b/src/OAuth2/OAuth2WebRoutes.php index 6f17d0ec..5033b24c 100644 --- a/src/OAuth2/OAuth2WebRoutes.php +++ b/src/OAuth2/OAuth2WebRoutes.php @@ -4,8 +4,11 @@ namespace Misuzu\OAuth2; use InvalidArgumentException; use RuntimeException; use Index\XArray; -use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest}; -use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\{HttpResponseBuilder,HttpRequest}; +use Index\Http\Content\FormContent; +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\Auth\AuthInfo; @@ -24,15 +27,14 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { private AuthInfo $authInfo, ) {} - #[HttpGet('/oauth2/authorise')] - #[HttpGet('/oauth2/authorize')] + #[PatternRoute('GET', '/oauth2/authori[sz]e')] #[UrlFormat('oauth2-authorise', '/oauth2/authorize')] public function getAuthorise(HttpResponseBuilder $response, HttpRequest $request): string { return Template::renderRaw('oauth2.authorise'); } /** - * @return int|array{ + * @return array{ * error: 'auth'|'csrf'|'method'|'length'|'client'|'scope'|'format'|'required'|'authorise'|'resptype', * scope?: string, * reason?: string, @@ -41,11 +43,9 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { * redirect: string, * } */ - #[HttpPost('/oauth2/authorise')] - public function postAuthorise(HttpResponseBuilder $response, HttpRequest $request): int|array { - if(!($request->content instanceof FormHttpContent)) - return 400; - + #[ExactRoute('POST', '/oauth2/authorize')] + #[Before('input:urlencoded')] + public function postAuthorise(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array { // TODO: RATE LIMITING if(!CSRF::validate($request->getHeaderLine('X-CSRF-token'))) @@ -56,20 +56,20 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { if(!$this->authInfo->loggedIn) return ['error' => 'auth']; - $responseTypes = explode(' ', (string)$request->content->getParam('rt'), 2); + $responseTypes = explode(' ', (string)$content->getParam('rt'), 2); sort($responseTypes); $responseType = implode(' ', $responseTypes); if(!in_array($responseType, self::RESPONSE_TYPES)) return ['error' => 'resptype']; $codeChallengeMethod = 'plain'; - if($request->content->hasParam('ccm')) { - $codeChallengeMethod = $request->content->getParam('ccm'); + if($content->hasParam('ccm')) { + $codeChallengeMethod = $content->getParam('ccm'); if(!in_array($codeChallengeMethod, ['plain', 'S256'])) return ['error' => 'method']; } - $codeChallenge = $request->content->getParam('cc'); + $codeChallenge = $content->getParam('cc'); $codeChallengeLength = strlen($codeChallenge); if($codeChallengeMethod === 'S256') { if($codeChallengeLength !== 43) @@ -81,16 +81,16 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { try { $appInfo = $this->oauth2Ctx->appsCtx->apps->getAppInfo( - clientId: (string)$request->content->getParam('client'), + clientId: (string)$content->getParam('client'), deleted: false ); } catch(RuntimeException $ex) { return ['error' => 'client']; } - if($request->content->hasParam('scope')) { + if($content->hasParam('scope')) { $scope = []; - $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, (string)$request->content->getParam('scope')); + $scopeInfos = $this->oauth2Ctx->appsCtx->handleScopeString($appInfo, (string)$content->getParam('scope')); foreach($scopeInfos as $scopeName => $scopeInfo) { if(is_string($scopeInfo)) @@ -102,8 +102,8 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { $scope = implode(' ', $scope); } else $scope = ''; - if($request->content->hasParam('redirect')) { - $redirectUri = (string)$request->content->getParam('redirect'); + if($content->hasParam('redirect')) { + $redirectUri = (string)$content->getParam('redirect'); $redirectUriId = $this->oauth2Ctx->appsCtx->apps->getAppUriId($appInfo, $redirectUri); if($redirectUriId === null) return ['error' => 'format']; @@ -169,7 +169,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { * scope: string[], * } */ - #[HttpGet('/oauth2/resolve-authorise-app')] + #[ExactRoute('GET', '/oauth2/resolve-authorise-app')] #[UrlFormat('oauth2-resolve-authorise-app', '/oauth2/resolve-authorise-app')] public function getResolveAuthorise(HttpResponseBuilder $response, HttpRequest $request): array { // TODO: RATE LIMITING @@ -243,14 +243,14 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { return $result; } - #[HttpGet('/oauth2/verify')] + #[ExactRoute('GET', '/oauth2/verify')] #[UrlFormat('oauth2-verify', '/oauth2/verify')] public function getVerify(HttpResponseBuilder $response, HttpRequest $request): string { return Template::renderRaw('oauth2.verify'); } /** - * @return int|array{ + * @return array{ * error: 'auth'|'csrf'|'invalid'|'code'|'expired'|'approval'|'code'|'scope', * scope?: string, * reason?: string, @@ -258,11 +258,9 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { * approval: 'approved'|'denied', * } */ - #[HttpPost('/oauth2/verify')] - public function postVerify(HttpResponseBuilder $response, HttpRequest $request): int|array { - if(!($request->content instanceof FormHttpContent)) - return 400; - + #[ExactRoute('POST', '/oauth2/verify')] + #[Before('input:urlencoded')] + public function postVerify(HttpResponseBuilder $response, HttpRequest $request, FormContent $content): array { // TODO: RATE LIMITING if(!CSRF::validate($request->getHeaderLine('X-CSRF-token'))) @@ -273,13 +271,13 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { if(!$this->authInfo->loggedIn) return ['error' => 'auth']; - $approve = (string)$request->content->getParam('approve'); + $approve = (string)$content->getParam('approve'); if(!in_array($approve, ['yes', 'no'])) return ['error' => 'invalid']; try { $deviceInfo = $this->oauth2Ctx->devices->getDeviceInfo( - userCode: (string)$request->content->getParam('code') + userCode: (string)$content->getParam('code') ); } catch(RuntimeException $ex) { return ['error' => 'code']; @@ -357,7 +355,7 @@ final class OAuth2WebRoutes implements RouteHandler, UrlSource { * scope: string[], * } */ - #[HttpGet('/oauth2/resolve-verify')] + #[ExactRoute('GET', '/oauth2/resolve-verify')] #[UrlFormat('oauth2-resolve-verify', '/oauth2/resolve-verify')] public function getResolveVerify(HttpResponseBuilder $response, HttpRequest $request) { // TODO: RATE LIMITING diff --git a/src/Pagination.php b/src/Pagination.php index 0e7edc2a..238603ac 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -57,7 +57,7 @@ final class Pagination { ): self { return self::fromPage( $count, - (int)(filter_input(INPUT_GET, $pageParam, FILTER_SANITIZE_NUMBER_INT) ?? $firstPage), + !empty($_GET[$pageParam]) && is_scalar($_GET[$pageParam]) ? (int)$_GET[$pageParam] : $firstPage, $range, $firstPage ); @@ -72,7 +72,7 @@ final class Pagination { ): Pagination { return self::fromPage( $count, - (int)($request->getParam($pageParam, FILTER_SANITIZE_NUMBER_INT) ?? $firstPage), + (int)($request->getFilteredParam($pageParam, FILTER_SANITIZE_NUMBER_INT) ?? $firstPage), $range, $firstPage ); diff --git a/src/Perm.php b/src/Perm.php index cc5e65d8..59e7b6de 100644 --- a/src/Perm.php +++ b/src/Perm.php @@ -423,7 +423,7 @@ final class Perm { $item->perms[] = $permItem = new stdClass; $permItem->category = $categoryName; - $permItem->name = sprintf('perms[%s:%d]', $categoryName, $perm); + $permItem->name = sprintf('perm_%s_%d', $categoryName, $perm); $permItem->title = self::label($categoryName, $perm); $permItem->value = $perm; } @@ -437,11 +437,18 @@ final class Perm { * @param string[] $categories * @return array<string, array<string, int>> */ - public static function convertSubmission(array $raw, array $categories): array { + public static function convertSubmission(array $raw, array $categories, string $prefix = 'perm_'): array { $apply = []; foreach($raw as $name => $mode) { - $nameParts = explode(':', $name, 2); + if($prefix !== '') { + if(!str_starts_with($name, $prefix)) + continue; + + $name = substr($name, strlen($prefix)); + } + + $nameParts = explode('_', $name, 2); if(count($nameParts) !== 2 || !ctype_alpha($nameParts[0]) || !ctype_digit($nameParts[1])) continue; @@ -449,12 +456,11 @@ final class Perm { if(!in_array($category, $categories)) continue; - if($mode === 'yes' || $mode === 'never') { - if(!array_key_exists($category, $apply)) - $apply[$category] = ['allow' => 0, 'deny' => 0]; + if(!array_key_exists($category, $apply)) + $apply[$category] = ['allow' => 0, 'deny' => 0]; + if($mode === 'yes' || $mode === 'never') $apply[$category][$mode === 'yes' ? 'allow' : 'deny'] |= (int)$value; - } } return $apply; diff --git a/src/Redirects/AliasRedirectsRoutes.php b/src/Redirects/AliasRedirectsRoutes.php index aa52a178..0d120a53 100644 --- a/src/Redirects/AliasRedirectsRoutes.php +++ b/src/Redirects/AliasRedirectsRoutes.php @@ -3,7 +3,8 @@ namespace Misuzu\Redirects; use Index\Config\Config; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\PatternRoute; class AliasRedirectsRoutes implements RouteHandler { use RouteHandlerCommon; @@ -24,23 +25,23 @@ class AliasRedirectsRoutes implements RouteHandler { $response->redirect($url, true); } - #[HttpGet('/[up]([0-9]+)')] - #[HttpGet('/[up]/([A-Za-z0-9\-_]+)')] + #[PatternRoute('GET', '/[up]([0-9]+)')] + #[PatternRoute('GET', '/[up]/([A-Za-z0-9\-_]+)')] public function getProfileRedirect(HttpResponseBuilder $response, HttpRequest $request, string $userId): void { $this->redirect($response, $request, 'user_profile', $userId); } - #[HttpGet('/fc?/?([0-9]+)')] + #[PatternRoute('GET', '/fc?/?([0-9]+)')] public function getForumCategoryRedirect(HttpResponseBuilder $response, HttpRequest $request, string $categoryId): void { $this->redirect($response, $request, 'forum_category', $categoryId); } - #[HttpGet('/ft/?([0-9]+)')] + #[PatternRoute('GET', '/ft/?([0-9]+)')] public function getForumTopicRedirect(HttpResponseBuilder $response, HttpRequest $request, string $topicId): void { $this->redirect($response, $request, 'forum_topic', $topicId); } - #[HttpGet('/fp/?([0-9]+)')] + #[PatternRoute('GET', '/fp/?([0-9]+)')] public function getForumPostRedirect(HttpResponseBuilder $response, HttpRequest $request, string $postId): void { $this->redirect($response, $request, 'forum_post', $postId); } diff --git a/src/Redirects/IncrementalRedirectsRoutes.php b/src/Redirects/IncrementalRedirectsRoutes.php index ea308935..5d75dcfc 100644 --- a/src/Redirects/IncrementalRedirectsRoutes.php +++ b/src/Redirects/IncrementalRedirectsRoutes.php @@ -3,8 +3,11 @@ namespace Misuzu\Redirects; use RuntimeException; use Index\XNumber; -use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\HttpResponseBuilder; +use Index\Http\Content\FormContent; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Processors\Before; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; class IncrementalRedirectsRoutes implements RouteHandler { use RouteHandlerCommon; @@ -13,8 +16,8 @@ class IncrementalRedirectsRoutes implements RouteHandler { private RedirectsContext $redirectsCtx, ) {} - #[HttpGet('/[bg]/([A-Za-z0-9]+)')] - public function getIncrementalRedirect(HttpResponseBuilder $response, HttpRequest $request, string $linkId): int { + #[PatternRoute('GET', '/[bg]/([A-Za-z0-9]+)')] + public function getIncrementalRedirect(HttpResponseBuilder $response, string $linkId): int { $linkId = XNumber::fromBase62($linkId); if($linkId === false) return 400; @@ -30,15 +33,13 @@ class IncrementalRedirectsRoutes implements RouteHandler { } /** @return int|array{url: string} */ - #[HttpPost('/satori/create')] - public function postIncrementalRedirect(HttpResponseBuilder $response, HttpRequest $request): int|array { - if(!($request->content instanceof FormHttpContent)) - return 400; - + #[ExactRoute('POST', '/satori/create')] + #[Before('input:urlencoded')] + public function postIncrementalRedirect(FormContent $content): int|array { $config = $this->redirectsCtx->config->scopeTo('incremental'); - $url = (string)$request->content->getParam('u'); - $time = (int)$request->content->getParam('t', FILTER_SANITIZE_NUMBER_INT); - $sign = base64_decode((string)$request->content->getParam('s')); + $url = (string)$content->getParam('u'); + $time = (int)$content->getFilteredParam('t', FILTER_SANITIZE_NUMBER_INT); + $sign = base64_decode((string)$content->getParam('s')); $hash = hash_hmac('sha256', "satori#create#{$time}#{$url}", $config->getString('secret'), true); if(!hash_equals($hash, $sign)) diff --git a/src/Redirects/LandingRedirectsRoutes.php b/src/Redirects/LandingRedirectsRoutes.php index 19535e50..01b45e61 100644 --- a/src/Redirects/LandingRedirectsRoutes.php +++ b/src/Redirects/LandingRedirectsRoutes.php @@ -1,13 +1,14 @@ <?php namespace Misuzu\Redirects; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\ExactRoute; use Misuzu\Template; class LandingRedirectsRoutes implements RouteHandler { use RouteHandlerCommon; - #[HttpGet('/')] + #[ExactRoute('GET', '/')] public function getIndex(): string { return Template::renderRaw('redirects.landing'); } diff --git a/src/Redirects/NamedRedirectsRoutes.php b/src/Redirects/NamedRedirectsRoutes.php index 4dad14bc..d6c6d54d 100644 --- a/src/Redirects/NamedRedirectsRoutes.php +++ b/src/Redirects/NamedRedirectsRoutes.php @@ -2,9 +2,9 @@ namespace Misuzu\Redirects; use RuntimeException; -use Index\Config\Config; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\PatternRoute; class NamedRedirectsRoutes implements RouteHandler { use RouteHandlerCommon; @@ -13,7 +13,7 @@ class NamedRedirectsRoutes implements RouteHandler { private RedirectsContext $redirectsCtx, ) {} - #[HttpGet('/([A-Za-z0-9\-_]+)')] + #[PatternRoute('GET', '/([A-Za-z0-9\-_]+)')] public function getNamedRedirect(HttpResponseBuilder $response, HttpRequest $request, string $name): int { try { $redirectInfo = $this->redirectsCtx->named->getNamedRedirect( diff --git a/src/Redirects/SocialRedirectsRoutes.php b/src/Redirects/SocialRedirectsRoutes.php index 2b921296..173ed7bd 100644 --- a/src/Redirects/SocialRedirectsRoutes.php +++ b/src/Redirects/SocialRedirectsRoutes.php @@ -1,8 +1,9 @@ <?php namespace Misuzu\Redirects; -use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\HttpResponseBuilder; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\PatternRoute; use Misuzu\Template; class SocialRedirectsRoutes implements RouteHandler { @@ -12,8 +13,8 @@ class SocialRedirectsRoutes implements RouteHandler { private RedirectsContext $redirectsCtx ) {} - #[HttpGet('/bsky/((did:[a-z0-9]+:[A-Za-z0-9.\-_:%]+)|(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])))')] - public function getBlueskyRedirect(HttpResponseBuilder $response, HttpRequest $request, string $handle): int|string { + #[PatternRoute('GET', '/bsky/((did:[a-z0-9]+:[A-Za-z0-9.\-_:%]+)|(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])))')] + public function getBlueskyRedirect(HttpResponseBuilder $response, string $handle): int|string { $did = null; if(str_starts_with($handle, 'did:')) @@ -47,8 +48,8 @@ class SocialRedirectsRoutes implements RouteHandler { ]); } - #[HttpGet('/fedi/([A-Za-z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})')] - public function getFediverseRedirect(HttpResponseBuilder $response, HttpRequest $request, string $userName, string $instance): string { + #[PatternRoute('GET', '/fedi/([A-Za-z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})')] + public function getFediverseRedirect(string $userName, string $instance): string { return Template::renderRaw('redirects.fedi', [ 'fedi_username' => $userName, 'fedi_instance' => $instance, diff --git a/src/Routing/BackedRoutingContext.php b/src/Routing/BackedRoutingContext.php deleted file mode 100644 index a52623ea..00000000 --- a/src/Routing/BackedRoutingContext.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -namespace Misuzu\Routing; - -use Index\Http\HttpRequest; -use Index\Http\Routing\{HttpRouter,RouteHandler}; -use Index\Urls\{ArrayUrlRegistry,UrlRegistry,UrlSource}; - -class BackedRoutingContext implements RoutingContext { - public private(set) HttpRouter $router; - public private(set) UrlRegistry $urls; - - public function __construct( - ?HttpRouter $router = null, - ?UrlRegistry $urls = null - ) { - $this->urls = $urls ?? new ArrayUrlRegistry; - $this->router = $router ?? new HttpRouter(errorHandler: new RoutingErrorHandler); - $this->router->use('/', fn($resp) => $resp->setPoweredBy('Misuzu')); - } - - public function register(RouteHandler|UrlSource $handler): void { - if($handler instanceof RouteHandler) - $this->router->register($handler); - if($handler instanceof UrlSource) - $this->urls->register($handler); - } - - public function scopeTo(string $prefix): RoutingContext { - return new ScopedRoutingContext( - $this->router->scopeTo($prefix), - $this->urls->scopeTo(pathPrefix: $prefix) - ); - } - - /** @param mixed[] $args */ - public function dispatch(?HttpRequest $request = null, array $args = []): void { - $this->router->dispatch($request, $args); - } -} diff --git a/src/Routing/RoutingContext.php b/src/Routing/RoutingContext.php index 7c2ffcd0..d40fd9f3 100644 --- a/src/Routing/RoutingContext.php +++ b/src/Routing/RoutingContext.php @@ -1,11 +1,32 @@ <?php namespace Misuzu\Routing; -use Index\Http\HttpRequest; -use Index\Http\Routing\RouteHandler; -use Index\Urls\UrlSource; +use Index\Http\{HttpRequest,HttpResponseBuilder}; +use Index\Http\Routing\{Router,RouteHandler}; +use Index\Http\Routing\Filters\FilterInfo; +use Index\Urls\{ArrayUrlRegistry,UrlRegistry,UrlSource}; -interface RoutingContext { - public function register(RouteHandler|UrlSource $handler): void; - public function scopeTo(string $prefix): RoutingContext; +class RoutingContext { + public private(set) Router $router; + public private(set) UrlRegistry $urls; + + public function __construct( + ?Router $router = null, + ?UrlRegistry $urls = null + ) { + $this->urls = $urls ?? new ArrayUrlRegistry; + $this->router = $router ?? new Router(errorHandler: new RoutingErrorHandler); + $this->router->filter(FilterInfo::prefix('/', fn(HttpResponseBuilder $response) => $response->setPoweredBy('Misuzu'))); + } + + public function register(RouteHandler|UrlSource $handler): void { + if($handler instanceof RouteHandler) + $this->router->register($handler); + if($handler instanceof UrlSource) + $this->urls->register($handler); + } + + public function dispatch(?HttpRequest $request = null): void { + $this->router->dispatch($request); + } } diff --git a/src/Routing/RoutingErrorHandler.php b/src/Routing/RoutingErrorHandler.php index ecd45513..a01cf401 100644 --- a/src/Routing/RoutingErrorHandler.php +++ b/src/Routing/RoutingErrorHandler.php @@ -1,14 +1,19 @@ <?php namespace Misuzu\Routing; -use Index\Http\{HtmlHttpErrorHandler,HttpResponseBuilder,HttpRequest}; -use Misuzu\{Misuzu,Template}; +use Index\Http\Routing\HandlerContext; +use Index\Http\Routing\ErrorHandling\HtmlErrorHandler; +use Index\Http\Streams\Stream; +use Misuzu\Misuzu; -class RoutingErrorHandler extends HtmlHttpErrorHandler { - public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void { - if(str_starts_with($request->path, '/_')) { - $response->setTypePlain(); - $response->content = sprintf('HTTP %03d', $code); +class RoutingErrorHandler extends HtmlErrorHandler { + public function handle(HandlerContext $context): void { + if(!$context->response->needsBody) + return; + + if(str_starts_with($context->request->requestTarget, '/_')) { + $context->response->setTypePlain(); + $context->response->body = Stream::createStream(sprintf('HTTP %03d', $context->response->statusCode)); return; } @@ -23,11 +28,11 @@ class RoutingErrorHandler extends HtmlHttpErrorHandler { $path = sprintf('%s/error-%03d.html', Misuzu::PATH_PUBLIC, $code); if(is_file($path)) { - $response->setTypeHTML(); - $response->content = file_get_contents($path); + $context->response->setTypeHTML(); + $context->response->body = Stream::createStreamFromFile($path, 'rb'); return; } - parent::handle($response, $request, $code, $message); + parent::handle($context); } } diff --git a/src/Routing/ScopedRoutingContext.php b/src/Routing/ScopedRoutingContext.php deleted file mode 100644 index 2669bde0..00000000 --- a/src/Routing/ScopedRoutingContext.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -namespace Misuzu\Routing; - -use Index\Http\HttpRequest; -use Index\Http\Routing\{RouteHandler,Router}; -use Index\Urls\{UrlRegistry,UrlSource}; - -class ScopedRoutingContext implements RoutingContext { - public function __construct( - private Router $router, - private UrlRegistry $urls - ) {} - - public function register(RouteHandler|UrlSource $handler): void { - if($handler instanceof RouteHandler) - $this->router->register($handler); - if($handler instanceof UrlSource) - $this->urls->register($handler); - } - - public function scopeTo(string $prefix): RoutingContext { - return new ScopedRoutingContext( - $this->router->scopeTo($prefix), - $this->urls->scopeTo(pathPrefix: $prefix) - ); - } -} diff --git a/src/Satori/SatoriRoutes.php b/src/Satori/SatoriRoutes.php index 45667b1b..aa133853 100644 --- a/src/Satori/SatoriRoutes.php +++ b/src/Satori/SatoriRoutes.php @@ -5,7 +5,12 @@ use RuntimeException; use Index\Colour\Colour; use Index\Config\Config; use Index\Http\{HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HttpGet,HttpMiddleware,RouteHandler,RouteHandlerCommon}; +use Index\Http\Content\FormContent; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\AccessControl\AccessControl; +use Index\Http\Routing\Filters\PrefixFilter; +use Index\Http\Routing\Processors\Before; +use Index\Http\Routing\Routes\ExactRoute; use Misuzu\Pagination; use Misuzu\Forum\ForumContext; use Misuzu\Profile\ProfileContext; @@ -22,8 +27,8 @@ final class SatoriRoutes implements RouteHandler { ) {} /** @return void|int */ - #[HttpMiddleware('/_satori')] - public function verifyRequest(HttpResponseBuilder $response, HttpRequest $request) { + #[PrefixFilter('/_satori')] + public function verifyRequest(HttpRequest $request) { $secretKey = $this->config->getString('secret'); if(!empty($secretKey)) { @@ -34,7 +39,7 @@ final class SatoriRoutes implements RouteHandler { if(empty($userHash) || $userTime < $currentTime - 60 || $userTime > $currentTime + 60) return 403; - $verifyText = (string)$userTime . '#' . $request->path . '?' . $request->getParamString(); + $verifyText = (string)$userTime . '#' . $request->requestTarget . '?' . $request->getParamString(true); $verifyHash = hash_hmac('sha256', $verifyText, $secretKey, true); if(!hash_equals($verifyHash, $userHash)) @@ -43,10 +48,10 @@ final class SatoriRoutes implements RouteHandler { } /** @return array{error: int}|array{field_value: string} */ - #[HttpGet('/_satori/get-profile-field')] - public function getProfileField(HttpResponseBuilder $response, HttpRequest $request): array { - $userId = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT); - $fieldId = (string)$request->getParam('field', FILTER_SANITIZE_NUMBER_INT); + #[ExactRoute('GET', '/_satori/get-profile-field')] + public function getProfileField(HttpRequest $request): array { + $userId = (string)$request->getFilteredParam('user', FILTER_SANITIZE_NUMBER_INT); + $fieldId = (string)$request->getFilteredParam('field', FILTER_SANITIZE_NUMBER_INT); try { $fieldValue = $this->profileCtx->fields->getFieldValue($fieldId, $userId); @@ -72,15 +77,15 @@ final class SatoriRoutes implements RouteHandler { * is_opening_post: int * }[] */ - #[HttpGet('/_satori/get-recent-forum-posts')] - public function getRecentForumPosts(HttpResponseBuilder $response, HttpRequest $request): array { + #[ExactRoute('GET', '/_satori/get-recent-forum-posts')] + public function getRecentForumPosts(HttpRequest $request): array { $categoryIds = $this->config->getArray('forum.categories'); if(empty($categoryIds)) return []; $batchSize = $this->config->getInteger('forum.batch', 6); $backlogDays = $this->config->getInteger('forum.backlog', 7); - $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); + $startId = (string)$request->getFilteredParam('start', FILTER_SANITIZE_NUMBER_INT); $posts = []; $postInfos = $this->forumCtx->posts->getPosts( @@ -120,11 +125,11 @@ final class SatoriRoutes implements RouteHandler { * username: string * }[] */ - #[HttpGet('/_satori/get-recent-registrations')] - public function getRecentRegistrations(HttpResponseBuilder $response, HttpRequest $request): array { + #[ExactRoute('GET', '/_satori/get-recent-registrations')] + public function getRecentRegistrations(HttpRequest $request): array { $batchSize = $this->config->getInteger('users.batch', 10); $backlogDays = $this->config->getInteger('users.backlog', 7); - $startId = (string)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); + $startId = (string)$request->getFilteredParam('start', FILTER_SANITIZE_NUMBER_INT); $userInfos = $this->usersCtx->users->getUsers( after: $startId, diff --git a/src/SharpChat/SharpChatRoutes.php b/src/SharpChat/SharpChatRoutes.php index 8fa23872..c9516fe3 100644 --- a/src/SharpChat/SharpChatRoutes.php +++ b/src/SharpChat/SharpChatRoutes.php @@ -4,8 +4,12 @@ namespace Misuzu\SharpChat; use RuntimeException; use Index\Colour\Colour; use Index\Config\Config; -use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder}; -use Index\Http\Routing\{HandlerAttribute,HttpDelete,HttpGet,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon}; +use Index\Http\{HttpRequest,HttpResponseBuilder}; +use Index\Http\Content\{FormContent,UrlEncodedFormContent}; +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\Auth\{AuthContext,AuthInfo,Sessions}; use Misuzu\Counters\CountersData; @@ -34,17 +38,10 @@ final class SharpChatRoutes implements RouteHandler { $this->hashKey = $this->config->getString('hashKey', 'woomy'); } - /** @return int|array{Text: string[], Image: string, Hierarchy: int}[] */ - #[HttpOptions('/_sockchat/emotes')] - #[HttpGet('/_sockchat/emotes')] - public function getEmotes(HttpResponseBuilder $response, HttpRequest $request): array|int { - $response->setHeader('Access-Control-Allow-Origin', '*'); - $response->setHeader('Access-Control-Allow-Methods', 'GET'); - $response->setHeader('Access-Control-Allow-Headers', 'Cache-Control'); - - if($request->method === 'OPTIONS') - return 204; - + /** @return array{Text: string[], Image: string, Hierarchy: int}[] */ + #[AccessControl(allowHeaders: ['Cache-Control'])] + #[ExactRoute('GET', '/_sockchat/emotes')] + public function getEmotes(): array { $this->counters->increment('dev:legacy_emotes_loads'); $emotes = $this->emotes->getEmotes(orderBy: 'order'); @@ -66,7 +63,7 @@ final class SharpChatRoutes implements RouteHandler { return $out; } - #[HttpGet('/_sockchat/login')] + #[ExactRoute('GET', '/_sockchat/login')] public function getLogin(HttpResponseBuilder $response, HttpRequest $request): void { if(!$this->authInfo->loggedIn) { $response->redirect($this->urls->format('auth-login')); @@ -87,34 +84,11 @@ final class SharpChatRoutes implements RouteHandler { return in_array($targetId, $whitelist, true); } - /** @return int|array{ok: false, err: string}|array{ok: true, usr: int, tkn: string} */ - #[HttpOptions('/_sockchat/token')] - #[HttpGet('/_sockchat/token')] - public function getToken(HttpResponseBuilder $response, HttpRequest $request): int|array { - $host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : ''; - $origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : ''; - $originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? ''); - - if(!empty($originHost) && $originHost !== $host) { - $whitelist = $this->config->getArray('origins', []); - - if(!in_array($originHost, $whitelist)) - return 403; - - $originProto = strtolower(parse_url($origin, PHP_URL_SCHEME)); - $origin = $originProto . '://' . $originHost; - - $response->setHeader('Access-Control-Allow-Origin', $origin); - $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); - $response->setHeader('Access-Control-Allow-Credentials', 'true'); - $response->setHeader('Vary', 'Origin'); - } - - if($request->method === 'OPTIONS') - return 204; - + /** @return array{ok: false, err: string}|array{ok: true, usr: int, tkn: string} */ + #[AccessControl(credentials: true)] + #[ExactRoute('GET', '/_sockchat/token')] + public function getToken(HttpRequest $request): array { $tokenInfo = $this->authInfo->tokenInfo; - if(!$tokenInfo->hasSessionToken) return ['ok' => false, 'err' => 'token']; @@ -144,25 +118,28 @@ final class SharpChatRoutes implements RouteHandler { } /** @return int|void */ - #[HttpPost('/_sockchat/bump')] - public function postBump(HttpResponseBuilder $response, HttpRequest $request) { + #[ExactRoute('POST', '/_sockchat/bump')] + #[Before('input:urlencoded')] + public function postBump(HttpRequest $request, UrlEncodedFormContent $content) { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - if(!($request->content instanceof FormHttpContent)) - return 400; + $bumpList = []; + foreach($content->params as $name => $value) { + if(count($value) < 1) + continue; - $bumpList = $request->content->getParam('u', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); - if(!is_array($bumpList)) - return 400; + if(str_starts_with($name, 'u[') && str_ends_with($name, ']')) + $bumpList[substr($name, 2, -1)] = $value[0]; + } - $userTime = (int)$request->content->getParam('t', FILTER_SANITIZE_NUMBER_INT); + $userTime = (int)$content->getFilteredParam('t', FILTER_SANITIZE_NUMBER_INT); $signature = "bump#{$userTime}"; foreach($bumpList as $userId => $ipAddr) $signature .= "#{$userId}:{$ipAddr}"; - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); $realHash = hash_hmac('sha256', $signature, $this->hashKey); if(!hash_equals($realHash, $userHash)) return 403; @@ -185,19 +162,17 @@ final class SharpChatRoutes implements RouteHandler { * super: bool * } */ - #[HttpPost('/_sockchat/verify')] - public function postVerify(HttpResponseBuilder $response, HttpRequest $request): int|array { + #[ExactRoute('POST', '/_sockchat/verify')] + #[Before('input:urlencoded')] + public function postVerify(HttpRequest $request, FormContent $content): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - if(!($request->content instanceof FormHttpContent)) - return ['success' => false, 'reason' => 'request']; + $authMethod = (string)$content->getParam('method'); + $authToken = (string)$content->getParam('token'); + $ipAddress = (string)$content->getParam('ipaddr'); - $authMethod = (string)$request->content->getParam('method'); - $authToken = (string)$request->content->getParam('token'); - $ipAddress = (string)$request->content->getParam('ipaddr'); - - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); if(strlen($userHash) !== 64) return ['success' => false, 'reason' => 'length']; @@ -291,13 +266,13 @@ final class SharpChatRoutes implements RouteHandler { * expires: string * }[] */ - #[HttpGet('/_sockchat/bans/list')] - public function getBanList(HttpResponseBuilder $response, HttpRequest $request): int|array { + #[ExactRoute('GET', '/_sockchat/bans/list')] + public function getBanList(HttpRequest $request): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); - $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); + $userTime = (int)$request->getFilteredParam('x', FILTER_SANITIZE_NUMBER_INT); $realHash = hash_hmac('sha256', "list#{$userTime}", $this->hashKey); if(!hash_equals($realHash, $userHash) || $userTime < time() - 60) @@ -335,16 +310,16 @@ final class SharpChatRoutes implements RouteHandler { * expires: string * } */ - #[HttpGet('/_sockchat/bans/check')] - public function getBanCheck(HttpResponseBuilder $response, HttpRequest $request): int|array { + #[ExactRoute('GET', '/_sockchat/bans/check')] + public function getBanCheck(HttpRequest $request): int|array { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); - $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); + $userTime = (int)$request->getFilteredParam('x', FILTER_SANITIZE_NUMBER_INT); $ipAddress = (string)$request->getParam('a'); $userId = (string)$request->getParam('u'); - $userIdIsName = (int)$request->getParam('n', FILTER_SANITIZE_NUMBER_INT); + $userIdIsName = (int)$request->getFilteredParam('n', FILTER_SANITIZE_NUMBER_INT); $realHash = hash_hmac('sha256', "check#{$userTime}#{$userId}#{$ipAddress}#{$userIdIsName}", $this->hashKey); if(!hash_equals($realHash, $userHash) || $userTime < time() - 60) @@ -371,23 +346,21 @@ final class SharpChatRoutes implements RouteHandler { ]; } - #[HttpPost('/_sockchat/bans/create')] - public function postBanCreate(HttpResponseBuilder $response, HttpRequest $request): int { + #[ExactRoute('POST', '/_sockchat/bans/create')] + #[Before('input:urlencoded')] + public function postBanCreate(HttpRequest $request, FormContent $content): int { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - if(!($request->content instanceof FormHttpContent)) - return 400; - - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); - $userTime = (int)$request->content->getParam('t', FILTER_SANITIZE_NUMBER_INT); - $userId = (string)$request->content->getParam('ui', FILTER_SANITIZE_NUMBER_INT); - $userAddr = (string)$request->content->getParam('ua'); - $modId = (string)$request->content->getParam('mi', FILTER_SANITIZE_NUMBER_INT); - $modAddr = (string)$request->content->getParam('ma'); - $duration = (int)$request->content->getParam('d', FILTER_SANITIZE_NUMBER_INT); - $isPermanent = (int)$request->content->getParam('p', FILTER_SANITIZE_NUMBER_INT); - $reason = (string)$request->content->getParam('r'); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); + $userTime = (int)$content->getFilteredParam('t', FILTER_SANITIZE_NUMBER_INT); + $userId = (string)$content->getFilteredParam('ui', FILTER_SANITIZE_NUMBER_INT); + $userAddr = (string)$content->getParam('ua'); + $modId = (string)$content->getFilteredParam('mi', FILTER_SANITIZE_NUMBER_INT); + $modAddr = (string)$content->getParam('ma'); + $duration = (int)$content->getFilteredParam('d', FILTER_SANITIZE_NUMBER_INT); + $isPermanent = (int)$content->getFilteredParam('p', FILTER_SANITIZE_NUMBER_INT); + $reason = (string)$content->getParam('r'); $signature = implode('#', [ 'create', $userTime, $userId, $userAddr, @@ -447,13 +420,13 @@ final class SharpChatRoutes implements RouteHandler { return 201; } - #[HttpDelete('/_sockchat/bans/revoke')] - public function deleteBanRevoke(HttpResponseBuilder $response, HttpRequest $request): int { + #[ExactRoute('DELETE', '/_sockchat/bans/revoke')] + public function deleteBanRevoke(HttpRequest $request): int { if(!$request->hasHeader('X-SharpChat-Signature')) return 400; - $userHash = (string)$request->getHeaderFirstLine('X-SharpChat-Signature'); - $userTime = (int)$request->getParam('x', FILTER_SANITIZE_NUMBER_INT); + $userHash = (string)$request->getHeaderLine('X-SharpChat-Signature'); + $userTime = (int)$request->getFilteredParam('x', FILTER_SANITIZE_NUMBER_INT); $type = (string)$request->getParam('t'); $subject = (string)$request->getParam('s'); diff --git a/src/Tools.php b/src/Tools.php index a57e7a5b..b3a641d4 100644 --- a/src/Tools.php +++ b/src/Tools.php @@ -16,7 +16,7 @@ final class Tools { return false; if(isset($parsed['scheme'])) { - $isSecure ??= filter_has_var(INPUT_SERVER, 'HTTPS'); + $isSecure ??= !empty($_SERVER['HTTPS']); // only allow https when secure if($isSecure && $parsed['scheme'] !== 'https') @@ -28,7 +28,7 @@ final class Tools { } if(isset($parsed['host'])) { - $host ??= filter_input(INPUT_SERVER, 'HTTP_HOST'); + $host ??= $_SERVER['HTTP_HOST']; // ensure host is identical if($parsed['host'] !== $host) diff --git a/src/Users/Assets/AssetsRoutes.php b/src/Users/Assets/AssetsRoutes.php index a0693c3e..b2dc7ff3 100644 --- a/src/Users/Assets/AssetsRoutes.php +++ b/src/Users/Assets/AssetsRoutes.php @@ -5,6 +5,7 @@ use InvalidArgumentException; use RuntimeException; use Index\Http\{HttpRequest,HttpResponseBuilder}; use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Routes\{ExactRoute,PatternRoute}; use Index\Urls\{UrlFormat,UrlRegistry,UrlSource,UrlSourceCommon}; use Misuzu\{Misuzu,Perm}; use Misuzu\Auth\AuthInfo; @@ -24,14 +25,14 @@ class AssetsRoutes implements RouteHandler, UrlSource { // allow staff viewing profile to still see banned user assets // should change the Referer check with some query param only applied when needed return $this->authInfo->getPerms('user')->check(Perm::U_USERS_MANAGE) - && parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === $this->urls->format('user-profile'); + && parse_url($request->getHeaderLine('Referer'), PHP_URL_PATH) === $this->urls->format('user-profile'); return true; } /** @return void */ - #[HttpGet('/assets/avatar')] - #[HttpGet('/assets/avatar/([0-9]+)(?:\.[a-z]+)?')] + #[ExactRoute('GET', '/assets/avatar')] + #[PatternRoute('GET', '/assets/avatar/([0-9]+)(?:\.[a-z]+)?')] #[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); @@ -53,8 +54,8 @@ class AssetsRoutes implements RouteHandler, UrlSource { } /** @return string|void */ - #[HttpGet('/assets/profile-background')] - #[HttpGet('/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')] + #[ExactRoute('GET', '/assets/profile-background')] + #[PatternRoute('GET', '/assets/profile-background/([0-9]+)(?:\.[a-z]+)?')] #[UrlFormat('user-background', '/assets/profile-background/<user>')] public function getProfileBackground(HttpResponseBuilder $response, HttpRequest $request, string $userId = '') { try { @@ -78,9 +79,9 @@ class AssetsRoutes implements RouteHandler, UrlSource { } /** @return string|void */ - #[HttpGet('/user-assets.php')] + #[ExactRoute('GET', '/user-assets.php')] public function getUserAssets(HttpResponseBuilder $response, HttpRequest $request) { - $userId = (string)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT); + $userId = (string)$request->getFilteredParam('u', FILTER_SANITIZE_NUMBER_INT); $mode = (string)$request->getParam('m'); if($mode === 'avatar') { @@ -102,8 +103,8 @@ class AssetsRoutes implements RouteHandler, UrlSource { $fileName = $assetInfo->getFileName(); if($assetInfo instanceof UserAssetScalableInterface) { - $dimensions = (int)($request->getParam('res', FILTER_SANITIZE_NUMBER_INT) - ?? $request->getParam('r', FILTER_SANITIZE_NUMBER_INT)); + $dimensions = (int)($request->getFilteredParam('res', FILTER_SANITIZE_NUMBER_INT) + ?? $request->getFilteredParam('r', FILTER_SANITIZE_NUMBER_INT)); if($dimensions > 0) { $assetInfo->ensureScaledExists($dimensions); diff --git a/src/WebFinger/WebFingerRoutes.php b/src/WebFinger/WebFingerRoutes.php index e1f62338..81de66a3 100644 --- a/src/WebFinger/WebFingerRoutes.php +++ b/src/WebFinger/WebFingerRoutes.php @@ -3,7 +3,9 @@ namespace Misuzu\WebFinger; use Index\XArray; use Index\Http\{HttpResponseBuilder,HttpRequest}; -use Index\Http\Routing\{HttpGet,HttpOptions,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\{RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\AccessControl\AccessControl; +use Index\Http\Routing\Routes\ExactRoute; class WebFingerRoutes implements RouteHandler { use RouteHandlerCommon; @@ -69,13 +71,9 @@ class WebFingerRoutes implements RouteHandler { * }[] * } */ - #[HttpOptions('/.well-known/webfinger')] - #[HttpGet('/.well-known/webfinger')] + #[AccessControl] + #[ExactRoute('GET', '/.well-known/webfinger')] public function getWebFinger(HttpResponseBuilder $response, HttpRequest $request): array|int { - $response->setHeader('Access-Control-Allow-Origin', '*'); - if($request->method === 'OPTIONS') - return 204; - $resInfos = $this->registry->resolve(trim((string)$request->getParam('resource'))); if(empty($resInfos)) return 404; diff --git a/templates/auth/login.twig b/templates/auth/login.twig index b749b4a5..3574a769 100644 --- a/templates/auth/login.twig +++ b/templates/auth/login.twig @@ -7,7 +7,7 @@ {% block content %} <form class="container auth__container auth__login js-login-form" method="post" action="{{ url('auth-login') }}"> {{ input_csrf() }} - {{ input_hidden('login[redirect]', login_redirect) }} + {{ input_hidden('redirect', login_redirect) }} <div class="container__title"> <div class="container__title__background"></div> @@ -45,7 +45,7 @@ Username </div> <div class="auth__label__value"> - {{ input_text('login[username]', 'auth__label__input js-login-username', login_username, 'text', '', true, null, 1, not login_welcome) }} + {{ input_text('username', 'auth__label__input js-login-username', login_username, 'text', '', true, null, 1, not login_welcome) }} </div> </label> @@ -57,7 +57,7 @@ {% endif %} </div> <div class="auth__label__value"> - {{ input_text('login[password]', 'auth__label__input', '', 'password', '', true, null, 2, login_welcome) }} + {{ input_text('password', 'auth__label__input', '', 'password', '', true, null, 2, login_welcome) }} </div> </label> diff --git a/templates/auth/password_forgot.twig b/templates/auth/password_forgot.twig index 23b715c4..c280e05c 100644 --- a/templates/auth/password_forgot.twig +++ b/templates/auth/password_forgot.twig @@ -24,7 +24,7 @@ E-mail </div> <div class="auth__label__value"> - {{ input_text('forgot[email]', 'auth__label__input', password_email, 'text', '', true, null, 1) }} + {{ input_text('email', 'auth__label__input', password_email, 'text', '', true, null, 1) }} </div> </label> diff --git a/templates/auth/password_reset.twig b/templates/auth/password_reset.twig index ef4b371d..4ce053c1 100644 --- a/templates/auth/password_reset.twig +++ b/templates/auth/password_reset.twig @@ -8,7 +8,7 @@ <form class="container auth__container auth__password" method="post" action="{{ url('auth-reset') }}"> {{ container_title('<i class="fas fa-user-lock fa-fw"></i> Resetting password for ' ~ password_user.name) }} - {{ input_hidden('reset[user]', password_user.id) }} + {{ input_hidden('user', password_user.id) }} {{ input_csrf() }} {% if password_notices|length > 0 %} @@ -28,14 +28,14 @@ {% endif %} {% if password_verification|length == 12 %} - {{ input_hidden('reset[verification]', password_verification) }} + {{ input_hidden('verification', password_verification) }} {% else %} <label class="auth__label"> <div class="auth__label__text"> Verification Code </div> <div class="auth__label__value"> - {{ input_text('reset[verification]', 'input__text--monospace auth__label__input', '', 'text', '', true, {'maxlength':12}, 1) }} + {{ input_text('verification', 'input__text--monospace auth__label__input', '', 'text', '', true, {'maxlength':12}, 1) }} </div> </label> {% endif %} @@ -45,7 +45,7 @@ New Password </div> <div class="auth__label__value"> - {{ input_text('reset[password][new]', 'auth__label__input', '', 'password', '', true, null, 2) }} + {{ input_text('password_new', 'auth__label__input', '', 'password', '', true, null, 2) }} </div> </label> @@ -54,7 +54,7 @@ Confirm Password </div> <div class="auth__label__value"> - {{ input_text('reset[password][confirm]', 'auth__label__input', '', 'password', '', true, null, 3) }} + {{ input_text('password_confirm', 'auth__label__input', '', 'password', '', true, null, 3) }} </div> </label> diff --git a/templates/auth/register.twig b/templates/auth/register.twig index b99717de..381d4f46 100644 --- a/templates/auth/register.twig +++ b/templates/auth/register.twig @@ -55,7 +55,7 @@ Username </div> <div class="auth__label__value"> - {{ input_text('register[username]', 'auth__label__input', register_username, 'text', '', true, null, 10, true) }} + {{ input_text('username', 'auth__label__input', register_username, 'text', '', true, null, 10, true) }} </div> </label> @@ -64,7 +64,7 @@ Password </div> <div class="auth__label__value"> - {{ input_text('register[password]', 'auth__label__input', '', 'password', '', true, null, 20) }} + {{ input_text('password', 'auth__label__input', '', 'password', '', true, null, 20) }} </div> </label> @@ -73,7 +73,7 @@ Confirm Password </div> <div class="auth__label__value"> - {{ input_text('register[password_confirm]', 'auth__label__input', '', 'password', '', true, null, 30) }} + {{ input_text('password_confirm', 'auth__label__input', '', 'password', '', true, null, 30) }} </div> </label> @@ -82,7 +82,7 @@ E-mail </div> <div class="auth__label__value"> - {{ input_text('register[email]', 'auth__label__input', register_email, 'text', '', true, null, 40) }} + {{ input_text('email', 'auth__label__input', register_email, 'text', '', true, null, 40) }} </div> </label> @@ -91,7 +91,7 @@ What is the outcome of nine plus ten? </div> <div class="auth__label__value"> - {{ input_text('register[question]', 'auth__label__input', '', 'text', '', true, null, 50) }} + {{ input_text('question', 'auth__label__input', '', 'text', '', true, null, 50) }} </div> </label> diff --git a/templates/auth/twofactor.twig b/templates/auth/twofactor.twig index 83a8525b..e2a89d0e 100644 --- a/templates/auth/twofactor.twig +++ b/templates/auth/twofactor.twig @@ -9,8 +9,8 @@ {{ container_title('<i class="fas fa-user-shield fa-fw"></i> Two Factor Authentication') }} {{ input_csrf() }} - {{ input_hidden('twofactor[redirect]', twofactor_redirect) }} - {{ input_hidden('twofactor[token]', twofactor_token) }} + {{ input_hidden('redirect', twofactor_redirect) }} + {{ input_hidden('token', twofactor_token) }} {% if twofactor_notices|length > 0 %} <div class="warning auth__warning"> @@ -27,7 +27,7 @@ Code </div> <div class="auth__label__value"> - {{ input_text('twofactor[code]', 'input__text--monospace input__text--centre auth__label__input', '', 'text', '', true, {'maxlength': 6, 'inputmode': 'numeric'}, 1) }} + {{ input_text('code', 'input__text--monospace input__text--centre auth__label__input', '', 'text', '', true, {'maxlength': 6, 'inputmode': 'numeric'}, 1) }} </div> </label> diff --git a/templates/forum/posting.twig b/templates/forum/posting.twig index 633e7f55..0b09491d 100644 --- a/templates/forum/posting.twig +++ b/templates/forum/posting.twig @@ -8,15 +8,15 @@ {% set is_opening = not is_reply or posting_post.isOriginalPost|default(false) %} {% block content %} - <form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}" class="js-forum-posting"> - {{ input_hidden('post[' ~ (is_reply ? 'topic' : 'forum') ~ ']', is_reply ? posting_topic.id : posting_forum.id) }} - {{ input_hidden('post[mode]', posting_mode) }} + <form method="post" action="{{ url('forum-' ~ (is_reply ? 'post' : 'topic') ~ '-create') }}" class="js-forum-posting" enctype="multipart/form-data"> + {{ input_hidden((is_reply ? 'topic' : 'forum'), is_reply ? posting_topic.id : posting_forum.id) }} + {{ input_hidden('mode', posting_mode) }} {{ input_csrf() }} {{ forum_header( is_reply and not is_opening ? posting_topic.title : input_text( - 'post[title]', + 'title', 'forum__header__input', posting_defaults.title|default(posting_topic.title|default('')), 'text', @@ -30,7 +30,7 @@ ) }} {% if posting_post is defined %} - {{ input_hidden('post[id]', posting_post.info.id) }} + {{ input_hidden('id', posting_post.info.id) }} {% endif %} {% if posting_notices|length > 0 %} @@ -74,25 +74,25 @@ {% endif %} </span> </div> - <textarea name="post[text]" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.info.body|default('')) }}</textarea> + <textarea name="text" class="forum__post__text forum__post__text--edit js-forum-posting-text js-ctrl-enter-submit" placeholder="Type your post content here...">{{ posting_defaults.text|default(posting_post.info.body|default('')) }}</textarea> <div class="forum__post__text js-forum-posting-preview" hidden></div> <div class="forum__post__actions js-forum-posting-actions"></div> <div class="forum__post__options"> <div class="forum__post__settings"> {{ input_select( - 'post[parser]', parser_options(), + 'parser', parser_options(), posting_defaults.parser|default(posting_post.info.bodyFormat|default(posting_user_preferred_parser)).value, null, null, false, 'forum__post__dropdown js-forum-posting-parser' ) }} {% if is_opening and posting_types|length > 1 %} - <select class="input__select forum__post__dropdown" name="post[type]"> + <select class="input__select forum__post__dropdown" name="type"> {% for type_name, type_title in posting_types %} <option value="{{ type_name }}"{% if type_name == posting_topic.typeString|default('discussion') %} selected{% endif %}>{{ type_title }}</option> {% endfor %} </select> {% endif %} {{ input_checkbox( - 'post[signature]', + 'signature', 'Display Signature', posting_defaults.signature is not null ? posting_defaults.signature : ( diff --git a/templates/manage/users/ban.twig b/templates/manage/users/ban.twig index 79e320da..33861a7a 100644 --- a/templates/manage/users/ban.twig +++ b/templates/manage/users/ban.twig @@ -6,7 +6,7 @@ <div class="container"> {{ container_title('<i class="fas fa-ban fa-fw"></i> Issuing a ban on user #' ~ ban_user.id ~ ' ' ~ ban_user.name) }} - <form method="post" enctype="multipart/form-data" action="{{ url('manage-users-ban', {'user': ban_user.id}) }}" class="manage__ban"> + <form method="post" action="{{ url('manage-users-ban', {'user': ban_user.id}) }}" class="manage__ban"> {{ input_csrf() }} <div class="manage__ban__field"> diff --git a/templates/manage/users/note.twig b/templates/manage/users/note.twig index 89145f78..4ff0f492 100644 --- a/templates/manage/users/note.twig +++ b/templates/manage/users/note.twig @@ -6,7 +6,7 @@ <div class="container"> {{ container_title('<i class="fas fa-sticky-note fa-fw"></i> ' ~ (note_new ? ('Adding mod note to ' ~ note_user.name) : ('Editing mod note #' ~ note_info.id))) }} - <form method="post" enctype="multipart/form-data" action="{{ url('manage-users-note', note_new ? {'user': note_user.id} : {'note': note_info.id}) }}" class="manage__note {{ note_new ? 'manage__note--edit' : 'manage__note--view' }}"> + <form method="post" action="{{ url('manage-users-note', note_new ? {'user': note_user.id} : {'note': note_info.id}) }}" class="manage__note {{ note_new ? 'manage__note--edit' : 'manage__note--view' }}"> {{ input_csrf() }} <div class="manage__note__header"> diff --git a/templates/manage/users/warning.twig b/templates/manage/users/warning.twig index 505a9ea2..3b718ecf 100644 --- a/templates/manage/users/warning.twig +++ b/templates/manage/users/warning.twig @@ -6,7 +6,7 @@ <div class="container"> {{ container_title('<i class="fas fa-exclamation-circle fa-fw"></i> Issuing a warning to user #' ~ warn_user.id ~ ' ' ~ warn_user.name) }} - <form method="post" enctype="multipart/form-data" action="{{ url('manage-users-warning', {'user': warn_user.id}) }}" class="manage__warning"> + <form method="post" action="{{ url('manage-users-warning', {'user': warn_user.id}) }}" class="manage__warning"> {{ input_csrf() }} <div class="manage__warning__field"> diff --git a/templates/profile/_layout/header.twig b/templates/profile/_layout/header.twig index 73a60318..f6a47007 100644 --- a/templates/profile/_layout/header.twig +++ b/templates/profile/_layout/header.twig @@ -16,7 +16,7 @@ Select </label> - {{ input_checkbox_raw('avatar[delete]', false, 'profile__header__avatar__check', '', false, {'id':'avatar-delete'}) }} + {{ input_checkbox_raw('avatar_delete', false, 'profile__header__avatar__check', null, false, {'id':'avatar-delete'}) }} <label class="input__button profile__header__avatar__option profile__header__avatar__option--delete" for="avatar-delete"> Remove diff --git a/templates/profile/index.twig b/templates/profile/index.twig index 800304ff..21dfef8e 100644 --- a/templates/profile/index.twig +++ b/templates/profile/index.twig @@ -15,7 +15,7 @@ {{ input_csrf() }} {% if perms.edit_avatar %} - {{ input_file_raw('avatar[file]', 'profile__hidden', ['image/png', 'image/jpeg', 'image/gif'], {'id':'avatar-selection'}) }} + {{ input_file_raw('avatar_file', 'profile__hidden', ['image/png', 'image/jpeg', 'image/gif'], {'id':'avatar-selection'}) }} <script> function updateAvatarPreview(name, url, preview) { @@ -120,7 +120,7 @@ </div> {% if profile_is_editing %} - {{ input_text('profile[' ~ fieldInfo.name ~ ']', 'profile__accounts__input', profile_fields_raw_values[fieldInfo.name]|default('')) }} + {{ input_text('profile_' ~ fieldInfo.name, 'profile__accounts__input', profile_fields_raw_values[fieldInfo.name]|default('')) }} {% else %} <div class="profile__accounts__value"> {% if profile_fields_link_values[fieldInfo.name] is defined %} @@ -212,14 +212,14 @@ <div class="profile__birthdate__title"> Day </div> - {{ input_select('birthdate[day]', ['-']|merge(range(1, 31)), birthdate_info.day|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--day') }} + {{ input_select('birth_day', ['-']|merge(range(1, 31)), birthdate_info.day|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--day') }} </label> <label class="profile__birthdate__label"> <div class="profile__birthdate__title"> Month </div> - {{ input_select('birthdate[month]', ['-']|merge(range(1, 12)), birthdate_info.month|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--month') }} + {{ input_select('birth_month', ['-']|merge(range(1, 12)), birthdate_info.month|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--month') }} </label> </div> @@ -228,7 +228,7 @@ <div class="profile__birthdate__title"> Year (may be left empty) </div> - {{ input_select('birthdate[year]', ['-']|merge(range(null|date('Y'), null|date('Y') - 100)), birthdate_info.year|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--year') }} + {{ input_select('birth_year', ['-']|merge(range(null|date('Y'), null|date('Y') - 100)), birthdate_info.year|default(0), '', '', true, 'profile__birthdate__select profile__birthdate__select--year') }} </label> </div> </div> diff --git a/tools/render-tpl b/tools/render-tpl index 5b2f1bc3..9207b77a 100755 --- a/tools/render-tpl +++ b/tools/render-tpl @@ -2,7 +2,8 @@ <?php namespace Misuzu; -use Index\Http\{HttpHeader,HttpHeaders,HttpRequest}; +use Index\Http\{HttpRequest,HttpUri}; +use Index\Http\Streams\NullStream; require_once __DIR__ . '/../misuzu.php'; @@ -76,10 +77,7 @@ handleValue: $hostName ??= 'localhost'; // this should really not be necessary, mostly done to make sure the url registry is available -$msz->createRouting(new HttpRequest('::1', true, 'XX', '1.1', 'GET', '/', [], [], new HttpHeaders([ - new HttpHeader('Host', $hostName), -]), null)); - +$msz->createRouting(new HttpRequest('1.1', ['Host' => [$hostName]], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], [])); $msz->startTemplating(false); $ctx = $msz->templating->load(implode(' ', array_slice($argv, $pathIndex)));