Converted all Misuzu style route handlers to Index style ones.

This commit is contained in:
flash 2023-08-04 20:51:02 +00:00
parent 6bfa3d7238
commit cf71129153
20 changed files with 1047 additions and 905 deletions

View file

@ -9,7 +9,26 @@ $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
switch($indexMode) {
case 'mark':
url_redirect($forumId < 1 ? 'forum-mark-global' : 'forum-mark-single', ['forum' => $forumId]);
if(!$msz->isLoggedIn()) {
echo render_error(403);
break;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
forum_mark_read($forumId, (int)$msz->getAuthInfo()->getUserId());
$redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]);
redirect($redirect);
break;
}
Template::render('confirm', [
'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($forumId === 0 ? 'the entire' : 'this') . ' forum as read?',
'return' => url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
'params' => [
'forum' => $forumId,
]
]);
break;
default:

View file

@ -208,5 +208,5 @@ if(!empty($mszLegacyPath) && str_starts_with($mszLegacyPath, $mszLegacyPathPrefi
}
}
$msz->setUpHttp(str_contains($mszRequestPath, '.php'));
$msz->setUpHttp();
$msz->dispatchHttp($request);

View file

@ -1,26 +1,73 @@
<?php
namespace Misuzu\Http\Handlers;
namespace Misuzu\Changelog;
use ErrorException;
use RuntimeException;
use Index\Routing\IRouter;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\Comments;
use Misuzu\Comments\CommentsEx;
use Misuzu\Config\IConfig;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Users\Users;
final class ChangelogRoutes {
private IConfig $config;
private Changelog $changelog;
private Users $users;
private AuthInfo $authInfo;
private Comments $comments;
class ChangelogHandler extends Handler {
private array $userInfos = [];
private array $userColours = [];
public function index($response, $request) {
$filterDate = (string)$request->getParam('date');
$filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$filterTags = (string)$request->getParam('tags');
public function __construct(
IRouter $router,
IConfig $config,
Changelog $changelog,
Users $users,
AuthInfo $authInfo,
Comments $comments
) {
$this->config = $config;
$this->changelog = $changelog;
$this->users = $users;
$this->authInfo = $authInfo;
$this->comments = $comments;
$users = $this->context->getUsers();
$router->get('/changelog', [$this, 'getIndex']);
$router->get('/changelog.rss', [$this, 'getFeedRSS']);
$router->get('/changelog.atom', [$this, 'getFeedAtom']);
$router->get('/changelog/change/:id', [$this, 'getChange']);
$router->get('/changelog.php', function($response, $request) {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) {
$response->redirect(url('changelog-change', ['change' => $changeId]), true);
return;
}
$response->redirect(url('changelog-index', [
'date' => $request->getParam('d'),
'user' => $request->getParam('u', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
}
private function getCommentsInfo(string $categoryName): object {
$comments = new CommentsEx($this->authInfo, $this->comments, $this->users, $this->userInfos, $this->userColours);
return $comments->getCommentsForLayout($categoryName);
}
public function getIndex($response, $request) {
$filterDate = (string)$request->getParam('date');
$filterUser = (string)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
$filterTags = (string)$request->getParam('tags');
if(empty($filterDate))
$filterDate = null;
@ -32,14 +79,14 @@ class ChangelogHandler extends Handler {
return 404;
}
if($filterUser > 0)
if(empty($filterUser))
$filterUser = null;
else
try {
$filterUser = $users->getUser((string)$filterUser, 'id');
$filterUser = $this->users->getUser($filterUser, 'id');
} catch(RuntimeException $ex) {
return 404;
}
else
$filterUser = null;
if(empty($filterTags))
$filterTags = null;
@ -49,13 +96,12 @@ class ChangelogHandler extends Handler {
$tag = trim($tag);
}
$changelog = $this->context->getChangelog();
$count = $changelog->countAllChanges($filterUser, $filterDate, $filterTags);
$count = $this->changelog->countAllChanges($filterUser, $filterDate, $filterTags);
$pagination = new Pagination($count, 30);
if(!$pagination->hasValidOffset())
return 404;
$changeInfos = $changelog->getAllChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
$changeInfos = $this->changelog->getAllChanges(userInfo: $filterUser, dateTime: $filterDate, tags: $filterTags, pagination: $pagination);
if(empty($changeInfos))
return 404;
@ -69,8 +115,8 @@ class ChangelogHandler extends Handler {
$userColour = $this->userColours[$userId];
} else {
try {
$userInfo = $users->getUser($userId, 'id');
$userColour = $users->getUserColour($userInfo);
$userInfo = $this->users->getUser($userId, 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
@ -87,49 +133,42 @@ class ChangelogHandler extends Handler {
];
}
$response->setContent(Template::renderRaw('changelog.index', [
return Template::renderRaw('changelog.index', [
'changelog_infos' => $changes,
'changelog_date' => $filterDate,
'changelog_user' => $filterUser,
'changelog_tags' => $filterTags,
'changelog_pagination' => $pagination,
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
]));
]);
}
private function getCommentsInfo(string $categoryName): object {
$comments = new CommentsEx($this->context, $this->context->getComments(), $this->context->getUsers(), $this->userInfos, $this->userColours);
return $comments->getCommentsForLayout($categoryName);
}
public function change($response, $request, string $changeId) {
public function getChange($response, $request, string $changeId) {
try {
$changeInfo = $this->context->getChangelog()->getChangeById($changeId, withTags: true);
$changeInfo = $this->changelog->getChangeById($changeId, withTags: true);
} catch(RuntimeException $ex) {
return 404;
}
$users = $this->context->getUsers();
try {
$userInfo = $users->getUser($changeInfo->getUserId(), 'id');
$userColour = $users->getUserColour($userInfo);
$userInfo = $this->users->getUser($changeInfo->getUserId(), 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
}
$response->setContent(Template::renderRaw('changelog.change', [
return Template::renderRaw('changelog.change', [
'change_info' => $changeInfo,
'change_user_info' => $userInfo,
'change_user_colour' => $userColour,
'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
]));
]);
}
private function createFeed(string $feedMode): Feed {
$siteName = $this->context->getConfig()->getString('site.name', 'Misuzu');
$changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$siteName = $this->config->getString('site.name', 'Misuzu');
$changes = $this->changelog->getAllChanges(pagination: new Pagination(10));
$feed = (new Feed)
->setTitle($siteName . ' » Changelog')
@ -154,13 +193,13 @@ class ChangelogHandler extends Handler {
return $feed;
}
public function feedAtom($response, $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(self::createFeed('atom'));
public function getFeedRSS($response) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss'));
}
public function feedRss($response, $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(self::createFeed('rss'));
public function getFeedAtom($response) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed($this->createFeed('atom'));
}
}

View file

@ -4,11 +4,12 @@ namespace Misuzu\Comments;
use stdClass;
use RuntimeException;
use Misuzu\MisuzuContext;
use Misuzu\Auth\AuthInfo;
use Misuzu\Users\Users;
class CommentsEx {
public function __construct(
private MisuzuContext $context,
private AuthInfo $authInfo,
private Comments $comments,
private Users $users,
private array $userInfos = [],
@ -20,8 +21,8 @@ class CommentsEx {
if(is_string($category))
$category = $this->comments->ensureCategory($category);
$hasUser = $this->context->isLoggedIn();
$info->user = $hasUser ? $this->context->getActiveUser() : null;
$hasUser = $this->authInfo->isLoggedIn();
$info->user = $hasUser ? $this->authInfo->getUserInfo() : null;
$info->colour = $hasUser ? $this->users->getUserColour($info->user) : null;
$info->perms = $hasUser ? perms_for_comments($info->user->getId()) : [];
$info->category = $category;

283
src/Home/HomeRoutes.php Normal file
View file

@ -0,0 +1,283 @@
<?php
namespace Misuzu\Home;
use Index\DateTime;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Routing\IRouter;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo;
use Misuzu\Changelog\Changelog;
use Misuzu\Comments\Comments;
use Misuzu\Config\IConfig;
use Misuzu\Counters\Counters;
use Misuzu\News\News;
use Misuzu\Users\Users;
class HomeRoutes {
private IConfig $config;
private IDbConnection $dbConn;
private AuthInfo $authInfo;
private Changelog $changelog;
private Comments $comments;
private Counters $counters;
private News $news;
private Users $users;
public function __construct(
IRouter $router,
IConfig $config,
IDbConnection $dbConn,
AuthInfo $authInfo,
Changelog $changelog,
Comments $comments,
Counters $counters,
News $news,
Users $users
) {
$this->config = $config;
$this->dbConn = $dbConn;
$this->authInfo = $authInfo;
$this->changelog = $changelog;
$this->comments = $comments;
$this->counters = $counters;
$this->news = $news;
$this->users = $users;
$router->get('/', [$this, 'getIndex']);
if(MSZ_DEBUG)
$router->get('/dev-landing', [$this, 'getLanding']);
$router->get('/index.php', function($response) {
$response->redirect(url('index'), true);
});
}
private function getStats(): array {
return $this->counters->get([
'users:active',
'users:online:recent',
'users:online:today',
'comments:posts:visible',
'forum:topics:visible',
'forum:posts:visible',
]);
}
private function getOnlineUsers(): array {
return $this->users->getUsers(
lastActiveInMinutes: 5,
deleted: false,
orderBy: 'random',
);
}
private array $userInfos = [];
private array $userColours = [];
private array $newsCategoryInfos = [];
private function getFeaturedNewsPosts(int $amount, bool $decorate): array {
$postInfos = $this->news->getAllPosts(
onlyFeatured: true,
pagination: new Pagination($amount)
);
if(!$decorate)
return $postInfos;
$posts = [];
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
$userColour = $this->userColours[$userId];
} else {
try {
$userInfo = $this->users->getUser($userId, 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
}
$this->userInfos[$userId] = $userInfo;
$this->userColours[$userId] = $userColour;
}
if(array_key_exists($categoryId, $this->newsCategoryInfos))
$categoryInfo = $this->newsCategoryInfos[$categoryId];
else
$this->newsCategoryInfos[$categoryId] = $categoryInfo = $this->news->getCategoryByPost($postInfo);
$commentsCount = $postInfo->hasCommentsCategoryId()
? $this->comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$posts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount,
];
}
return $posts;
}
public function getPopularForumTopics(array $categoryIds): array {
$args = 0;
$stmt = $this->dbConn->prepare(
'SELECT t.topic_id, c.forum_id, t.topic_title, c.forum_icon, t.topic_count_views'
. ', (SELECT COUNT(*) FROM msz_forum_posts AS p WHERE p.topic_id = t.topic_id AND post_deleted IS NULL)'
. ' FROM msz_forum_topics AS t'
. ' LEFT JOIN msz_forum_categories AS c ON c.forum_id = t.forum_id'
. ' WHERE c.forum_id IN (' . DbTools::prepareListString($categoryIds) . ') AND topic_deleted IS NULL AND topic_locked IS NULL'
. ' ORDER BY (SELECT COUNT(*) FROM msz_forum_posts AS p WHERE p.topic_id = t.topic_id AND post_deleted IS NULL AND post_created > NOW() - INTERVAL 3 MONTH) DESC'
. ' LIMIT 10'
);
foreach($categoryIds as $categoryId)
$stmt->addParameter(++$args, (string)$categoryId);
$stmt->execute();
$topics = [];
$result = $stmt->getResult();
while($result->next())
$topics[] = [
'topic_id' => $result->getInteger(0),
'forum_id' => $result->getInteger(1),
'topic_title' => $result->getString(2),
'forum_icon' => $result->getString(3),
'topic_count_views' => $result->getInteger(4),
'topic_count_posts' => $result->getInteger(5),
];
return $topics;
}
public function getActiveForumTopics(array $categoryIds): array {
$args = 0;
$stmt = $this->dbConn->prepare(
'SELECT t.topic_id, c.forum_id, t.topic_title, c.forum_icon, t.topic_count_views'
. ', (SELECT COUNT(*) FROM msz_forum_posts AS p WHERE p.topic_id = t.topic_id AND post_deleted IS NULL)'
. ', (SELECT MAX(post_id) FROM msz_forum_posts AS p WHERE p.topic_id = t.topic_id AND post_deleted IS NULL)'
. ' FROM msz_forum_topics AS t'
. ' LEFT JOIN msz_forum_categories AS c ON c.forum_id = t.forum_id'
. ' WHERE c.forum_id IN (' . DbTools::prepareListString($categoryIds) . ') AND topic_deleted IS NULL AND topic_locked IS NULL'
. ' ORDER BY topic_bumped DESC'
. ' LIMIT 10'
);
foreach($categoryIds as $categoryId)
$stmt->addParameter(++$args, (string)$categoryId);
$stmt->execute();
$topics = [];
$result = $stmt->getResult();
while($result->next())
$topics[] = [
'topic_id' => $result->getInteger(0),
'forum_id' => $result->getInteger(1),
'topic_title' => $result->getString(2),
'forum_icon' => $result->getString(3),
'topic_count_views' => $result->getInteger(4),
'topic_count_posts' => $result->getInteger(5),
'latest_post_id' => $result->getInteger(6),
];
return $topics;
}
public function getIndex(...$args) {
return $this->authInfo->isLoggedIn()
? $this->getHome(...$args)
: $this->getLanding(...$args);
}
public function getHome() {
$stats = $this->getStats();
$onlineUserInfos = $this->getOnlineUsers();
$featuredNews = $this->getFeaturedNewsPosts(5, true);
$changelog = $this->changelog->getAllChanges(pagination: new Pagination(10));
$stats['users:online:recent'] = count($onlineUserInfos);
$birthdays = [];
$birthdayInfos = $this->users->getUsers(deleted: false, birthdate: DateTime::now(), orderBy: 'random');
foreach($birthdayInfos as $birthdayInfo)
$birthdays[] = [
'info' => $birthdayInfo,
'colour' => $this->users->getUserColour($birthdayInfo),
];
$newestMember = [];
if(empty($birthdays)) {
$newestMemberId = $this->config->getString('users.newest');
if(!empty($newestMemberId))
try {
$newestMemberInfo = $this->users->getUser($newestMemberId, 'id');
$newestMemberColour = $this->users->getUserColour($newestMemberInfo);
$newestMember['info'] = $newestMemberInfo;
$newestMember['colour'] = $newestMemberColour;
} catch(RuntimeException $ex) {
$newestMember = [];
$config->removeValues('users.newest');
}
}
return Template::renderRaw('home.home', [
'statistics' => $stats,
'newest_member' => $newestMember,
'online_users' => $onlineUserInfos,
'birthdays' => $birthdays,
'featured_changelog' => $changelog,
'featured_news' => $featuredNews,
]);
}
public function getLanding() {
$config = $this->config->getValues([
['social.embed_linked:b'],
['landing.forum_categories:a'],
['site.name:s', 'Misuzu'],
'site.url:s',
'site.ext_logo:s',
'social.linked:a'
]);
if($config['social.embed_linked']) {
$linkedData = [
'@context' => 'http://schema.org',
'@type' => 'Organization',
'name' => $config['site.name'],
'url' => $config['site.url'],
'logo' => $config['site.ext_logo'],
'same_as' => $config['social.linked'],
];
} else $linkedData = null;
$stats = $this->getStats();
$onlineUserInfos = $this->getOnlineUsers();
$featuredNews = $this->getFeaturedNewsPosts(3, false);
$popularTopics = $this->getPopularForumTopics($config['landing.forum_categories']);
$activeTopics = $this->getActiveForumTopics($config['landing.forum_categories']);
$stats['users:online:recent'] = count($onlineUserInfos);
return Template::renderRaw('home.landing', [
'statistics' => $stats,
'online_users' => $onlineUserInfos,
'featured_news' => $featuredNews,
'linked_data' => $linkedData,
'forum_popular' => $popularTopics,
'forum_active' => $activeTopics,
]);
}
}

View file

@ -1,107 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use RuntimeException;
use Misuzu\GitInfo;
use Misuzu\Users\UserInfo;
use Misuzu\Users\Assets\StaticUserImageAsset;
use Misuzu\Users\Assets\UserAssetScalableInterface;
use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset;
use Misuzu\Users\Assets\UserImageAssetInterface;
final class AssetsHandler extends Handler {
private function canViewAsset($request, UserInfo $assetUser): bool {
return !$this->context->hasActiveBan($assetUser) || (
$this->context->isLoggedIn()
&& parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === url('user-profile')
&& perms_check_user(MSZ_PERMS_USER, $this->context->getActiveUser()->getId(), MSZ_PERM_USER_MANAGE_USERS)
);
}
private function serveUserAsset($response, $request, UserImageAssetInterface $assetInfo): void {
$contentType = $assetInfo->getMimeType();
$publicPath = $assetInfo->getPublicPath();
$fileName = $assetInfo->getFileName();
if($assetInfo instanceof UserAssetScalableInterface) {
$dimensions = (int)($request->getParam('res', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('r', FILTER_SANITIZE_NUMBER_INT));
if($dimensions > 0) {
$assetInfo->ensureScaledExists($dimensions);
$contentType = $assetInfo->getScaledMimeType($dimensions);
$publicPath = $assetInfo->getPublicScaledPath($dimensions);
$fileName = $assetInfo->getScaledFileName($dimensions);
}
}
$response->accelRedirect($publicPath);
$response->setContentType($contentType);
$response->setFileName($fileName, false);
}
public function serveAvatar($response, $request, string $fileName) {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
if($type !== '' && $type !== 'png')
return 404;
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try {
$userInfo = $this->context->getUsers()->getUser($userId, 'id');
if(!$this->canViewAsset($request, $userInfo)) {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC);
} else {
$userAssetInfo = new UserAvatarAsset($userInfo);
if($userAssetInfo->isPresent())
$assetInfo = $userAssetInfo;
}
} catch(RuntimeException $ex) {}
$this->serveUserAsset($response, $request, $assetInfo);
}
public function serveProfileBackground($response, $request, string $fileName) {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
if($type !== '' && $type !== 'png')
return 404;
try {
$userInfo = $this->context->getUsers()->getUser($userId, 'id');
} catch(RuntimeException $ex) {}
if(!empty($userInfo)) {
$userAssetInfo = new UserBackgroundAsset($userInfo);
if($userAssetInfo->isPresent() && $this->canViewAsset($request, $userInfo))
$assetInfo = $userAssetInfo;
}
if(!isset($assetInfo)) {
$response->setContent('');
return 404;
}
$this->serveUserAsset($response, $request, $assetInfo);
}
public function serveLegacy($response, $request) {
$assetUserId = $request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
switch($request->getParam('m')) {
case 'avatar':
$this->serveAvatar($response, $request, $assetUserId);
return;
case 'background':
$this->serveProfileBackground($response, $request, $assetUserId);
return;
}
$response->setContent('');
return 404;
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use Misuzu\CSRF;
use Misuzu\Template;
final class ForumHandler extends Handler {
public function markAsReadGET($response, $request) {
if(!$this->context->isLoggedIn())
return 403;
$forumId = (int)$request->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
$response->setContent(Template::renderRaw('confirm', [
'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($forumId === 0 ? 'the entire' : 'this') . ' forum as read?',
'return' => url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
'params' => [
'forum' => $forumId,
]
]));
}
public function markAsReadPOST($response, $request) {
if(!$this->context->isLoggedIn())
return 403;
if(!$request->isFormContent())
return 400;
$token = $request->getContent()->getParam('_csrf');
if(empty($token) || !CSRF::validate($token))
return 400;
$forumId = (int)$request->getContent()->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
forum_mark_read($forumId, (int)$this->context->getActiveUser()->getId());
$redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]);
$response->redirect($redirect, false);
}
}

View file

@ -1,12 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use Misuzu\MisuzuContext;
abstract class Handler {
protected MisuzuContext $context;
public function __construct(MisuzuContext $context) {
$this->context = $context;
}
}

View file

@ -1,208 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use RuntimeException;
use Index\DateTime;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
final class HomeHandler extends Handler {
private const STATS = [
'users:active',
'users:online:recent',
'users:online:today',
'comments:posts:visible',
'forum:topics:visible',
'forum:posts:visible',
];
public function index($response, $request): void {
if($this->context->isLoggedIn())
$this->home($response, $request);
else
$this->landing($response, $request);
}
public function landing($response, $request): void {
$users = $this->context->getUsers();
$config = $this->context->getConfig();
$counters = $this->context->getCounters();
if($config->getBoolean('social.embed_linked')) {
$ldr = $config->getValues([
['site.name:s', 'Misuzu'],
'site.url:s',
'site.ext_logo:s',
'social.linked:a'
]);
$linkedData = [
'name' => $ldr['site.name'],
'url' => $ldr['site.url'],
'logo' => $ldr['site.ext_logo'],
'same_as' => $ldr['social.linked'],
];
} else $linkedData = null;
$featuredNews = $this->context->getNews()->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(3)
);
$stats = $counters->get(self::STATS);
$onlineUserInfos = $users->getUsers(
lastActiveInMinutes: 5,
deleted: false,
orderBy: 'random',
);
// can also cheat here, whoa
$stats['users:online:recent'] = count($onlineUserInfos);
// TODO: don't hardcode forum ids
$featuredForums = $config->getArray('landing.forum_categories');
$popularTopics = [];
$activeTopics = [];
if(!empty($featuredForums)) {
$getPopularTopics = DB::prepare(
'SELECT t.`topic_id`, c.`forum_id`, t.`topic_title`, c.`forum_icon`, t.`topic_count_views`'
. ', (SELECT COUNT(*) FROM `msz_forum_posts` AS p WHERE p.`topic_id` = t.`topic_id` AND `post_deleted` IS NULL) AS `topic_count_posts`'
. ' FROM `msz_forum_topics` AS t'
. ' LEFT JOIN `msz_forum_categories` AS c ON c.`forum_id` = t.`forum_id`'
. ' WHERE c.`forum_id` IN (' . implode(',', $featuredForums) . ') AND `topic_deleted` IS NULL AND `topic_locked` IS NULL'
. ' ORDER BY (SELECT COUNT(*) FROM `msz_forum_posts` AS p WHERE p.`topic_id` = t.`topic_id` AND `post_deleted` IS NULL AND `post_created` > NOW() - INTERVAL 3 MONTH) DESC'
)->stmt;
$getPopularTopics->execute();
for($i = 0; $i < 10; ++$i) {
$topicInfo = $getPopularTopics->fetchObject();
if(empty($topicInfo))
break;
$popularTopics[] = $topicInfo;
}
$getActiveTopics = DB::prepare(
'SELECT t.`topic_id`, c.`forum_id`, t.`topic_title`, c.`forum_icon`, t.`topic_count_views`'
. ', (SELECT COUNT(*) FROM `msz_forum_posts` AS p WHERE p.`topic_id` = t.`topic_id` AND `post_deleted` IS NULL) AS `topic_count_posts`'
. ', (SELECT MAX(`post_id`) FROM `msz_forum_posts` AS p WHERE p.`topic_id` = t.`topic_id` AND `post_deleted` IS NULL) AS `latest_post_id`'
. ' FROM `msz_forum_topics` AS t'
. ' LEFT JOIN `msz_forum_categories` AS c ON c.`forum_id` = t.`forum_id`'
. ' WHERE c.`forum_id` IN (' . implode(',', $featuredForums) . ') AND `topic_deleted` IS NULL AND `topic_locked` IS NULL'
. ' ORDER BY `topic_bumped` DESC'
)->stmt;
$getActiveTopics->execute();
for($i = 0; $i < 10; ++$i) {
$topicInfo = $getActiveTopics->fetchObject();
if(empty($topicInfo))
break;
$activeTopics[] = $topicInfo;
}
}
$response->setContent(Template::renderRaw('home.landing', [
'statistics' => $stats,
'online_users' => $onlineUserInfos,
'featured_news' => $featuredNews,
'linked_data' => $linkedData,
'forum_popular' => $popularTopics,
'forum_active' => $activeTopics,
]));
}
public function home($response, $request): void {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$config = $this->context->getConfig();
$comments = $this->context->getComments();
$counters = $this->context->getCounters();
$featuredNews = [];
$userInfos = [];
$userColours = [];
$categoryInfos = [];
$featuredNewsInfos = $news->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(5)
);
foreach($featuredNewsInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
$userColour = $userColours[$userId];
} else {
try {
$userInfo = $users->getUser($userId, 'id');
$userColour = $userColours[$userId] = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = $userColours[$userId] = null;
}
$userInfos[$userId] = $userInfo;
}
if(array_key_exists($categoryId, $categoryInfos))
$categoryInfo = $categoryInfos[$categoryId];
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = $postInfo->hasCommentsCategoryId()
? $comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$featuredNews[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount,
];
}
$stats = $counters->get(self::STATS);
$changelog = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
$birthdays = [];
$birthdayInfos = $users->getUsers(deleted: false, birthdate: DateTime::now(), orderBy: 'random');
foreach($birthdayInfos as $birthdayInfo)
$birthdays[] = [
'info' => $birthdayInfo,
'colour' => $users->getUserColour($birthdayInfo),
];
$newestMember = [];
if(empty($birthdays)) {
$newestMemberId = $config->getString('users.newest');
if(!empty($newestMemberId))
try {
$newestMemberInfo = $users->getUser($newestMemberId, 'id');
$newestMember['info'] = $newestMemberInfo;
$newestMember['colour'] = $users->getUserColour($newestMemberInfo);
} catch(RuntimeException $ex) {
$newestMember = [];
$config->removeValues('users.newest');
}
}
$onlineUserInfos = $users->getUsers(
lastActiveInMinutes: 5,
deleted: false,
orderBy: 'random',
);
// today we cheat
$stats['users:online:recent'] = count($onlineUserInfos);
$response->setContent(Template::renderRaw('home.home', [
'statistics' => $stats,
'newest_member' => $newestMember,
'online_users' => $onlineUserInfos,
'birthdays' => $birthdays,
'featured_changelog' => $changelog,
'featured_news' => $featuredNews,
]));
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use Misuzu\Template;
use Misuzu\Parsers\Parser;
final class InfoHandler extends Handler {
public function index($response): void {
$response->setContent(Template::renderRaw('info.index'));
}
public function page($response, $request, string ...$parts) {
$name = implode('/', $parts);
$document = [
'content' => '',
'title' => '',
];
$isIndexDoc = $name === 'index' || str_starts_with($name, 'index/');
$isMisuzuDoc = $name === 'misuzu' || str_starts_with($name, 'misuzu/');
if($isMisuzuDoc) {
$fileName = substr($name, 7);
$fileName = empty($fileName) ? 'README' : strtoupper($fileName);
if($fileName !== 'README')
$titleSuffix = ' - Misuzu Project';
} elseif($isIndexDoc) {
$fileName = substr($name, 6);
$fileName = empty($fileName) ? 'README' : strtoupper($fileName);
if($fileName !== 'README')
$titleSuffix = ' - Index Project';
} else $fileName = strtolower($name);
if(!preg_match('#^([A-Za-z0-9_]+)$#', $fileName))
return 404;
if($fileName !== 'LICENSE' && $fileName !== 'LICENCE')
$fileName .= '.md';
$pfx = '';
if($isIndexDoc)
$pfx = '/vendor/flashwave/index';
elseif(!$isMisuzuDoc)
$pfx = '/docs';
$fileName = MSZ_ROOT . $pfx . '/' . $fileName;
$document['content'] = is_file($fileName) ? file_get_contents($fileName) : '';
if(empty($document['content']))
return 404;
if($document['title'] === '') {
if(str_starts_with($document['content'], '# ')) {
$titleOffset = strpos($document['content'], "\n");
$document['title'] = trim(substr($document['content'], 2, $titleOffset - 1));
$document['content'] = substr($document['content'], $titleOffset);
} else
$document['title'] = ucfirst(basename($fileName));
if(!empty($titleSuffix))
$document['title'] .= $titleSuffix;
}
$document['content'] = Parser::instance(Parser::MARKDOWN)->parseText($document['content']);
$response->setContent(Template::renderRaw('info.view', [
'document' => $document,
]));
}
}

View file

@ -1,282 +0,0 @@
<?php
namespace Misuzu\Http\Handlers;
use RuntimeException;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsEx;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\NewsCategoryInfo;
use Misuzu\Parsers\Parser;
final class NewsHandler extends Handler {
private function fetchPostInfo(array $postInfos, array $categoryInfos = []): array {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments();
$posts = [];
$userInfos = [];
$userColours = [];
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
$userColour = $userColours[$userId];
} else {
try {
$userInfo = $users->getUser($userId, 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userInfo = null;
$userColour = null;
}
$userInfos[$userId] = $userInfo;
$userColours[$userId] = $userColour;
}
if(array_key_exists($categoryId, $categoryInfos))
$categoryInfo = $categoryInfos[$categoryId];
else
$categoryInfos[$categoryId] = $categoryInfo = $news->getCategoryByPost($postInfo);
$commentsCount = $postInfo->hasCommentsCategoryId()
? $comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true) : 0;
$posts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount,
];
}
return $posts;
}
public function index($response, $request) {
$news = $this->context->getNews();
$categories = $news->getAllCategories();
$pagination = new Pagination($news->countAllPosts(onlyFeatured: true), 5);
if(!$pagination->hasValidOffset())
return 404;
$postInfos = $news->getAllPosts(onlyFeatured: true, pagination: $pagination);
$posts = $this->fetchPostInfo($postInfos);
$response->setContent(Template::renderRaw('news.index', [
'news_categories' => $categories,
'news_posts' => $posts,
'news_pagination' => $pagination,
]));
}
public function viewCategory($response, $request, string $fileName) {
$news = $this->context->getNews();
$categoryId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
try {
$categoryInfo = $news->getCategoryById($categoryId);
} catch(RuntimeException $ex) {
return 404;
}
if($type === 'atom')
return $this->feedCategoryAtom($response, $request, $categoryInfo);
elseif($type === 'rss')
return $this->feedCategoryRss($response, $request, $categoryInfo);
elseif($type !== '')
return 404;
$pagination = new Pagination($news->countPostsByCategory($categoryInfo), 5);
if(!$pagination->hasValidOffset())
return 404;
$postInfos = $news->getPostsByCategory($categoryInfo, pagination: $pagination);
$posts = $this->fetchPostInfo($postInfos, [$categoryInfo->getId() => $categoryInfo]);
$response->setContent(Template::renderRaw('news.category', [
'news_category' => $categoryInfo,
'news_posts' => $posts,
'news_pagination' => $pagination,
]));
}
public function viewPost($response, $request, string $postId) {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$comments = $this->context->getComments();
try {
$postInfo = $news->getPostById($postId);
} catch(RuntimeException $ex) {
return 404;
}
if(!$postInfo->isPublished() || $postInfo->isDeleted())
return 404;
$categoryInfo = $news->getCategoryByPost($postInfo);
$comments = $this->context->getComments();
if($postInfo->hasCommentsCategoryId())
try {
$commentsCategory = $comments->getCategoryById($postInfo->getCommentsCategoryId());
} catch(RuntimeException $ex) {}
if(!isset($commentsCategory)) {
$commentsCategory = $comments->ensureCategory($postInfo->getCommentsCategoryName());
$news->updatePostCommentCategory($postInfo, $commentsCategory);
}
$userInfo = null;
$userColour = null;
if($postInfo->hasUserId())
try {
$userInfo = $users->getUser($postInfo->getUserId(), 'id');
$userColour = $users->getUserColour($userInfo);
} catch(RuntimeException $ex) {}
$comments = new CommentsEx($this->context, $comments, $users);
$response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo,
'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo,
'post_user_colour' => $userColour,
'comments_info' => $comments->getCommentsForLayout($commentsCategory),
]));
}
private function createFeed(string $feedMode, ?NewsCategoryInfo $categoryInfo, array $posts): Feed {
$hasCategory = $categoryInfo !== null;
$siteName = $this->context->getConfig()->getString('site.name', 'Misuzu');
$feed = (new Feed)
->setTitle($siteName . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
->setDescription($hasCategory ? $categoryInfo->getDescription() : 'A live featured news feed.')
->setContentUrl(url_prefix(false) . ($hasCategory ? url('news-category', ['category' => $categoryInfo->getId()]) : url('news-index')))
->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedMode}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedMode}")));
foreach($posts as $post) {
$postInfo = $post['post'];
$userInfo = $post['user'];
$userId = 0;
$userName = 'Author';
if($userInfo !== null) {
$userId = $userInfo->getId();
$userName = $userInfo->getName();
}
$postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]);
$commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $postInfo->getId()]);
$authorUrl = url_prefix(false) . url('user-profile', ['user' => $userId]);
$feedItem = (new FeedItem)
->setTitle($postInfo->getTitle())
->setSummary($postInfo->getFirstParagraph())
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()))
->setCreationDate($postInfo->getCreatedTime())
->setUniqueId($postUrl)
->setContentUrl($postUrl)
->setCommentsUrl($commentsUrl)
->setAuthorName($userName)
->setAuthorUrl($authorUrl);
if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate())
$feed->setLastUpdate($feedItem->getCreationDate());
$feed->addItem($feedItem);
}
return $feed;
}
private function fetchPostInfoForFeed(array $postInfos): array {
$news = $this->context->getNews();
$users = $this->context->getUsers();
$posts = [];
$userInfos = [];
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
if(array_key_exists($userId, $userInfos)) {
$userInfo = $userInfos[$userId];
} else {
try {
$userInfo = $users->getUser($userId, 'id');
} catch(RuntimeException $ex) {
$userInfo = null;
}
$userInfos[$userId] = $userInfo;
}
$posts[] = [
'post' => $postInfo,
'user' => $userInfo,
];
}
return $posts;
}
private function getFeaturedPostsForFeed(): array {
return $this->fetchPostInfoForFeed(
$this->context->getNews()->getAllPosts(
onlyFeatured: true,
pagination: new Pagination(10)
)
);
}
public function feedIndexAtom($response, $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', null, $this->getFeaturedPostsForFeed())
);
}
public function feedIndexRss($response, $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', null, $this->getFeaturedPostsForFeed())
);
}
private function getCategoryPostsForFeed(NewsCategoryInfo $categoryInfo): array {
return $this->fetchPostInfoForFeed(
$this->context->getNews()->getPostsByCategory($categoryInfo, pagination: new Pagination(10))
);
}
public function feedCategoryAtom($response, $request, NewsCategoryInfo $categoryInfo) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo))
);
}
public function feedCategoryRss($response, $request, NewsCategoryInfo $categoryInfo) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', $categoryInfo, $this->getCategoryPostsForFeed($categoryInfo))
);
}
}

122
src/Info/InfoRoutes.php Normal file
View file

@ -0,0 +1,122 @@
<?php
namespace Misuzu\Info;
use Index\Routing\IRouter;
use Misuzu\Template;
use Misuzu\Parsers\Parser;
class InfoRoutes {
private const DOCS_PATH = MSZ_ROOT . '/docs';
private const PROJECT_PATHS = [
'misuzu' => MSZ_ROOT,
'index' => MSZ_ROOT . '/vendor/flashwave/index',
];
private const PROJECT_SUFFIXES = [
'misuzu' => 'Misuzu Project » %s',
'index' => 'Index Project » %s',
];
public function __construct(IRouter $router) {
$router->get('/info', [$this, 'getIndex']);
$router->get('/info/:name', [$this, 'getDocsPage']);
$router->get('/info/:project/:name', [$this, 'getProjectPage']);
$router->get('/info.php', function($response) {
$response->redirect(url('info'), true);
});
$router->get('/info.php/:name', function($response, $request, string $name) {
$response->redirect(url('info', ['title' => $name]), true);
});
$router->get('/info.php/:project/:name', function($response, $request, string $project, string $name) {
$response->redirect(url('info', ['title' => $project . '/' . $name]), true);
});
}
private static function checkName(string $name): bool {
return preg_match('#^([A-Za-z0-9_]+)$#', $name) === 1;
}
public function getIndex() {
return Template::renderRaw('info.index');
}
public function getDocsPage($response, $request, string $name) {
if(!self::checkName($name))
return 404;
return $this->serveMarkdownDocument(
sprintf('%s/%s.md', self::DOCS_PATH, $name)
);
}
public function getProjectPage($response, $request, string $project, string $name) {
if(!array_key_exists($project, self::PROJECT_PATHS))
return 404;
if(!self::checkName($name))
return 404;
$projectPath = self::PROJECT_PATHS[$project];
$titleSuffix = array_key_exists($project, self::PROJECT_SUFFIXES) ? self::PROJECT_SUFFIXES[$project] : '';
$attempts = 0;
$licenceHack = false;
for(;;) {
$path = match(++$attempts) {
1 => sprintf('%s/%s', $projectPath, $name),
2 => sprintf('%s/%s.md', $projectPath, $name),
3 => sprintf('%s/%s', $projectPath, strtoupper($name)),
4 => sprintf('%s/%s.md', $projectPath, strtoupper($name)),
5 => sprintf('%s/%s', $projectPath, strtolower($name)),
6 => sprintf('%s/%s.md', $projectPath, strtolower($name)),
default => '',
};
if($path === '') {
if(!$licenceHack) {
$isBritish = strtolower($name) === 'licence';
$isAmerican = strtolower($name) === 'license';
if($isBritish || $isAmerican) {
$attempts = 0;
$licenceHack = true;
$name = $isAmerican ? 'licence' : 'license';
continue;
}
}
return 404;
}
if(is_file($path))
break;
}
return $this->serveMarkdownDocument($path, $titleSuffix);
}
private function serveMarkdownDocument(string $path, string $titleFormat = '') {
if(!is_file($path))
return 404;
$body = file_get_contents($path);
if(str_starts_with($body, '#')) {
$offset = strpos($body, "\n");
$title = trim(substr($body, 1, $offset));
$body = substr($body, $offset);
} else
$title = ucfirst(basename($path));
if($titleFormat !== '')
$title = sprintf($titleFormat, $title);
$body = Parser::instance(Parser::MARKDOWN)->parseText($body);
return Template::renderRaw('info.view', [
'document' => [
'title' => $title,
'content' => $body,
],
]);
}
}

View file

@ -1,6 +1,13 @@
<?php
namespace Misuzu;
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo;
use Index\Data\Migration\DbMigrationManager;
use Index\Data\Migration\FsDbMigrationRepo;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
use Index\Routing\Router;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo;
use Misuzu\Auth\AuthTokenPacker;
@ -10,11 +17,15 @@ use Misuzu\Auth\Sessions;
use Misuzu\Auth\TwoFactorAuthSessions;
use Misuzu\AuditLog\AuditLog;
use Misuzu\Changelog\Changelog;
use Misuzu\Changelog\ChangelogRoutes;
use Misuzu\Comments\Comments;
use Misuzu\Config\IConfig;
use Misuzu\Counters\Counters;
use Misuzu\Emoticons\Emotes;
use Misuzu\Home\HomeRoutes;
use Misuzu\Info\InfoRoutes;
use Misuzu\News\News;
use Misuzu\News\NewsRoutes;
use Misuzu\Profile\ProfileFields;
use Misuzu\Satori\SatoriRoutes;
use Misuzu\SharpChat\SharpChatRoutes;
@ -25,13 +36,7 @@ use Misuzu\Users\Roles;
use Misuzu\Users\Users;
use Misuzu\Users\UserInfo;
use Misuzu\Users\Warnings;
use Index\Data\IDbConnection;
use Index\Data\Migration\IDbMigrationRepo;
use Index\Data\Migration\DbMigrationManager;
use Index\Data\Migration\FsDbMigrationRepo;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
use Index\Routing\Router;
use Misuzu\Users\Assets\AssetsRoutes;
// this class should function as the root for everything going forward
// no more magical static classes that are just kind of assumed to exist
@ -340,17 +345,13 @@ class MisuzuContext {
return $menu;
}
public function setUpHttp(bool $legacy = false): void {
public function setUpHttp(): void {
$this->router = new HttpFx;
$this->router->use('/', function($response) {
$response->setPoweredBy('Misuzu');
});
$this->registerErrorPages();
if($legacy)
$this->registerLegacyRedirects();
else
$this->registerHttpRoutes();
}
@ -377,66 +378,39 @@ class MisuzuContext {
}
private function registerHttpRoutes(): void {
$mszCompatHandler = fn($className, $method) => fn(...$args) => (new ("\\Misuzu\\Http\\Handlers\\{$className}Handler")($this))->{$method}(...$args);
new HomeRoutes(
$this->router, $this->config, $this->dbConn, $this->authInfo,
$this->changelog, $this->comments, $this->counters, $this->news,
$this->users
);
$this->router->get('/', $mszCompatHandler('Home', 'index'));
new AssetsRoutes($this->router, $this->authInfo, $this->bans, $this->users);
$this->router->get('/assets/avatar/:filename', $mszCompatHandler('Assets', 'serveAvatar'));
$this->router->get('/assets/profile-background/:filename', $mszCompatHandler('Assets', 'serveProfileBackground'));
new InfoRoutes($this->router);
$this->router->get('/info', $mszCompatHandler('Info', 'index'));
$this->router->get('/info/:name', $mszCompatHandler('Info', 'page'));
$this->router->get('/info/:project/:name', $mszCompatHandler('Info', 'page'));
new NewsRoutes(
$this->router, $this->config, $this->authInfo,
$this->news, $this->users, $this->comments
);
$this->router->get('/changelog', $mszCompatHandler('Changelog', 'index'));
$this->router->get('/changelog.rss', $mszCompatHandler('Changelog', 'feedRss'));
$this->router->get('/changelog.atom', $mszCompatHandler('Changelog', 'feedAtom'));
$this->router->get('/changelog/change/:id', $mszCompatHandler('Changelog', 'change'));
new ChangelogRoutes(
$this->router, $this->config, $this->changelog,
$this->users, $this->authInfo, $this->comments
);
$this->router->get('/news', $mszCompatHandler('News', 'index'));
$this->router->get('/news.rss', $mszCompatHandler('News', 'feedIndexRss'));
$this->router->get('/news.atom', $mszCompatHandler('News', 'feedIndexAtom'));
$this->router->get('/news/:category', $mszCompatHandler('News', 'viewCategory'));
$this->router->get('/news/post/:id', $mszCompatHandler('News', 'viewPost'));
new SharpChatRoutes(
$this->router, $this->config->scopeTo('sockChat'),
$this->bans, $this->emotes, $this->users,
$this->sessions, $this->authInfo,
$this->createAuthTokenPacker(...)
);
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
$this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST'));
new SatoriRoutes(
$this->dbConn, $this->config->scopeTo('satori'),
$this->router, $this->users, $this->profileFields
);
new SharpChatRoutes($this->router, $this->config->scopeTo('sockChat'), $this->bans, $this->emotes, $this->users, $this->sessions, $this->authInfo, $this->createAuthTokenPacker(...));
new SatoriRoutes($this->dbConn, $this->config->scopeTo('satori'), $this->router, $this->users, $this->profileFields);
}
private function registerLegacyRedirects(): void {
$this->router->get('/index.php', function($response) {
$response->redirect(url('index'), true);
});
$this->router->get('/info.php', function($response) {
$response->redirect(url('info'), true);
});
$this->router->get('/settings.php', function($response) {
$response->redirect(url('settings-index'), true);
});
$this->router->get('/changelog.php', function($response, $request) {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) {
$response->redirect(url('changelog-change', ['change' => $changeId]), true);
return;
}
$response->redirect(url('changelog-index', [
'date' => $request->getParam('d'),
'user' => $request->getParam('u', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$infoRedirect = function($response, $request, string ...$parts) {
$response->redirect(url('info', ['title' => implode('/', $parts)]), true);
};
$this->router->get('/info.php/:name', $infoRedirect);
$this->router->get('/info.php/:project/:name', $infoRedirect);
// below is still only otherwise available as stinky php files
$this->router->get('/auth.php', function($response, $request) {
$response->redirect(url([
@ -447,73 +421,8 @@ class MisuzuContext {
][$request->getParam('m')] ?? 'auth-login'), true);
});
$this->router->get('/news.php', function($response, $request) {
$postId = $request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT);
if($postId > 0)
$location = url('news-post', ['post' => $postId]);
else {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$pageId = $request->getParam('page', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category' : 'news-index', ['category' => $catId, 'page' => $pageId]);
}
$response->redirect($location, true);
});
$this->router->get('/news.php/rss', function($response, $request) {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]);
$response->redirect($location, true);
});
$this->router->get('/news.php/atom', function($response, $request) {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]);
$response->redirect($location, true);
});
$this->router->get('/news/index.php', function($response, $request) {
$response->redirect(url('news-index', [
'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$this->router->get('/news/category.php', function($response, $request) {
$response->redirect(url('news-category', [
'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$this->router->get('/news/post.php', function($response, $request) {
$response->redirect(url('news-post', [
'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$this->router->get('/news/feed.php', function() {
return 400;
});
$this->router->get('/news/feed.php/rss', function($response, $request) {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect(url(
$catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss',
['category' => $catId]
), true);
});
$this->router->get('/news/feed.php/atom', function($response, $request) {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect(url(
$catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom',
['category' => $catId]
), true);
});
$this->router->get('/user-assets.php', function($response, $request) {
return (new \Misuzu\Http\Handlers\AssetsHandler($this))->serveLegacy($response, $request);
$this->router->get('/settings.php', function($response) {
$response->redirect(url('settings-index'), true);
});
}
}

361
src/News/NewsRoutes.php Normal file
View file

@ -0,0 +1,361 @@
<?php
namespace Misuzu\News;
use RuntimeException;
use Index\DateTime;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Routing\IRouter;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Auth\AuthInfo;
use Misuzu\Comments\Comments;
use Misuzu\Comments\CommentsCategory;
use Misuzu\Comments\CommentsEx;
use Misuzu\Config\IConfig;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\News;
use Misuzu\News\NewsCategoryInfo;
use Misuzu\Users\Users;
use Misuzu\Parsers\Parser;
class NewsRoutes {
private IConfig $config;
private AuthInfo $authInfo;
private News $news;
private Users $users;
private Comments $comments;
public function __construct(
IRouter $router,
IConfig $config,
AuthInfo $authInfo,
News $news,
Users $users,
Comments $comments
) {
$this->config = $config;
$this->authInfo = $authInfo;
$this->news = $news;
$this->users = $users;
$this->comments = $comments;
$router->get('/news', [$this, 'getIndex']);
$router->get('/news.rss', [$this, 'getFeedRss']);
$router->get('/news.atom', [$this, 'getFeedAtom']);
$router->get('/news/:category', [$this, 'getCategory']);
$router->get('/news/post/:id', [$this, 'getPost']);
$router->get('/news.php', function($response, $request) {
$postId = $request->getParam('n', FILTER_SANITIZE_NUMBER_INT)
?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT);
if($postId > 0)
$location = url('news-post', ['post' => $postId]);
else {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$pageId = $request->getParam('page', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category' : 'news-index', ['category' => $catId, 'page' => $pageId]);
}
$response->redirect($location, true);
});
$router->get('/news.php/rss', function($response, $request) {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss', ['category' => $catId]);
$response->redirect($location, true);
});
$router->get('/news.php/atom', function($response, $request) {
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom', ['category' => $catId]);
$response->redirect($location, true);
});
$router->get('/news/index.php', function($response, $request) {
$response->redirect(url('news-index', [
'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$router->get('/news/category.php', function($response, $request) {
$response->redirect(url('news-category', [
'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$router->get('/news/post.php', function($response, $request) {
$response->redirect(url('news-post', [
'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]), true);
});
$router->get('/news/feed.php/rss', function($response, $request) {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect(url(
$catId > 0 ? 'news-category-feed-rss' : 'news-feed-rss',
['category' => $catId]
), true);
});
$router->get('/news/feed.php/atom', function($response, $request) {
$catId = (int)$request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$response->redirect(url(
$catId > 0 ? 'news-category-feed-atom' : 'news-feed-atom',
['category' => $catId]
), true);
});
}
private array $userInfos = [];
private array $userColours = [];
private array $categoryInfos = [];
private function getNewsPostsForView(Pagination $pagination, ?NewsCategoryInfo $categoryInfo = null): array {
$posts = [];
$postInfos = $categoryInfo === null
? $this->news->getAllPosts(onlyFeatured: true, pagination: $pagination)
: $this->news->getPostsByCategory($categoryInfo, pagination: $pagination);
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
} else {
try {
$userInfo = $this->users->getUser($userId, 'id');
} catch(RuntimeException $ex) {
$userInfo = null;
}
$this->userInfos[$userId] = $userInfo;
}
if(array_key_exists($userId, $this->userColours)) {
$userColour = $this->userColours[$userId];
} else {
try {
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {
$userColour = null;
}
$this->userColours[$userId] = $userColour;
}
if(array_key_exists($categoryId, $this->categoryInfos))
$categoryInfo = $this->categoryInfos[$categoryId];
else
$this->categoryInfos[$categoryId] = $categoryInfo = $this->news->getCategoryByPost($postInfo);
$commentsCount = $postInfo->hasCommentsCategoryId()
? $this->comments->countPosts($postInfo->getCommentsCategoryId(), includeReplies: true)
: 0;
$posts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
'user_colour' => $userColour,
'comments_count' => $commentsCount,
];
}
return $posts;
}
private function getNewsPostsForFeed(?NewsCategoryInfo $categoryInfo = null): array {
$posts = [];
$postInfos = $categoryInfo === null
? $this->news->getAllPosts(onlyFeatured: true, pagination: new Pagination(10))
: $this->news->getPostsByCategory($categoryInfo, pagination: new Pagination(10));
foreach($postInfos as $postInfo) {
$userId = $postInfo->getUserId();
$categoryId = $postInfo->getCategoryId();
if(array_key_exists($userId, $this->userInfos)) {
$userInfo = $this->userInfos[$userId];
} else {
try {
$userInfo = $this->users->getUser($userId, 'id');
} catch(RuntimeException $ex) {
$userInfo = null;
}
$this->userInfos[$userId] = $userInfo;
}
$posts[] = [
'post' => $postInfo,
'category' => $categoryInfo,
'user' => $userInfo,
];
}
return $posts;
}
public function getIndex() {
$categories = $this->news->getAllCategories();
$pagination = new Pagination($this->news->countAllPosts(onlyFeatured: true), 5);
if(!$pagination->hasValidOffset())
return 404;
$posts = $this->getNewsPostsForView($pagination);
return Template::renderRaw('news.index', [
'news_categories' => $categories,
'news_posts' => $posts,
'news_pagination' => $pagination,
]);
}
public function getFeedRss($response) {
return $this->getFeed($response, 'rss');
}
public function getFeedAtom($response) {
return $this->getFeed($response, 'atom');
}
public function getCategory($response, $request, string $fileName) {
$categoryId = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
try {
$categoryInfo = $this->news->getCategoryById($categoryId);
} catch(RuntimeException $ex) {
return 404;
}
if($type === 'rss')
return $this->getCategoryFeedRss($response, $request, $categoryInfo);
elseif($type === 'atom')
return $this->getCategoryFeedAtom($response, $request, $categoryInfo);
elseif($type !== '')
return 404;
$pagination = new Pagination($this->news->countPostsByCategory($categoryInfo), 5);
if(!$pagination->hasValidOffset())
return 404;
$posts = $this->getNewsPostsForView($pagination, $categoryInfo);
return Template::renderRaw('news.category', [
'news_category' => $categoryInfo,
'news_posts' => $posts,
'news_pagination' => $pagination,
]);
}
private function getCategoryFeedRss($response, $request, NewsCategoryInfo $categoryInfo) {
return $this->getFeed($response, 'rss', $categoryInfo);
}
private function getCategoryFeedAtom($response, $request, NewsCategoryInfo $categoryInfo) {
return $this->getFeed($response, 'atom', $categoryInfo);
}
public function getPost($response, $request, string $postId) {
try {
$postInfo = $this->news->getPostById($postId);
} catch(RuntimeException $ex) {
return 404;
}
if(!$postInfo->isPublished() || $postInfo->isDeleted())
return 404;
$categoryInfo = $this->news->getCategoryByPost($postInfo);
if($postInfo->hasCommentsCategoryId())
try {
$commentsCategory = $this->comments->getCategoryById($postInfo->getCommentsCategoryId());
} catch(RuntimeException $ex) {}
if(!isset($commentsCategory)) {
$commentsCategory = $this->comments->ensureCategory($postInfo->getCommentsCategoryName());
$this->news->updatePostCommentCategory($postInfo, $commentsCategory);
}
$userInfo = null;
$userColour = null;
if($postInfo->hasUserId())
try {
$userInfo = $this->users->getUser($postInfo->getUserId(), 'id');
$userColour = $this->users->getUserColour($userInfo);
} catch(RuntimeException $ex) {}
$comments = new CommentsEx($this->authInfo, $this->comments, $this->users);
return Template::renderRaw('news.post', [
'post_info' => $postInfo,
'post_category_info' => $categoryInfo,
'post_user_info' => $userInfo,
'post_user_colour' => $userColour,
'comments_info' => $comments->getCommentsForLayout($commentsCategory),
]);
}
private function getFeed($response, string $feedType, ?NewsCategoryInfo $categoryInfo = null) {
$hasCategory = $categoryInfo !== null;
$siteName = $this->config->getString('site.name', 'Misuzu');
$posts = $this->getNewsPostsForFeed($categoryInfo);
$serialiser = match($feedType) {
'rss' => new RssFeedSerializer,
'atom' => new AtomFeedSerializer,
default => throw new RuntimeException('Invalid $feedType specified.'),
};
$response->setContentType(sprintf('application/%s+xml; charset=utf-8', $feedType));
$feed = (new Feed)
->setTitle($siteName . ' » ' . ($hasCategory ? $categoryInfo->getName() : 'Featured News'))
->setDescription($hasCategory ? $categoryInfo->getDescription() : 'A live featured news feed.')
->setContentUrl(url_prefix(false) . ($hasCategory ? url('news-category', ['category' => $categoryInfo->getId()]) : url('news-index')))
->setFeedUrl(url_prefix(false) . ($hasCategory ? url("news-category-feed-{$feedType}", ['category' => $categoryInfo->getId()]) : url("news-feed-{$feedType}")));
foreach($posts as $post) {
$postInfo = $post['post'];
$userInfo = $post['user'];
$userId = 0;
$userName = 'Author';
if($userInfo !== null) {
$userId = $userInfo->getId();
$userName = $userInfo->getName();
}
$postUrl = url_prefix(false) . url('news-post', ['post' => $postInfo->getId()]);
$commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $postInfo->getId()]);
$authorUrl = url_prefix(false) . url('user-profile', ['user' => $userId]);
$feedItem = (new FeedItem)
->setTitle($postInfo->getTitle())
->setSummary($postInfo->getFirstParagraph())
->setContent(Parser::instance(Parser::MARKDOWN)->parseText($postInfo->getBody()))
->setCreationDate($postInfo->getCreatedTime())
->setUniqueId($postUrl)
->setContentUrl($postUrl)
->setCommentsUrl($commentsUrl)
->setAuthorName($userName)
->setAuthorUrl($authorUrl);
if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate())
$feed->setLastUpdate($feedItem->getCreationDate());
$feed->addItem($feedItem);
}
return $serialiser->serializeFeed($feed);
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Misuzu\Users\Assets;
use RuntimeException;
use Index\Routing\IRouter;
use Misuzu\Auth\AuthInfo;
use Misuzu\Users\Bans;
use Misuzu\Users\Users;
use Misuzu\Users\UserInfo;
class AssetsRoutes {
private AuthInfo $authInfo;
private Bans $bans;
private Users $users;
public function __construct(IRouter $router, AuthInfo $authInfo, Bans $bans, Users $users) {
$this->authInfo = $authInfo;
$this->bans = $bans;
$this->users = $users;
$router->get('/assets/avatar/:filename', [$this, 'getAvatar']);
$router->get('/assets/profile-background/:filename', [$this, 'getProfileBackground']);
$router->get('/user-assets.php', [$this, 'getUserAssets']);
}
private function canViewAsset($request, UserInfo $assetUser): bool {
if($this->bans->countActiveBans($assetUser))
return $this->authInfo->isLoggedIn() // allow staff viewing profile to still see banned user assets
&& perms_check_user(MSZ_PERMS_USER, (int)$this->authInfo->getUserId(), MSZ_PERM_USER_MANAGE_USERS)
&& parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === url('user-profile');
return true;
}
public function getAvatar($response, $request, string $fileName) {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try {
$userInfo = $this->users->getUser($userId, 'id');
if(!$this->canViewAsset($request, $userInfo)) {
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/banned-avatar.png', MSZ_PUBLIC);
} else {
$userAssetInfo = new UserAvatarAsset($userInfo);
if($userAssetInfo->isPresent())
$assetInfo = $userAssetInfo;
}
} catch(RuntimeException $ex) {}
return $this->serveAsset($response, $request, $assetInfo);
}
public function getProfileBackground($response, $request, string $fileName) {
$userId = pathinfo($fileName, PATHINFO_FILENAME);
try {
$userInfo = $this->users->getUser($userId, 'id');
} catch(RuntimeException $ex) {}
if(!empty($userInfo)) {
$userAssetInfo = new UserBackgroundAsset($userInfo);
if($userAssetInfo->isPresent() && $this->canViewAsset($request, $userInfo))
$assetInfo = $userAssetInfo;
}
if(!isset($assetInfo)) {
// circumvent the default error page
$response->setContent('Not Found');
return 404;
}
return $this->serveAsset($response, $request, $assetInfo);
}
public function getUserAssets($response, $request) {
$userId = (string)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
$mode = (string)$request->getParam('m');
if($mode === 'avatar')
return $this->getAvatar($response, $request, $userId);
if($mode === 'background')
return $this->getProfileBackground($response, $request, $userId);
// circumvent the default error page
$response->setContent('Not Found');
return 404;
}
private function serveAsset($response, $request, UserImageAssetInterface $assetInfo): void {
$contentType = $assetInfo->getMimeType();
$publicPath = $assetInfo->getPublicPath();
$fileName = $assetInfo->getFileName();
if($assetInfo instanceof UserAssetScalableInterface) {
$dimensions = (int)($request->getParam('res', FILTER_SANITIZE_NUMBER_INT)
?? $request->getParam('r', FILTER_SANITIZE_NUMBER_INT));
if($dimensions > 0) {
$assetInfo->ensureScaledExists($dimensions);
$contentType = $assetInfo->getScaledMimeType($dimensions);
$publicPath = $assetInfo->getPublicScaledPath($dimensions);
$fileName = $assetInfo->getScaledFileName($dimensions);
}
}
$response->accelRedirect($publicPath);
$response->setContentType($contentType);
$response->setFileName($fileName, false);
}
}

View file

@ -122,6 +122,23 @@ class Bans {
return new BanInfo($result);
}
public function countActiveBans(
UserInfo|string $userInfo,
int $minimumSeverity = self::SEVERITY_MIN
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
// orders by ban_expires descending with NULLs (permanent) first
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users_bans WHERE user_id = ? AND ban_severity >= ? AND (ban_expires IS NULL OR ban_expires > NOW()) ORDER BY ban_expires IS NULL DESC, ban_expires DESC');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $minimumSeverity);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
public function tryGetActiveBan(
UserInfo|string $userInfo,
int $minimumSeverity = self::SEVERITY_MIN

View file

@ -41,8 +41,8 @@ define('MSZ_URLS', [
'forum-index' => ['/forum'],
'forum-leaderboard' => ['/forum/leaderboard.php', ['id' => '<id>', 'mode' => '<mode>']],
'forum-mark-global' => ['/forum/mark-as-read'],
'forum-mark-single' => ['/forum/mark-as-read', ['forum' => '<forum>']],
'forum-mark-global' => ['/forum/index.php', ['m' => 'mark']],
'forum-mark-single' => ['/forum/index.php', ['m' => 'mark', 'f' => '<forum>']],
'forum-topic-new' => ['/forum/posting.php', ['f' => '<forum>']],
'forum-reply-new' => ['/forum/posting.php', ['t' => '<topic>']],
'forum-category' => ['/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']],

View file

@ -3,7 +3,7 @@
<div class="footer__wrapper">
{% autoescape false %}
<div class="footer__copyright">
<a href="https://flash.moe" target="_blank" rel="noreferrer noopener" class="footer__link">Flashwave</a>
<a href="https://flash.moe" target="_blank" rel="noreferrer noopener" class="footer__link">flashwave</a>
2013-{{ ''|date('Y') }} /
{% set git_branch = git_branch() %}
{% if git_branch != 'HEAD' %}

View file

@ -75,7 +75,7 @@
</div>
<div class="landingv2-footer-copyright">
<div class="landingv2-footer-copyright-line">
<a href="https://flash.moe" target="_blank" rel="noreferrer noopener">Flashwave</a> 2013-{{ ''|date('Y') }}
<a href="https://flash.moe" target="_blank" rel="noreferrer noopener">flashwave</a> 2013-{{ ''|date('Y') }}
</div>
<div class="landingv2-footer-copyright-line">
{% set git_branch = git_branch() %}
@ -193,8 +193,8 @@
{% endif %}
</div>
{% if linked_data is defined and linked_data is iterable %}
<script type="application/ld+json">{ "@context": "http://schema.org", "@type": "Organization", "name": "{{ linked_data.name }}", "url": "{{ linked_data.url }}", "logo": "{{ linked_data.logo }}", "sameAs": ["{{ linked_data.same_as|join('", "')|raw }}"] }</script>
{% if linked_data is not null %}
<script type="application/ld+json">{{ linked_data|json_encode|raw }}</script>
{% endif %}
{% endblock %}

View file

@ -48,8 +48,7 @@
<div class="profile__header__country">
{% if hasCountryCode %}<div class="flag flag--{{ profile_user.countryCode|lower }}"></div>{% endif %}
<div class="profile__header__country__name">
{% if hasCountryCode %}{{ profile_user.countryCode|country_name }}{% endif %}
{% if hasAge %}{% if hasCountryCode %}, {% endif %}{{ age }} year{{ age != 's' ? 's' : '' }} old{% endif %}
{% if hasCountryCode %}{{ profile_user.countryCode|country_name }}{% endif %}{% if hasAge %}{% if hasCountryCode %}, {% endif %}{{ age }} year{{ age != 's' ? 's' : '' }} old{% endif %}
</div>
</div>
{% endif %}