diff --git a/misuzu.php b/misuzu.php index d0dd009b..981cd347 100644 --- a/misuzu.php +++ b/misuzu.php @@ -46,6 +46,7 @@ require_once 'src/pagination.php'; require_once 'src/perms.php'; require_once 'src/string.php'; require_once 'src/tpl.php'; +require_once 'src/url.php'; require_once 'src/zalgo.php'; require_once 'src/Forum/forum.php'; require_once 'src/Forum/perms.php'; diff --git a/public/auth.php b/public/auth.php index 7900c67d..2213d70f 100644 --- a/public/auth.php +++ b/public/auth.php @@ -66,26 +66,26 @@ switch ($authMode) { case 'reset': // If we're logged in, redirect to the password/e-mail change part in settings instead. if (user_session_active()) { - header('Location: /settings.php#account'); + header(sprintf('Location: %s', url('settings-mode', ['mode' => 'account']))); break; } if (!$canResetPassword) { - header('Location: /'); + header(sprintf('Location: %s', url('index'))); return; } $resetUserId = (int)($_POST['user'] ?? $_GET['u'] ?? 0); if (empty($resetUserId)) { - header('Location: /auth.php?m=forgot'); + header(sprintf('Location: %s', url('auth-forgot'))); break; } $resetUsername = user_username_from_id($resetUserId); if (empty($resetUsername)) { - header('Location: /auth.php'); + header(sprintf('Location: %s', url('auth-login'))); break; } @@ -124,7 +124,7 @@ switch ($authMode) { user_recovery_token_invalidate($resetUserId, $authVerification); - header("Location: /auth.php?m=login&u={$resetUserId}"); + header(sprintf('Location: %s', url('auth-login'))); break; } @@ -192,7 +192,7 @@ MSG; } } - header("Location: ?m=reset&u={$forgotUser['user_id']}"); + header(sprintf('Location: %s', url('auth-reset', ['user' => $forgotUser['user_id']]))); break; } @@ -201,7 +201,7 @@ MSG; case 'login': if (user_session_active()) { - header('Location: /'); + header('Location: ' . url('index')); break; } @@ -272,7 +272,7 @@ MSG; set_cookie_m('sid', $sessionKey, $cookieLife); if (!is_local_url($authRedirect)) { - $authRedirect = '/'; + $authRedirect = url('index'); } header("Location: {$authRedirect}"); @@ -290,7 +290,7 @@ MSG; case 'register': if (user_session_active()) { - header('Location: /'); + header('Location: ' . url('index')); } $authRegistrationError = ''; diff --git a/public/forum/index.php b/public/forum/index.php index 996c643a..8c4468ac 100644 --- a/public/forum/index.php +++ b/public/forum/index.php @@ -11,7 +11,7 @@ switch ($_GET['m'] ?? '') { $markAction = forum_mark_read($markEntireForum ? null : $forumId, user_session_current('user_id', 0)); } - header('Location: /forum' . (!$markAction || $markEntireForum ? '' : url_construct('/forum.php', ['f' => $forumId]))); + header('Location: /forum' . (!$markAction || $markEntireForum ? '' : url('forum-category', ['forum' => $forumId]))); break; default: diff --git a/public/forum/post.php b/public/forum/post.php index 40071f43..1aae7175 100644 --- a/public/forum/post.php +++ b/public/forum/post.php @@ -116,7 +116,10 @@ switch ($postMode) { if (!$isXHR) { if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + header("Location: " . url('forum_post', [ + 'post' => $postInfo['post_id'], + 'post_fragment' => 'p' . $postInfo['post_id'], + ])); break; } elseif (!$postRequestVerified) { echo tpl_render('forum.confirm', [ @@ -152,7 +155,7 @@ switch ($postMode) { break; } - header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + header("Location: " . url('forum_topic', ['topic' => $postInfo['topic_id']])); break; case 'nuke': @@ -163,7 +166,10 @@ switch ($postMode) { if (!$isXHR) { if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + header("Location: " . url('forum_post', [ + 'post' => $postInfo['post_id'], + 'post_fragment' => 'p' . $postInfo['post_id'], + ])); break; } elseif (!$postRequestVerified) { echo tpl_render('forum.confirm', [ @@ -190,7 +196,7 @@ switch ($postMode) { http_response_code(204); if (!$isXHR) { - header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + header("Location: " . url('forum_topic', ['topic' => $postInfo['topic_id']])); } break; @@ -202,7 +208,10 @@ switch ($postMode) { if (!$isXHR) { if ($postRequestVerified && isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?p={$postInfo['post_id']}#p{$postInfo['post_id']}"); + header("Location: " . url('forum_post', [ + 'post' => $postInfo['post_id'], + 'post_fragment' => 'p' . $postInfo['post_id'], + ])); break; } elseif (!$postRequestVerified) { echo tpl_render('forum.confirm', [ @@ -229,7 +238,7 @@ switch ($postMode) { http_response_code(204); if (!$isXHR) { - header('Location: /forum/topic.php?t=' . $postInfo['topic_id']); + header("Location: " . url('forum_topic', ['topic' => $postInfo['topic_id']])); } break; @@ -252,8 +261,8 @@ switch ($postMode) { break; } - header('Location: ' . url_construct('/forum/topic.php', [ - 't' => $postFind['topic_id'], - 'p' => floor($postFind['preceeding_post_count'] / MSZ_FORUM_POSTS_PER_PAGE) + 1, + header('Location: ' . url('forum-topic', [ + 'topic' => $postFind['topic_id'], + 'page' => floor($postFind['preceeding_post_count'] / MSZ_FORUM_POSTS_PER_PAGE) + 1, ])); } diff --git a/public/forum/posting.php b/public/forum/posting.php index c30bf097..1b10db65 100644 --- a/public/forum/posting.php +++ b/public/forum/posting.php @@ -203,7 +203,11 @@ if (!empty($_POST)) { } if (empty($notices)) { - $redirect = '/forum/topic.php' . (empty($topic) ? "?t={$topicId}" : "?p={$postId}#p{$postId}"); + $redirect = url(empty($topic) ? 'forum-topic' : 'forum-post', [ + 'topic' => $topicId ?? 0, + 'post' => $postId ?? 0, + 'post_fragment' => 'p' . ($postId ?? 0), + ]); header("Location: {$redirect}"); return; } diff --git a/public/forum/topic.php b/public/forum/topic.php index a1acf218..0c09f5f3 100644 --- a/public/forum/topic.php +++ b/public/forum/topic.php @@ -151,7 +151,10 @@ if (in_array($moderationMode, $validModerationModes, true)) { if (!$isXHR) { if (isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?t={$topic['topic_id']}"); + header("Location: " . url( + 'forum-topic', + ['topic' => $topic['topic_id']] + )); break; } elseif (!isset($_GET['confirm'])) { echo tpl_render('forum.confirm', [ @@ -187,7 +190,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { break; } - header('Location: /forum/forum.php?f=' . $topic['forum_id']); + header('Location: ' . url('forum-category', [ + 'forum' => $topic['forum_id'], + ])); break; case 'restore': @@ -198,7 +203,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { if (!$isXHR) { if (isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?t={$topic['topic_id']}"); + header("Location: " . url('forum-topic', [ + 'topic' => $topic['topic_id'], + ])); break; } elseif (!isset($_GET['confirm'])) { echo tpl_render('forum.confirm', [ @@ -225,7 +232,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { http_response_code(204); if (!$isXHR) { - header('Location: /forum/forum.php?f=' . $topic['forum_id']); + header('Location: ' . url('forum-category', [ + 'forum' => $topic['forum_id'], + ])); } break; @@ -237,7 +246,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { if (!$isXHR) { if (isset($_GET['confirm']) && $_GET['confirm'] !== '1') { - header("Location: /forum/topic.php?t={$topic['topic_id']}"); + header('Location: ' . url('forum-topic', [ + 'topic' => $topic['topic_id'], + ])); break; } elseif (!isset($_GET['confirm'])) { echo tpl_render('forum.confirm', [ @@ -264,7 +275,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { http_response_code(204); if (!$isXHR) { - header('Location: /forum/forum.php?f=' . $topic['forum_id']); + header('Location: ' . url('forum-category', [ + 'forum' => $topic['forum_id'], + ])); } break; @@ -273,7 +286,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { audit_log(MSZ_AUDIT_FORUM_TOPIC_BUMP, $topicUserId, [$topic['topic_id']]); } - header('Location: /forum/topic.php?t=' . $topic['topic_id']); + header('Location: ' . url('forum-topic', [ + 'topic' => $topic['topic_id'], + ])); break; case 'lock': @@ -281,7 +296,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { audit_log(MSZ_AUDIT_FORUM_TOPIC_LOCK, $topicUserId, [$topic['topic_id']]); } - header('Location: /forum/topic.php?t=' . $topic['topic_id']); + header('Location: ' . url('forum-topic', [ + 'topic' => $topic['topic_id'], + ])); break; case 'unlock': @@ -289,7 +306,9 @@ if (in_array($moderationMode, $validModerationModes, true)) { audit_log(MSZ_AUDIT_FORUM_TOPIC_UNLOCK, $topicUserId, [$topic['topic_id']]); } - header('Location: /forum/topic.php?t=' . $topic['topic_id']); + header('Location: ' . url('forum-topic', [ + 'topic' => $topic['topic_id'], + ])); break; } return; diff --git a/public/profile.php b/public/profile.php index b8abf01f..8f2ec7f7 100644 --- a/public/profile.php +++ b/public/profile.php @@ -355,7 +355,7 @@ switch ($mode) { // If there are no notices, redirect to regular profile. if (empty($notices)) { - header("Location: /profile.php?u={$userId}"); + header('Location: ' . url('user-profile', ['user' => $userId])); return; } } @@ -370,7 +370,7 @@ switch ($mode) { if ($backgroundInfo) { tpl_var('site_background', [ - 'url' => "/profile.php?m=background&u={$userId}", + 'url' => url('user-background', ['user' => $userId]), 'width' => $backgroundInfo[0], 'height' => $backgroundInfo[1], 'settings' => $profile['user_background_settings'], diff --git a/src/TwigMisuzu.php b/src/TwigMisuzu.php index 5a560a0c..3dc89f32 100644 --- a/src/TwigMisuzu.php +++ b/src/TwigMisuzu.php @@ -41,6 +41,8 @@ final class TwigMisuzu extends Twig_Extension new Twig_Function('url_construct', 'url_construct'), new Twig_Function('warning_has_duration', 'user_warning_has_duration'), new Twig_Function('get_csrf_tokens', 'csrf_get_list'), + new Twig_Function('url', 'url'), + new Twig_Function('url_list', 'url_list'), new Twig_Function('startup_time', function (float $time = MSZ_STARTUP) { return microtime(true) - $time; }), diff --git a/src/csrf.php b/src/csrf.php index 59b559b9..bbcd02aa 100644 --- a/src/csrf.php +++ b/src/csrf.php @@ -85,6 +85,13 @@ function csrf_init(string $secretKey, string $identity): void $GLOBALS[MSZ_CSRF_TOKEN_STORE] = []; } +function csrf_is_ready(): bool +{ + return !empty($GLOBALS[MSZ_CSRF_SECRET_STORE]) + && !empty($GLOBALS[MSZ_CSRF_IDENTITY_STORE]) + && is_array($GLOBALS[MSZ_CSRF_TOKEN_STORE]); +} + function csrf_token(string $realm): string { if (array_key_exists($realm, $GLOBALS[MSZ_CSRF_TOKEN_STORE])) { diff --git a/src/url.php b/src/url.php new file mode 100644 index 00000000..97a65586 --- /dev/null +++ b/src/url.php @@ -0,0 +1,188 @@ + Path part of URL. +// [1] => Query part of URL. +// [2] => Fragment part of URL. +// +// text surrounded by < and > will be replaced accordingly to an array of variables supplied to the format function +// text surrounded by [ and ] will be replaced by the constant/define of that name +// text surrounded by { and } will be replaced by a CSRF token with the given text as its realm, this will have no effect in a sessionless environment +define('MSZ_URLS', [ + 'index' => ['/'], + 'info' => ['/info.php/{title}'], + + 'auth-login' => ['/auth.php', ['m' => 'login']], + 'auth-register' => ['/auth.php', ['m' => 'register']], + 'auth-forgot' => ['/auth.php', ['m' => 'forgot']], + 'auth-reset' => ['/auth.php', ['m' => 'reset', 'u' => '']], + 'auth-logout' => ['/auth.php', ['m' => 'logout', 's' => '{logout}']], + + 'changelog-index' => ['/changelog.php'], + 'changelog-date' => ['/changelog.php', ['d' => '']], + 'changelog-tag' => ['/changelog.php', ['t' => '']], + + 'news-post' => ['/news.php', ['p' => '']], + 'news-post-comments' => ['/news.php', ['p' => ''], 'comments'], + 'news-category' => ['/news.php', ['c' => '', 'page' => '']], + 'news-index' => ['/news.php', ['page' => '']], + + 'forum-index' => ['/forum'], + 'forum-mark-global' => ['/forum/index.php', ['m' => 'mark', 'c' => '{forum_mark}']], + 'forum-mark-single' => ['/forum/index.php', ['m' => 'mark', 'c' => '{forum_mark}', 'f' => '']], + 'forum-topic-new' => ['/forum/posting.php', ['f' => '']], + 'forum-reply-new' => ['/forum/posting.php', ['t' => '']], + 'forum-category' => ['/forum/forum.php', ['f' => '', 'p' => '']], + 'forum-topic' => ['/forum/topic.php', ['t' => '', 'page' => '']], + 'forum-post' => ['/forum/topic.php', ['p' => ''], ''], + 'forum-post-delete' => ['/forum/post.php', ['p' => '', 'm' => 'delete']], + 'forum-post-restore' => ['/forum/post.php', ['p' => '', 'm' => 'restore']], + 'forum-post-nuke' => ['/forum/post.php', ['p' => '', 'm' => 'nuke']], + 'forum-post-quote' => ['/forum/posting.php', ['q' => '']], + 'forum-post-edit' => ['/forum/posting.php', ['p' => '', 'm' => 'edit']], + + 'user-profile' => ['/profile.php', ['u' => '']], + 'user-profile-edit' => ['/profile.php', ['u' => '', 'm' => 'edit']], + 'user-avatar' => ['/profile.php', ['u' => '', 'm' => 'avatar']], + 'user-background' => ['/profile.php', ['u' => '', 'm' => 'background']], + 'user-account-standing' => ['/profile.php', ['u' => ''], 'account-standing'], + + 'guest-avatar' => ['/profile.php', ['m' => 'avatar']], + + 'user-relation-none' => ['/relations.php', ['u' => '', 'm' => '[MSZ_USER_RELATION_NONE]', 'c' => '{user_relation}']], + 'user-relation-follow' => ['/relations.php', ['u' => '', 'm' => '[MSZ_USER_RELATION_FOLLOW]', 'c' => '{user_relation}']], + + 'members-role' => ['/members.php', ['r' => '']], + + 'settings-index' => ['/settings.php'], + 'settings-mode' => ['/settings.php', [], ''], + + 'comment-vote' => ['/comments.php', ['c' => '', 'csrf' => '{comments}', 'm' => 'vote', 'v' => '']], + 'comment-delete' => ['/comments.php', ['c' => '', 'csrf' => '{comments}', 'm' => 'delete']], + 'comment-restore' => ['/comments.php', ['c' => '', 'csrf' => '{comments}', 'm' => 'restore']], + 'comment-pin' => ['/comments.php', ['c' => '', 'csrf' => '{comments}', 'm' => 'pin']], + 'comment-unpin' => ['/comments.php', ['c' => '', 'csrf' => '{comments}', 'm' => 'unpin']], + + 'manage-changelog-tag-create' => ['/manage/changelog.php', ['v' => 'tag']], + 'manage-changelog-tag-edit' => ['/manage/changelog.php', ['v' => 'tag', 't' => '']], + 'manage-changelog-action-create' => ['/manage/changelog.php', ['v' => 'action']], + 'manage-changelog-action-edit' => ['/manage/changelog.php', ['v' => 'action', 'a' => '']], + 'manage-changelog-change-create' => ['/manage/changelog.php', ['v' => 'change']], + 'manage-changelog-change-edit' => ['/manage/changelog.php', ['v' => 'change', 'c' => '']], + + 'manage-forum-category-view' => ['/manage/forum.php', ['v' => 'forum', 'f' => '']], + + 'manage-news-category-create' => ['/manage/news.php', ['v' => 'category']], + 'manage-news-category-edit' => ['/manage/news.php', ['v' => 'category', 'c' => '']], + 'manage-news-post-create' => ['/manage/news.php', ['v' => 'post']], + 'manage-news-post-edit' => ['/manage/news.php', ['v' => 'post', 'p' => '']], + + 'manage-user-index' => ['/manage/users.php', ['v' => 'listing']], + 'manage-user-edit' => ['/manage/users.php', ['v' => 'view', 'u' => '']], + + 'manage-role-index' => ['/manage/users.php', ['v' => 'roles']], + 'manage-role-create' => ['/manage/users.php', ['v' => 'role']], + 'manage-role-edit' => ['/manage/users.php', ['v' => 'role', 'r' => '']], + + 'manage-warning-delete' => ['/manage/users.php', ['v' => 'warnings', 'u' => '', 'w' => '', 'm' => 'delete', 'c' => '']], +]); + +function url(string $name, array $variables = []): string +{ + if (!array_key_exists($name, MSZ_URLS)) { + return ''; + } + + $info = MSZ_URLS[$name]; + $url = $info[0]; + + if (!is_string($url)) { + return ''; + } + + if (!empty($info[1]) && is_array($info[1])) { + $url .= '?'; + + foreach ($info[1] as $key => $value) { + $value = url_variable($value, $variables); + + if (empty($value) || ($key === 'page' && $value < 2)) { + continue; + } + + $url .= sprintf('%s=%s&', $key, $value); + } + + $url = trim($url, '?&'); + } + + if (!empty($info[2]) && is_string($info[2])) { + $url .= rtrim(sprintf('#%s', url_variable($info[2], $variables)), '#'); + } + + return $url; +} + +function url_variable(string $value, array $variables): string +{ + if (starts_with($value, '<') && ends_with($value, '>')) { + return $variables[trim($value, '<>')] ?? ''; + } + + if (starts_with($value, '[') && ends_with($value, ']')) { + return constant(trim($value, '[]')); + } + + if (starts_with($value, '{') && ends_with($value, '}') && csrf_is_ready()) { + return csrf_token(trim($value, '{}')); + } + + return $value; +} + +function url_list(): array +{ + $collection = []; + + foreach (MSZ_URLS as $name => $urlInfo) { + $item = [ + 'name' => $name, + 'path' => $urlInfo[0], + 'query' => [], + 'fragment' => $urlInfo[2] ?? '', + ]; + + if (!empty($urlInfo[1]) && is_array($urlInfo[1])) { + foreach ($urlInfo[1] as $name => $value) { + $item['query'][] = [ + 'name' => $name, + 'value' => $value, + ]; + } + } + + $collection[] = $item; + } + + return $collection; +} + +function url_construct(string $url, array $query = [], ?string $fragment = null): string +{ + if (count($query)) { + $url .= mb_strpos($url, '?') !== false ? '&' : '?'; + + foreach ($query as $key => $value) { + if ($value) { + $url .= rawurlencode($key) . '=' . rawurlencode($value) . '&'; + } + } + + $url = mb_substr($url, 0, -1); + } + + if (!empty($fragment)) { + $url .= "#{$fragment}"; + } + + return $url; +} diff --git a/templates/_layout/comments.twig b/templates/_layout/comments.twig index 269740f0..8e59c6af 100644 --- a/templates/_layout/comments.twig +++ b/templates/_layout/comments.twig @@ -53,15 +53,15 @@
{% else %} + href="{{ url('user-profile', {'user':comment.user_id}) }}" + style="background-image:url('{{ url('user-avatar', {'user':comment.user_id}) }}')"> {% endif %} @@ -146,7 +146,7 @@
{% if user|default(null) is null %}
- Please login to comment. + Please login to comment.
{% elseif category|default(null) is null or perms|default(null) is null %}
diff --git a/templates/_layout/header.twig b/templates/_layout/header.twig index e41e304c..8cf71d5e 100644 --- a/templates/_layout/header.twig +++ b/templates/_layout/header.twig @@ -90,7 +90,7 @@
- @@ -123,11 +123,11 @@ {% endfor %} {% if current_user is defined %} - + {% else %} - + {% endif %}
@@ -138,7 +138,7 @@ - diff --git a/templates/auth/auth.twig b/templates/auth/auth.twig index 3dbcb278..1e012e4d 100644 --- a/templates/auth/auth.twig +++ b/templates/auth/auth.twig @@ -23,9 +23,9 @@
{% if auth_restricted == 2 %} - A user is currently in a banned and/or silenced state from the same IP address you're currently visiting the site from. If said user isn't you and you wish to create an account, please contact us! + A user is currently in a banned and/or silenced state from the same IP address you're currently visiting the site from. If said user isn't you and you wish to create an account, please contact us! {% else %} - The IP address from which you are visiting the website appears on our blacklist, you are not allowed to register from this address but if you already have an account you can log in just fine using the form above. If you think this blacklisting is a mistake, please contact us! + The IP address from which you are visiting the website appears on our blacklist, you are not allowed to register from this address but if you already have an account you can log in just fine using the form above. If you think this blacklisting is a mistake, please contact us! {% endif %}
diff --git a/templates/auth/logout.twig b/templates/auth/logout.twig index 6dfbda80..5d80b68a 100644 --- a/templates/auth/logout.twig +++ b/templates/auth/logout.twig @@ -9,7 +9,7 @@

We couldn't verify that you were actually the person attempting to log out.

Press the button below to verify the logout request, otherwise click back in your browser or close this tab.

This error is usually caused by pressing the logout button on a page that's been loaded for a while.

- Log out + Log out
{% endblock %} diff --git a/templates/changelog/change.twig b/templates/changelog/change.twig index 54a35f2b..c5ca47a1 100644 --- a/templates/changelog/change.twig +++ b/templates/changelog/change.twig @@ -27,18 +27,18 @@ {% if change.user_id is not null %} {% endif %} - + Created