Converted all Misuzu style route handlers to Index style ones.
This commit is contained in:
parent
6bfa3d7238
commit
cf71129153
20 changed files with 1047 additions and 905 deletions
|
@ -9,7 +9,26 @@ $currentUserId = $currentUser === null ? '0' : $currentUser->getId();
|
||||||
|
|
||||||
switch($indexMode) {
|
switch($indexMode) {
|
||||||
case 'mark':
|
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -208,5 +208,5 @@ if(!empty($mszLegacyPath) && str_starts_with($mszLegacyPath, $mszLegacyPathPrefi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$msz->setUpHttp(str_contains($mszRequestPath, '.php'));
|
$msz->setUpHttp();
|
||||||
$msz->dispatchHttp($request);
|
$msz->dispatchHttp($request);
|
||||||
|
|
|
@ -1,26 +1,73 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu\Http\Handlers;
|
namespace Misuzu\Changelog;
|
||||||
|
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use Index\Routing\IRouter;
|
||||||
use Misuzu\Pagination;
|
use Misuzu\Pagination;
|
||||||
use Misuzu\Template;
|
use Misuzu\Template;
|
||||||
|
use Misuzu\Auth\AuthInfo;
|
||||||
|
use Misuzu\Comments\Comments;
|
||||||
use Misuzu\Comments\CommentsEx;
|
use Misuzu\Comments\CommentsEx;
|
||||||
|
use Misuzu\Config\IConfig;
|
||||||
use Misuzu\Feeds\Feed;
|
use Misuzu\Feeds\Feed;
|
||||||
use Misuzu\Feeds\FeedItem;
|
use Misuzu\Feeds\FeedItem;
|
||||||
use Misuzu\Feeds\AtomFeedSerializer;
|
use Misuzu\Feeds\AtomFeedSerializer;
|
||||||
use Misuzu\Feeds\RssFeedSerializer;
|
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 $userInfos = [];
|
||||||
private array $userColours = [];
|
private array $userColours = [];
|
||||||
|
|
||||||
public function index($response, $request) {
|
public function __construct(
|
||||||
$filterDate = (string)$request->getParam('date');
|
IRouter $router,
|
||||||
$filterUser = (int)$request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
|
IConfig $config,
|
||||||
$filterTags = (string)$request->getParam('tags');
|
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))
|
if(empty($filterDate))
|
||||||
$filterDate = null;
|
$filterDate = null;
|
||||||
|
@ -32,14 +79,14 @@ class ChangelogHandler extends Handler {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($filterUser > 0)
|
if(empty($filterUser))
|
||||||
|
$filterUser = null;
|
||||||
|
else
|
||||||
try {
|
try {
|
||||||
$filterUser = $users->getUser((string)$filterUser, 'id');
|
$filterUser = $this->users->getUser($filterUser, 'id');
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
$filterUser = null;
|
|
||||||
|
|
||||||
if(empty($filterTags))
|
if(empty($filterTags))
|
||||||
$filterTags = null;
|
$filterTags = null;
|
||||||
|
@ -49,13 +96,12 @@ class ChangelogHandler extends Handler {
|
||||||
$tag = trim($tag);
|
$tag = trim($tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
$changelog = $this->context->getChangelog();
|
$count = $this->changelog->countAllChanges($filterUser, $filterDate, $filterTags);
|
||||||
$count = $changelog->countAllChanges($filterUser, $filterDate, $filterTags);
|
|
||||||
$pagination = new Pagination($count, 30);
|
$pagination = new Pagination($count, 30);
|
||||||
if(!$pagination->hasValidOffset())
|
if(!$pagination->hasValidOffset())
|
||||||
return 404;
|
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))
|
if(empty($changeInfos))
|
||||||
return 404;
|
return 404;
|
||||||
|
|
||||||
|
@ -69,8 +115,8 @@ class ChangelogHandler extends Handler {
|
||||||
$userColour = $this->userColours[$userId];
|
$userColour = $this->userColours[$userId];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$userInfo = $users->getUser($userId, 'id');
|
$userInfo = $this->users->getUser($userId, 'id');
|
||||||
$userColour = $users->getUserColour($userInfo);
|
$userColour = $this->users->getUserColour($userInfo);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
$userInfo = null;
|
$userInfo = null;
|
||||||
$userColour = 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_infos' => $changes,
|
||||||
'changelog_date' => $filterDate,
|
'changelog_date' => $filterDate,
|
||||||
'changelog_user' => $filterUser,
|
'changelog_user' => $filterUser,
|
||||||
'changelog_tags' => $filterTags,
|
'changelog_tags' => $filterTags,
|
||||||
'changelog_pagination' => $pagination,
|
'changelog_pagination' => $pagination,
|
||||||
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
|
'comments_info' => empty($filterDate) ? null : $this->getCommentsInfo($changeInfos[0]->getCommentsCategoryName()),
|
||||||
]));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getCommentsInfo(string $categoryName): object {
|
public function getChange($response, $request, string $changeId) {
|
||||||
$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) {
|
|
||||||
try {
|
try {
|
||||||
$changeInfo = $this->context->getChangelog()->getChangeById($changeId, withTags: true);
|
$changeInfo = $this->changelog->getChangeById($changeId, withTags: true);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $this->context->getUsers();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$userInfo = $users->getUser($changeInfo->getUserId(), 'id');
|
$userInfo = $this->users->getUser($changeInfo->getUserId(), 'id');
|
||||||
$userColour = $users->getUserColour($userInfo);
|
$userColour = $this->users->getUserColour($userInfo);
|
||||||
} catch(RuntimeException $ex) {
|
} catch(RuntimeException $ex) {
|
||||||
$userInfo = null;
|
$userInfo = null;
|
||||||
$userColour = null;
|
$userColour = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->setContent(Template::renderRaw('changelog.change', [
|
return Template::renderRaw('changelog.change', [
|
||||||
'change_info' => $changeInfo,
|
'change_info' => $changeInfo,
|
||||||
'change_user_info' => $userInfo,
|
'change_user_info' => $userInfo,
|
||||||
'change_user_colour' => $userColour,
|
'change_user_colour' => $userColour,
|
||||||
'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
|
'comments_info' => $this->getCommentsInfo($changeInfo->getCommentsCategoryName()),
|
||||||
]));
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createFeed(string $feedMode): Feed {
|
private function createFeed(string $feedMode): Feed {
|
||||||
$siteName = $this->context->getConfig()->getString('site.name', 'Misuzu');
|
$siteName = $this->config->getString('site.name', 'Misuzu');
|
||||||
$changes = $this->context->getChangelog()->getAllChanges(pagination: new Pagination(10));
|
$changes = $this->changelog->getAllChanges(pagination: new Pagination(10));
|
||||||
|
|
||||||
$feed = (new Feed)
|
$feed = (new Feed)
|
||||||
->setTitle($siteName . ' » Changelog')
|
->setTitle($siteName . ' » Changelog')
|
||||||
|
@ -154,13 +193,13 @@ class ChangelogHandler extends Handler {
|
||||||
return $feed;
|
return $feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function feedAtom($response, $request) {
|
public function getFeedRSS($response) {
|
||||||
$response->setContentType('application/atom+xml; charset=utf-8');
|
$response->setContentType('application/rss+xml; charset=utf-8');
|
||||||
return (new AtomFeedSerializer)->serializeFeed(self::createFeed('atom'));
|
return (new RssFeedSerializer)->serializeFeed($this->createFeed('rss'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function feedRss($response, $request) {
|
public function getFeedAtom($response) {
|
||||||
$response->setContentType('application/rss+xml; charset=utf-8');
|
$response->setContentType('application/atom+xml; charset=utf-8');
|
||||||
return (new RssFeedSerializer)->serializeFeed(self::createFeed('rss'));
|
return (new AtomFeedSerializer)->serializeFeed($this->createFeed('atom'));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,11 +4,12 @@ namespace Misuzu\Comments;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Misuzu\MisuzuContext;
|
use Misuzu\MisuzuContext;
|
||||||
|
use Misuzu\Auth\AuthInfo;
|
||||||
use Misuzu\Users\Users;
|
use Misuzu\Users\Users;
|
||||||
|
|
||||||
class CommentsEx {
|
class CommentsEx {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private MisuzuContext $context,
|
private AuthInfo $authInfo,
|
||||||
private Comments $comments,
|
private Comments $comments,
|
||||||
private Users $users,
|
private Users $users,
|
||||||
private array $userInfos = [],
|
private array $userInfos = [],
|
||||||
|
@ -20,8 +21,8 @@ class CommentsEx {
|
||||||
if(is_string($category))
|
if(is_string($category))
|
||||||
$category = $this->comments->ensureCategory($category);
|
$category = $this->comments->ensureCategory($category);
|
||||||
|
|
||||||
$hasUser = $this->context->isLoggedIn();
|
$hasUser = $this->authInfo->isLoggedIn();
|
||||||
$info->user = $hasUser ? $this->context->getActiveUser() : null;
|
$info->user = $hasUser ? $this->authInfo->getUserInfo() : null;
|
||||||
$info->colour = $hasUser ? $this->users->getUserColour($info->user) : null;
|
$info->colour = $hasUser ? $this->users->getUserColour($info->user) : null;
|
||||||
$info->perms = $hasUser ? perms_for_comments($info->user->getId()) : [];
|
$info->perms = $hasUser ? perms_for_comments($info->user->getId()) : [];
|
||||||
$info->category = $category;
|
$info->category = $category;
|
||||||
|
|
283
src/Home/HomeRoutes.php
Normal file
283
src/Home/HomeRoutes.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
122
src/Info/InfoRoutes.php
Normal 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,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
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\Template;
|
||||||
use Misuzu\Auth\AuthInfo;
|
use Misuzu\Auth\AuthInfo;
|
||||||
use Misuzu\Auth\AuthTokenPacker;
|
use Misuzu\Auth\AuthTokenPacker;
|
||||||
|
@ -10,11 +17,15 @@ use Misuzu\Auth\Sessions;
|
||||||
use Misuzu\Auth\TwoFactorAuthSessions;
|
use Misuzu\Auth\TwoFactorAuthSessions;
|
||||||
use Misuzu\AuditLog\AuditLog;
|
use Misuzu\AuditLog\AuditLog;
|
||||||
use Misuzu\Changelog\Changelog;
|
use Misuzu\Changelog\Changelog;
|
||||||
|
use Misuzu\Changelog\ChangelogRoutes;
|
||||||
use Misuzu\Comments\Comments;
|
use Misuzu\Comments\Comments;
|
||||||
use Misuzu\Config\IConfig;
|
use Misuzu\Config\IConfig;
|
||||||
use Misuzu\Counters\Counters;
|
use Misuzu\Counters\Counters;
|
||||||
use Misuzu\Emoticons\Emotes;
|
use Misuzu\Emoticons\Emotes;
|
||||||
|
use Misuzu\Home\HomeRoutes;
|
||||||
|
use Misuzu\Info\InfoRoutes;
|
||||||
use Misuzu\News\News;
|
use Misuzu\News\News;
|
||||||
|
use Misuzu\News\NewsRoutes;
|
||||||
use Misuzu\Profile\ProfileFields;
|
use Misuzu\Profile\ProfileFields;
|
||||||
use Misuzu\Satori\SatoriRoutes;
|
use Misuzu\Satori\SatoriRoutes;
|
||||||
use Misuzu\SharpChat\SharpChatRoutes;
|
use Misuzu\SharpChat\SharpChatRoutes;
|
||||||
|
@ -25,13 +36,7 @@ use Misuzu\Users\Roles;
|
||||||
use Misuzu\Users\Users;
|
use Misuzu\Users\Users;
|
||||||
use Misuzu\Users\UserInfo;
|
use Misuzu\Users\UserInfo;
|
||||||
use Misuzu\Users\Warnings;
|
use Misuzu\Users\Warnings;
|
||||||
use Index\Data\IDbConnection;
|
use Misuzu\Users\Assets\AssetsRoutes;
|
||||||
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;
|
|
||||||
|
|
||||||
// this class should function as the root for everything going forward
|
// this class should function as the root for everything going forward
|
||||||
// no more magical static classes that are just kind of assumed to exist
|
// no more magical static classes that are just kind of assumed to exist
|
||||||
|
@ -340,18 +345,14 @@ class MisuzuContext {
|
||||||
return $menu;
|
return $menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUpHttp(bool $legacy = false): void {
|
public function setUpHttp(): void {
|
||||||
$this->router = new HttpFx;
|
$this->router = new HttpFx;
|
||||||
$this->router->use('/', function($response) {
|
$this->router->use('/', function($response) {
|
||||||
$response->setPoweredBy('Misuzu');
|
$response->setPoweredBy('Misuzu');
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->registerErrorPages();
|
$this->registerErrorPages();
|
||||||
|
$this->registerHttpRoutes();
|
||||||
if($legacy)
|
|
||||||
$this->registerLegacyRedirects();
|
|
||||||
else
|
|
||||||
$this->registerHttpRoutes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dispatchHttp(?HttpRequest $request = null): void {
|
public function dispatchHttp(?HttpRequest $request = null): void {
|
||||||
|
@ -377,66 +378,39 @@ class MisuzuContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
private function registerHttpRoutes(): void {
|
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'));
|
new InfoRoutes($this->router);
|
||||||
$this->router->get('/assets/profile-background/:filename', $mszCompatHandler('Assets', 'serveProfileBackground'));
|
|
||||||
|
|
||||||
$this->router->get('/info', $mszCompatHandler('Info', 'index'));
|
new NewsRoutes(
|
||||||
$this->router->get('/info/:name', $mszCompatHandler('Info', 'page'));
|
$this->router, $this->config, $this->authInfo,
|
||||||
$this->router->get('/info/:project/:name', $mszCompatHandler('Info', 'page'));
|
$this->news, $this->users, $this->comments
|
||||||
|
);
|
||||||
|
|
||||||
$this->router->get('/changelog', $mszCompatHandler('Changelog', 'index'));
|
new ChangelogRoutes(
|
||||||
$this->router->get('/changelog.rss', $mszCompatHandler('Changelog', 'feedRss'));
|
$this->router, $this->config, $this->changelog,
|
||||||
$this->router->get('/changelog.atom', $mszCompatHandler('Changelog', 'feedAtom'));
|
$this->users, $this->authInfo, $this->comments
|
||||||
$this->router->get('/changelog/change/:id', $mszCompatHandler('Changelog', 'change'));
|
);
|
||||||
|
|
||||||
$this->router->get('/news', $mszCompatHandler('News', 'index'));
|
new SharpChatRoutes(
|
||||||
$this->router->get('/news.rss', $mszCompatHandler('News', 'feedIndexRss'));
|
$this->router, $this->config->scopeTo('sockChat'),
|
||||||
$this->router->get('/news.atom', $mszCompatHandler('News', 'feedIndexAtom'));
|
$this->bans, $this->emotes, $this->users,
|
||||||
$this->router->get('/news/:category', $mszCompatHandler('News', 'viewCategory'));
|
$this->sessions, $this->authInfo,
|
||||||
$this->router->get('/news/post/:id', $mszCompatHandler('News', 'viewPost'));
|
$this->createAuthTokenPacker(...)
|
||||||
|
);
|
||||||
|
|
||||||
$this->router->get('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadGET'));
|
new SatoriRoutes(
|
||||||
$this->router->post('/forum/mark-as-read', $mszCompatHandler('Forum', 'markAsReadPOST'));
|
$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(...));
|
// below is still only otherwise available as stinky php files
|
||||||
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);
|
|
||||||
|
|
||||||
$this->router->get('/auth.php', function($response, $request) {
|
$this->router->get('/auth.php', function($response, $request) {
|
||||||
$response->redirect(url([
|
$response->redirect(url([
|
||||||
|
@ -447,73 +421,8 @@ class MisuzuContext {
|
||||||
][$request->getParam('m')] ?? 'auth-login'), true);
|
][$request->getParam('m')] ?? 'auth-login'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->router->get('/news.php', function($response, $request) {
|
$this->router->get('/settings.php', function($response) {
|
||||||
$postId = $request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT);
|
$response->redirect(url('settings-index'), true);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
361
src/News/NewsRoutes.php
Normal file
361
src/News/NewsRoutes.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
112
src/Users/Assets/AssetsRoutes.php
Normal file
112
src/Users/Assets/AssetsRoutes.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -122,6 +122,23 @@ class Bans {
|
||||||
return new BanInfo($result);
|
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(
|
public function tryGetActiveBan(
|
||||||
UserInfo|string $userInfo,
|
UserInfo|string $userInfo,
|
||||||
int $minimumSeverity = self::SEVERITY_MIN
|
int $minimumSeverity = self::SEVERITY_MIN
|
||||||
|
|
|
@ -41,8 +41,8 @@ define('MSZ_URLS', [
|
||||||
|
|
||||||
'forum-index' => ['/forum'],
|
'forum-index' => ['/forum'],
|
||||||
'forum-leaderboard' => ['/forum/leaderboard.php', ['id' => '<id>', 'mode' => '<mode>']],
|
'forum-leaderboard' => ['/forum/leaderboard.php', ['id' => '<id>', 'mode' => '<mode>']],
|
||||||
'forum-mark-global' => ['/forum/mark-as-read'],
|
'forum-mark-global' => ['/forum/index.php', ['m' => 'mark']],
|
||||||
'forum-mark-single' => ['/forum/mark-as-read', ['forum' => '<forum>']],
|
'forum-mark-single' => ['/forum/index.php', ['m' => 'mark', 'f' => '<forum>']],
|
||||||
'forum-topic-new' => ['/forum/posting.php', ['f' => '<forum>']],
|
'forum-topic-new' => ['/forum/posting.php', ['f' => '<forum>']],
|
||||||
'forum-reply-new' => ['/forum/posting.php', ['t' => '<topic>']],
|
'forum-reply-new' => ['/forum/posting.php', ['t' => '<topic>']],
|
||||||
'forum-category' => ['/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']],
|
'forum-category' => ['/forum/forum.php', ['f' => '<forum>', 'p' => '<page>']],
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="footer__wrapper">
|
<div class="footer__wrapper">
|
||||||
{% autoescape false %}
|
{% autoescape false %}
|
||||||
<div class="footer__copyright">
|
<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') }} /
|
2013-{{ ''|date('Y') }} /
|
||||||
{% set git_branch = git_branch() %}
|
{% set git_branch = git_branch() %}
|
||||||
{% if git_branch != 'HEAD' %}
|
{% if git_branch != 'HEAD' %}
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="landingv2-footer-copyright">
|
<div class="landingv2-footer-copyright">
|
||||||
<div class="landingv2-footer-copyright-line">
|
<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>
|
||||||
<div class="landingv2-footer-copyright-line">
|
<div class="landingv2-footer-copyright-line">
|
||||||
{% set git_branch = git_branch() %}
|
{% set git_branch = git_branch() %}
|
||||||
|
@ -193,8 +193,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if linked_data is defined and linked_data is iterable %}
|
{% if linked_data is not null %}
|
||||||
<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>
|
<script type="application/ld+json">{{ linked_data|json_encode|raw }}</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -48,8 +48,7 @@
|
||||||
<div class="profile__header__country">
|
<div class="profile__header__country">
|
||||||
{% if hasCountryCode %}<div class="flag flag--{{ profile_user.countryCode|lower }}"></div>{% endif %}
|
{% if hasCountryCode %}<div class="flag flag--{{ profile_user.countryCode|lower }}"></div>{% endif %}
|
||||||
<div class="profile__header__country__name">
|
<div class="profile__header__country__name">
|
||||||
{% if hasCountryCode %}{{ profile_user.countryCode|country_name }}{% endif %}
|
{% if hasCountryCode %}{{ profile_user.countryCode|country_name }}{% endif %}{% if hasAge %}{% if hasCountryCode %}, {% endif %}{{ age }} year{{ age != 's' ? 's' : '' }} old{% endif %}
|
||||||
{% if hasAge %}{% if hasCountryCode %}, {% endif %}{{ age }} year{{ age != 's' ? 's' : '' }} old{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in a new issue