From b66eb8ba76a0f00b0acf50c1e35fbed8891d9e9e Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 16 May 2020 22:35:11 +0000 Subject: [PATCH] Moved news pages into the router and made news object oriented. --- assets/js/misuzu/urls.js | 11 + misuzu.php | 7 +- public/index.php | 21 +- public/manage/general/blacklist.php | 2 +- public/manage/general/emoticon.php | 2 +- public/manage/general/emoticons.php | 2 +- public/manage/general/logs.php | 2 +- public/manage/general/settings.php | 2 +- public/manage/news/categories.php | 6 +- public/manage/news/category.php | 46 ++-- public/manage/news/post.php | 81 ++++--- public/manage/news/posts.php | 10 +- public/news.php | 11 +- public/news/category.php | 34 +-- public/news/feed.php | 76 +----- public/news/index.php | 31 +-- public/news/post.php | 33 +-- public/search.php | 4 +- src/Colour.php | 5 +- src/DB.php | 3 + src/Forum/forum.php | 47 ---- src/Forum/perms.php | 38 +++ src/General.php | 11 - src/Http/Handlers/HomeHandler.php | 6 +- src/Http/Handlers/NewsHandler.php | 202 ++++++++++++++++ src/Http/Routing/Route.php | 4 +- src/News/NewsCategory.php | 143 +++++++++++ src/News/NewsException.php | 6 + src/News/NewsPost.php | 287 ++++++++++++++++++++++ src/Pagination.php | 6 +- src/Users/User.php | 9 +- src/Users/user_legacy.php | 14 -- src/changelog.php | 4 - src/comments.php | 9 - src/manage.php | 22 +- src/news.php | 327 -------------------------- src/perms.php | 38 +++ src/url.php | 34 +-- templates/_layout/comments.twig | 2 +- templates/manage/news/categories.twig | 8 +- templates/manage/news/category.twig | 14 +- templates/manage/news/post.twig | 16 +- templates/manage/news/posts.twig | 20 +- templates/news/category.twig | 32 +-- templates/news/index.twig | 8 +- templates/news/macros.twig | 56 ++--- templates/news/post.twig | 8 +- 47 files changed, 980 insertions(+), 780 deletions(-) delete mode 100644 src/General.php create mode 100644 src/Http/Handlers/NewsHandler.php create mode 100644 src/News/NewsCategory.php create mode 100644 src/News/NewsException.php create mode 100644 src/News/NewsPost.php delete mode 100644 src/news.php diff --git a/assets/js/misuzu/urls.js b/assets/js/misuzu/urls.js index 65e80c9a..d911bb66 100644 --- a/assets/js/misuzu/urls.js +++ b/assets/js/misuzu/urls.js @@ -13,6 +13,17 @@ Misuzu.Urls.handleVariable = function(value, vars) { return ''; // not sure if there's a proper substitute for this, should probably resolve these in url_list if(value[0] === '{' && value.slice(-1) === '}') return Misuzu.CSRF.getToken(); + + // Allow file extensions + var split = value.split('.'), + extension = split[split.length - 1], + fileName = split.slice(0, -1).join('.'); + if(value !== fileName) { + var fallback = Misuzu.Urls.handleVariable(fileName, vars); + if(fallback !== fileName) + return fallback + '.' + extension; + } + return value; }; Misuzu.Urls.v = function(name, value) { diff --git a/misuzu.php b/misuzu.php index 166dc3bd..a55fdd27 100644 --- a/misuzu.php +++ b/misuzu.php @@ -61,16 +61,15 @@ class_alias(\Misuzu\Http\HttpResponseMessage::class, '\HttpResponse'); class_alias(\Misuzu\Http\HttpRequestMessage::class, '\HttpRequest'); require_once 'utility.php'; +require_once 'src/perms.php'; require_once 'src/audit_log.php'; require_once 'src/changelog.php'; require_once 'src/comments.php'; require_once 'src/manage.php'; -require_once 'src/news.php'; -require_once 'src/perms.php'; require_once 'src/url.php'; +require_once 'src/Forum/perms.php'; require_once 'src/Forum/forum.php'; require_once 'src/Forum/leaderboard.php'; -require_once 'src/Forum/perms.php'; require_once 'src/Forum/poll.php'; require_once 'src/Forum/post.php'; require_once 'src/Forum/topic.php'; @@ -500,7 +499,7 @@ MIG; $inManageMode = starts_with($_SERVER['REQUEST_URI'], '/manage'); $hasManageAccess = !empty($userDisplayInfo['user_id']) && !user_warning_check_restriction($userDisplayInfo['user_id']) - && perms_check_user(MSZ_PERMS_GENERAL, $userDisplayInfo['user_id'], General::PERM_CAN_MANAGE); + && perms_check_user(MSZ_PERMS_GENERAL, $userDisplayInfo['user_id'], MSZ_PERM_GENERAL_CAN_MANAGE); Template::set('has_manage_access', $hasManageAccess); if($inManageMode) { diff --git a/public/index.php b/public/index.php index 2203fdd7..7f8b9d7f 100644 --- a/public/index.php +++ b/public/index.php @@ -5,7 +5,7 @@ use Misuzu\Http\HttpRequestMessage; use Misuzu\Http\Routing\Router; use Misuzu\Http\Routing\Route; -require_once '../misuzu.php'; +require_once __DIR__ . '/../misuzu.php'; $request = HttpRequestMessage::fromGlobals(); @@ -22,6 +22,16 @@ Router::addRoutes( Route::get('/info', 'index', 'Info'), Route::get('/info/([A-Za-z0-9_/]+)', 'page', 'Info'), + // News + Route::get('/news', 'index', 'News')->addChildren( + Route::get('.atom', 'feedIndexAtom'), + Route::get('.rss', 'feedIndexRss'), + Route::get('/([0-9]+)', 'viewCategory'), + Route::get('/([0-9]+).atom', 'feedCategoryAtom'), + Route::get('/([0-9]+).rss', 'feedCategoryRss'), + Route::get('/post/([0-9]+)', 'viewPost') + ), + // Forum Route::group('/forum', 'Forum')->addChildren( Route::get('/mark-as-read', 'markAsReadGET')->addFilters('EnforceLogIn'), @@ -43,6 +53,15 @@ Router::addRoutes( Route::get('/info.php', url('info')), Route::get('/info.php/([A-Za-z0-9_/]+)', 'redir', 'Info'), Route::get('/auth.php', 'legacy', 'Auth'), + Route::get('/news.php', 'legacy', 'News'), + Route::get('/news.php/rss', 'legacy', 'News'), + Route::get('/news.php/atom', 'legacy', 'News'), + Route::get('/news/index.php', 'legacy', 'News'), + Route::get('/news/category.php', 'legacy', 'News'), + Route::get('/news/post.php', 'legacy', 'News'), + Route::get('/news/feed.php', 'legacy', 'News'), + Route::get('/news/feed.php/rss', 'legacy', 'News'), + Route::get('/news/feed.php/atom', 'legacy', 'News'), ); $response = Router::handle($request); diff --git a/public/manage/general/blacklist.php b/public/manage/general/blacklist.php index 51b9df02..cb3d6207 100644 --- a/public/manage/general/blacklist.php +++ b/public/manage/general/blacklist.php @@ -5,7 +5,7 @@ use Misuzu\Net\IPAddressBlacklist; require_once '../../../misuzu.php'; -if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_MANAGE_BLACKLIST)) { +if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_BLACKLIST)) { echo render_error(403); return; } diff --git a/public/manage/general/emoticon.php b/public/manage/general/emoticon.php index 44878a2f..9922ceb5 100644 --- a/public/manage/general/emoticon.php +++ b/public/manage/general/emoticon.php @@ -3,7 +3,7 @@ namespace Misuzu; require_once '../../../misuzu.php'; -if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_MANAGE_EMOTES)) { +if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { echo render_error(403); return; } diff --git a/public/manage/general/emoticons.php b/public/manage/general/emoticons.php index a2255220..a85c9809 100644 --- a/public/manage/general/emoticons.php +++ b/public/manage/general/emoticons.php @@ -3,7 +3,7 @@ namespace Misuzu; require_once '../../../misuzu.php'; -if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_MANAGE_EMOTES)) { +if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_EMOTES)) { echo render_error(403); return; } diff --git a/public/manage/general/logs.php b/public/manage/general/logs.php index 08e13823..7bd32556 100644 --- a/public/manage/general/logs.php +++ b/public/manage/general/logs.php @@ -3,7 +3,7 @@ namespace Misuzu; require_once '../../../misuzu.php'; -if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_VIEW_LOGS)) { +if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_VIEW_LOGS)) { echo render_error(403); return; } diff --git a/public/manage/general/settings.php b/public/manage/general/settings.php index 4d3e154d..d37a3a53 100644 --- a/public/manage/general/settings.php +++ b/public/manage/general/settings.php @@ -3,7 +3,7 @@ namespace Misuzu; require_once '../../../misuzu.php'; -if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General::PERM_MANAGE_CONFIG)) { +if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), MSZ_PERM_GENERAL_MANAGE_CONFIG)) { echo render_error(403); return; } diff --git a/public/manage/news/categories.php b/public/manage/news/categories.php index b923b5f3..f1b5b1b5 100644 --- a/public/manage/news/categories.php +++ b/public/manage/news/categories.php @@ -1,6 +1,8 @@ hasValidOffset()) { echo render_error(404); return; } -$categories = news_categories_get($categoriesPagination->getOffset(), $categoriesPagination->getRange(), true, false, true); +$categories = NewsCategory::all($categoriesPagination, true); Template::render('manage.news.categories', [ 'news_categories' => $categories, diff --git a/public/manage/news/category.php b/public/manage/news/category.php index bb9e94c6..dd89001e 100644 --- a/public/manage/news/category.php +++ b/public/manage/news/category.php @@ -1,6 +1,9 @@ 0) + try { + $categoryInfo = NewsCategory::byId($categoryId); + Template::set('category_info', $categoryInfo); + } catch(NewsCategoryNotFoundException $ex) { + echo render_error(404); + return; + } if(!empty($_POST['category']) && CSRF::validateRequest()) { - $originalCategoryId = (int)($_POST['category']['id'] ?? null); - $categoryId = news_category_create( - $_POST['category']['name'] ?? null, - $_POST['category']['description'] ?? null, - !empty($_POST['category']['hidden']), - $originalCategoryId - ); + if(!isset($categoryInfo)) { + $categoryInfo = new NewsCategory; + $isNew = true; + } + + $categoryInfo->setName($_POST['category']['name']) + ->setDescription($_POST['category']['description']) + ->setHidden(!empty($_POST['category']['hidden'])) + ->save(); audit_log( - $originalCategoryId === $categoryId + empty($isNew) ? MSZ_AUDIT_NEWS_CATEGORY_EDIT : MSZ_AUDIT_NEWS_CATEGORY_CREATE, user_session_current('user_id'), - [$categoryId] + [$categoryInfo->getId()] ); + + if(!empty($isNew)) { + header('Location: ' . url('manage-news-category', ['category' => $categoryInfo->getId()])); + return; + } } -if($categoryId > 0) { - $category = news_category_get($categoryId); -} - -Template::render('manage.news.category', compact('category')); +Template::render('manage.news.category'); diff --git a/public/manage/news/post.php b/public/manage/news/post.php index cd53ad16..b0fb40c1 100644 --- a/public/manage/news/post.php +++ b/public/manage/news/post.php @@ -1,6 +1,10 @@ 0) + try { + $postInfo = NewsPost::byId($postId); + Template::set('post_info', $postInfo); + } catch(NewsPostNotFoundException $ex) { + echo render_error(404); + return; + } + +$categories = NewsCategory::all(null, true); if(!empty($_POST['post']) && CSRF::validateRequest()) { - $originalPostId = (int)($_POST['post']['id'] ?? null); + if(!isset($postInfo)) { + $postInfo = new NewsPost; + $isNew = true; + } + $currentUserId = user_session_current('user_id'); - $title = $_POST['post']['title'] ?? null; - $isFeatured = !empty($_POST['post']['featured']); - $postId = news_post_create( - $title, - $_POST['post']['text'] ?? null, - (int)($_POST['post']['category'] ?? null), - user_session_current('user_id'), - $isFeatured, - null, - $originalPostId - ); + $postInfo->setTitle( $_POST['post']['title']) + ->setText($_POST['post']['text']) + ->setCategoryId($_POST['post']['category']) + ->setFeatured(!empty($_POST['post']['featured'])); + + if(!empty($isNew)) + $postInfo->setUserId($currentUserId); + + $postInfo->save(); audit_log( - $originalPostId === $postId + empty($isNew) ? MSZ_AUDIT_NEWS_POST_EDIT : MSZ_AUDIT_NEWS_POST_CREATE, $currentUserId, - [$postId] + [$postInfo->getId()] ); - if(!$originalPostId && $isFeatured) { - $twitterApiKey = Config::get('twitter.api.key', Config::TYPE_STR); - $twitterApiSecret = Config::get('twitter.api.secret', Config::TYPE_STR); - $twitterToken = Config::get('twitter.token.key', Config::TYPE_STR); - $twitterTokenSecret = Config::get('twitter.token.secret', Config::TYPE_STR); + if(!empty($isNew)) { + if($postInfo->isFeatured()) { + $twitterApiKey = Config::get('twitter.api.key', Config::TYPE_STR); + $twitterApiSecret = Config::get('twitter.api.secret', Config::TYPE_STR); + $twitterToken = Config::get('twitter.token.key', Config::TYPE_STR); + $twitterTokenSecret = Config::get('twitter.token.secret', Config::TYPE_STR); - if(!empty($twitterApiKey) && !empty($twitterApiSecret) - && !empty($twitterToken) && !empty($twitterTokenSecret)) { - Twitter::init($twitterApiKey, $twitterApiSecret, $twitterToken, $twitterTokenSecret); - $url = url('news-post', ['post' => $postId]); - Twitter::sendTweet("News :: {$title}\nhttps://{$_SERVER['HTTP_HOST']}{$url}"); + if(!empty($twitterApiKey) && !empty($twitterApiSecret) + && !empty($twitterToken) && !empty($twitterTokenSecret)) { + Twitter::init($twitterApiKey, $twitterApiSecret, $twitterToken, $twitterTokenSecret); + $url = url('news-post', ['post' => $postInfo->getId()]); + Twitter::sendTweet("News :: {$postInfo->getTitle()}\nhttps://{$_SERVER['HTTP_HOST']}{$url}"); + } } + + header('Location: ' . url('manage-news-post', ['post' => $postInfo->getId()])); + return; } } -if($postId > 0) { - $post = news_post_get($postId); -} - -Template::render('manage.news.post', compact('post', 'categories')); +Template::render('manage.news.post', [ + 'categories' => $categories, +]); diff --git a/public/manage/news/posts.php b/public/manage/news/posts.php index c8b546b1..8ac3f2e3 100644 --- a/public/manage/news/posts.php +++ b/public/manage/news/posts.php @@ -1,6 +1,8 @@ hasValidOffset()) { echo render_error(404); return; } -$posts = news_posts_get( - $postsPagination->getOffset(), - $postsPagination->getRange(), - null, false, true, false -); +$posts = NewsPost::all($postsPagination, false, true, true); Template::render('manage.news.posts', [ 'news_posts' => $posts, diff --git a/public/news.php b/public/news.php index c02f596f..69fd62f8 100644 --- a/public/news.php +++ b/public/news.php @@ -1,5 +1,7 @@ $categoryId]); } -if($postId > 0) { +if($postId > 0) $location = url('news-post', ['post' => $postId]); -} - -if($categoryId > 0) { +if($categoryId > 0) $location = url('news-category', ['category' => $categoryId, 'page' => Pagination::param('page')]); -} redirect($location); diff --git a/public/news/category.php b/public/news/category.php index 2d799c7c..6b2e29b3 100644 --- a/public/news/category.php +++ b/public/news/category.php @@ -1,34 +1,2 @@ hasValidOffset()) { - echo render_error(404); - return; -} - -$posts = news_posts_get( - $categoryPagination->getOffset(), - $categoryPagination->getRange(), - $category['category_id'] -); - -$featured = news_posts_get(0, 10, $category['category_id'], true); - -Template::render('news.category', [ - 'category' => $category, - 'posts' => $posts, - 'featured' => $featured, - 'news_pagination' => $categoryPagination, -]); +require_once __DIR__ . '/../index.php'; diff --git a/public/news/feed.php b/public/news/feed.php index 9541a718..6b2e29b3 100644 --- a/public/news/feed.php +++ b/public/news/feed.php @@ -1,76 +1,2 @@ setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($category['category_name'] ?? 'Featured News')) - ->setDescription($category['category_description'] ?? 'A live featured news feed.') - ->setContentUrl(url_prefix(false) . (empty($category) ? url('news-index') : url('news-category', ['category' => $category['category_id']]))) - ->setFeedUrl(url_prefix(false) . (empty($category) ? url("news-feed-{$feedMode}") : url("news-category-feed-{$feedMode}", ['category' => $category['category_id']]))); - -foreach($posts as $post) { - $postUrl = url_prefix(false) . url('news-post', ['post' => $post['post_id']]); - $commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $post['post_id']]); - $authorUrl = url_prefix(false) . url('user-profile', ['user' => $post['user_id']]); - - $feedItem = (new FeedItem) - ->setTitle($post['post_title']) - ->setSummary(first_paragraph($post['post_text'])) - ->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post['post_text'])) - ->setCreationDate(strtotime($post['post_created'])) - ->setUniqueId($postUrl) - ->setContentUrl($postUrl) - ->setCommentsUrl($commentsUrl) - ->setAuthorName($post['username']) - ->setAuthorUrl($authorUrl); - - if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate()) - $feed->setLastUpdate($feedItem->getCreationDate()); - - $feed->addItem($feedItem); -} - -header("Content-Type: application/{$feedMode}+xml; charset=utf-8"); - -echo $feedSerializer->serializeFeed($feed); +require_once __DIR__ . '/../index.php'; diff --git a/public/news/index.php b/public/news/index.php index ac8f81d4..6b2e29b3 100644 --- a/public/news/index.php +++ b/public/news/index.php @@ -1,31 +1,2 @@ hasValidOffset()) { - echo render_error(404); - return; -} - -$posts = news_posts_get( - $newsPagination->getOffset(), - $newsPagination->getRange(), - null, - true -); - -if(!$posts) { - echo render_error(404); - return; -} - -Template::render('news.index', [ - 'categories' => $categories, - 'posts' => $posts, - 'news_pagination' => $newsPagination, -]); +require_once __DIR__ . '/../index.php'; diff --git a/public/news/post.php b/public/news/post.php index 83a3e64a..6b2e29b3 100644 --- a/public/news/post.php +++ b/public/news/post.php @@ -1,33 +1,2 @@ $post, - 'comments_perms' => comments_get_perms(user_session_current('user_id', 0)), - 'comments_category' => $commentsInfo, - 'comments' => comments_category_get($commentsInfo['category_id'], user_session_current('user_id', 0)), -]); +require_once __DIR__ . '/../index.php'; diff --git a/public/search.php b/public/search.php index 43d7e8ec..ee6e6da6 100644 --- a/public/search.php +++ b/public/search.php @@ -1,6 +1,8 @@ getInherit()) return 'inherit'; - return '#' . $this->getHex(); } @@ -141,4 +140,8 @@ class Colour { return $this->getLuminance() > self::READABILITY_THRESHOLD ? $dark : $light; } + + public function __toString() { + return $this->getCSS(); + } } diff --git a/src/DB.php b/src/DB.php index 94e68d77..982ee603 100644 --- a/src/DB.php +++ b/src/DB.php @@ -7,6 +7,9 @@ use Misuzu\Database\Database; final class DB { private static $instance; + public const PREFIX = 'msz_'; + public const QUERY_SELECT = 'SELECT %2$s FROM `' . self::PREFIX . '%1$s` AS %1$s'; + public const ATTRS = [ PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, diff --git a/src/Forum/forum.php b/src/Forum/forum.php index ca6a878b..04e5296d 100644 --- a/src/Forum/forum.php +++ b/src/Forum/forum.php @@ -1,51 +1,4 @@ $onlineUsers, 'birthdays' => $birthdays, 'featured_changelog' => $changelog, - 'featured_news' => $news, + 'featured_news' => $featuredNews, 'linked_data' => $linkedData ?? null, ]); } diff --git a/src/Http/Handlers/NewsHandler.php b/src/Http/Handlers/NewsHandler.php new file mode 100644 index 00000000..a0a24699 --- /dev/null +++ b/src/Http/Handlers/NewsHandler.php @@ -0,0 +1,202 @@ +hasValidOffset()) + return 404; + + $response->setTemplate('news.index', [ + 'categories' => $categories, + 'posts' => NewsPost::all($newsPagination, true), + 'news_pagination' => $newsPagination, + ]); + } + + public function viewCategory(HttpResponse $response, HttpRequest $request, int $categoryId) { + try { + $categoryInfo = NewsCategory::byId($categoryId); + } catch(NewsCategoryNotFoundException $ex) { + return 404; + } + + $categoryPagination = new Pagination(NewsPost::countByCategory($categoryInfo), 5); + if(!$categoryPagination->hasValidOffset()) + return 404; + + $posts = NewsPost::byCategory($categoryInfo, $categoryPagination); + + $response->setTemplate('news.category', [ + 'category_info' => $categoryInfo, + 'posts' => $posts, + 'news_pagination' => $categoryPagination, + ]); + } + + public function viewPost(HttpResponse $response, HttpRequest $request, int $postId) { + try { + $postInfo = NewsPost::byId($postId); + } catch(NewsPostNotFoundException $ex) { + return 404; + } + + if(!$postInfo->isPublished() || $postInfo->isDeleted()) + return 404; + + $postInfo->ensureCommentsSection(); + $commentsInfo = $postInfo->getCommentSection(); + + $response->setTemplate('news.post', [ + 'post_info' => $postInfo, + 'comments_perms' => comments_get_perms(user_session_current('user_id', 0)), + 'comments_category' => $commentsInfo, + 'comments' => comments_category_get($commentsInfo['category_id'], user_session_current('user_id', 0)), + ]); + + } + + private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed { + $hasCategory = !empty($categoryInfo); + $pagination = new Pagination(10); + $posts = $hasCategory ? NewsPost::byCategory($categoryInfo, $pagination) : NewsPost::all($pagination, true); + + $feed = (new Feed) + ->setTitle(Config::get('site.name', Config::TYPE_STR, 'Misuzu') . ' » ' . ($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) { + $postUrl = url_prefix(false) . url('news-post', ['post' => $post->getId()]); + $commentsUrl = url_prefix(false) . url('news-post-comments', ['post' => $post->getId()]); + $authorUrl = url_prefix(false) . url('user-profile', ['user' => $post->getUser()->getId()]); + + $feedItem = (new FeedItem) + ->setTitle($post->getTitle()) + ->setSummary(first_paragraph($post->getText())) + ->setContent(Parser::instance(Parser::MARKDOWN)->parseText($post->getText())) + ->setCreationDate(strtotime($post->getCreatedTime())) + ->setUniqueId($postUrl) + ->setContentUrl($postUrl) + ->setCommentsUrl($commentsUrl) + ->setAuthorName($post->getUser()->getUsername()) + ->setAuthorUrl($authorUrl); + + if(!$feed->hasLastUpdate() || $feed->getLastUpdate() < $feedItem->getCreationDate()) + $feed->setLastUpdate($feedItem->getCreationDate()); + + $feed->addItem($feedItem); + } + + return $feed; + } + + public function feedIndexAtom(HttpResponse $response, HttpRequest $request) { + $response->setContentType('application/atom+xml; charset=utf-8'); + return (new AtomFeedSerializer)->serializeFeed( + self::createFeed('atom', null, NewsPost::all(new Pagination(10), true)) + ); + } + + public function feedIndexRss(HttpResponse $response, HttpRequest $request) { + $response->setContentType('application/rss+xml; charset=utf-8'); + return (new RssFeedSerializer)->serializeFeed( + self::createFeed('rss', null, NewsPost::all(new Pagination(10), true)) + ); + } + + public function feedCategoryAtom(HttpResponse $response, HttpRequest $request, int $categoryId) { + try { + $categoryInfo = NewsCategory::byId($categoryId); + } catch(NewsCategoryNotFoundException $ex) { + return 404; + } + + $response->setContentType('application/atom+xml; charset=utf-8'); + return (new AtomFeedSerializer)->serializeFeed( + self::createFeed('atom', $categoryInfo, NewsPost::byCategory($categoryInfo, new Pagination(10))) + ); + } + + public function feedCategoryRss(HttpResponse $response, HttpRequest $request, int $categoryId) { + try { + $categoryInfo = NewsCategory::byId($categoryId); + } catch(NewsCategoryNotFoundException $ex) { + return 404; + } + + $response->setContentType('application/rss+xml; charset=utf-8'); + return (new RssFeedSerializer)->serializeFeed( + self::createFeed('rss', $categoryInfo, NewsPost::byCategory($categoryInfo, new Pagination(10))) + ); + } + + public function legacy(HttpResponse $response, HttpRequest $request) { + $location = url('news-index'); + + switch('/' . trim($request->getUri()->getPath(), '/')) { + case '/news/index.php': + $location = url('news-index', [ + 'page' => $request->getQueryParam('page', FILTER_SANITIZE_NUMBER_INT), + ]); + break; + + case '/news/category.php': + $location = url('news-category', [ + 'category' => $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT), + 'page' => $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT), + ]); + break; + + case '/news/post.php': + $location = url('news-post', [ + 'post' => $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT), + ]); + break; + + case '/news/feed.php': + return 400; + + case '/news/feed.php/rss': + case '/news/feed.php/atom': + $feedType = basename($request->getUri()->getPath()); + $catId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT); + $location = url($catId > 0 ? "news-category-feed-{$feedType}" : "news-feed-{$feedType}", ['category' => $catId]); + break; + + case '/news.php/rss': + case '/news.php/atom': + $feedType = basename($request->getUri()->getPath()); + case '/news.php': + $postId = $request->getQueryParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT); + if($postId > 0) + $location = url('news-post', ['post' => $postId]); + else { + $catId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT); + $pageId = $request->getQueryParam('page', FILTER_SANITIZE_NUMBER_INT); + $location = url($catId > 0 ? (isset($feedType) ? "news-category-feed-{$feedType}" : 'news-category') : (isset($feedType) ? "news-feed-{$feedType}" : 'news-index'), ['category' => $catId, 'page' => $pageId]); + } + break; + } + + $response->redirect($location, true); + } +} diff --git a/src/Http/Routing/Route.php b/src/Http/Routing/Route.php index 056701b0..ccccb784 100644 --- a/src/Http/Routing/Route.php +++ b/src/Http/Routing/Route.php @@ -59,7 +59,7 @@ class Route implements Serializable { public function getPath(): string { $path = $this->path; if($this->parentRoute !== null) - $path = $this->parentRoute->getPath() . '/' . trim($path, '/'); + $path = $this->parentRoute->getPath() . ($path[0] !== '.' ? '/' : '') . trim($path, '/'); return $path; } public function setPath(string $path): self { @@ -91,7 +91,7 @@ class Route implements Serializable { $matches = []; if(!in_array($request->getMethod(), $this->methods)) return false; - return preg_match('#^' . $this->getPath() . '$#', $request->getUri()->getPath(), $matches) === 1; + return preg_match('#^' . $this->getPath() . '$#', '/' . trim($request->getUri()->getPath(), '/'), $matches) === 1; } public function serialize() { diff --git a/src/News/NewsCategory.php b/src/News/NewsCategory.php new file mode 100644 index 00000000..31aa4bc7 --- /dev/null +++ b/src/News/NewsCategory.php @@ -0,0 +1,143 @@ +category_id < 1 ? -1 : $this->category_id; + } + + public function getName(): string { + return $this->category_name ?? ''; + } + public function setName(string $name): self { + $this->category_name = $name; + return $this; + } + + public function getDescription(): string { + return $this->category_description ?? ''; + } + public function setDescription(string $description): self { + $this->category_description = $description; + return $this; + } + + public function isHidden(): bool { + return $this->category_is_hidden !== 0; + } + public function setHidden(bool $hide): self { + $this->category_is_hidden = $hide ? 1 : 0; + return $this; + } + + public function getCreatedTime(): int { + return $this->category_created === null ? -1 : $this->category_created; + } + + // Purely cosmetic, use ::countAll for pagination + public function getPostCount(): int { + if($this->postCount < 0) + $this->postCount = (int)DB::prepare(' + SELECT COUNT(`post_id`) + FROM `msz_news_posts` + WHERE `category_id` = :cat_id + AND `post_scheduled` <= NOW() + AND `post_deleted` IS NULL + ')->bind('cat_id', $this->getId())->fetchColumn(); + + return $this->postCount; + } + + public function save(): void { + $isInsert = $this->getId() < 1; + if($isInsert) { + $query = 'INSERT INTO `%1$s%2$s` (`category_name`, `category_description`, `category_is_hidden`) VALUES' + . ' (:name, :description, :hidden)'; + } else { + $query = 'UPDATE `%1$s%2$s` SET `category_name` = :name, `category_description` = :description, `category_is_hidden` = :hidden' + . ' WHERE `category_id` = :category'; + } + + $savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) + ->bind('name', $this->category_name) + ->bind('description', $this->category_description) + ->bind('hidden', $this->category_is_hidden); + + if($isInsert) { + $this->category_id = $savePost->executeGetId(); + $this->category_created = time(); + } else { + $savePost->bind('category', $this->getId()) + ->execute(); + } + } + + private static function countQueryBase(): string { + return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`category_id`)', self::TABLE)); + } + public static function countAll(bool $showHidden = false): int { + return (int)DB::prepare(self::countQueryBase() + . ($showHidden ? '' : ' WHERE `category_is_hidden` = 0')) + ->fetchColumn(); + } + + private static function byQueryBase(): string { + return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE)); + } + public static function byId(int $categoryId): self { + $getCat = DB::prepare(self::byQueryBase() . ' WHERE `category_id` = :cat_id'); + $getCat->bind('cat_id', $categoryId); + $cat = $getCat->fetchObject(self::class); + if(!$cat) + throw new NewsCategoryNotFoundException; + return $cat; + } + public static function all(?Pagination $pagination = null, bool $showHidden = false): array { + $catsQuery = self::byQueryBase() + . ($showHidden ? '' : ' WHERE `category_is_hidden` = 0') + . ' ORDER BY `category_id` ASC'; + + if($pagination !== null) + $catsQuery .= ' LIMIT :range OFFSET :offset'; + + $getCats = DB::prepare($catsQuery); + + if($pagination !== null) + $getCats->bind('range', $pagination->getRange()) + ->bind('offset', $pagination->getOffset()); + + return $getCats->fetchObjects(self::class); + } + + // Twig shim for the news category list in manage, don't use this class as an array normally. + public function offsetExists($offset): bool { + return $offset === 'name' || $offset === 'id'; + } + public function offsetGet($offset) { + return $this->{'get' . ucfirst($offset)}(); + } + public function offsetSet($offset, $value) {} + public function offsetUnset($offset) {} +} diff --git a/src/News/NewsException.php b/src/News/NewsException.php new file mode 100644 index 00000000..aaa4c004 --- /dev/null +++ b/src/News/NewsException.php @@ -0,0 +1,6 @@ +post_id < 1 ? -1 : $this->post_id; + } + + public function getCategoryId(): int { + return $this->category_id < 1 ? -1 : $this->category_id; + } + public function setCategoryId(int $categoryId): self { + $this->category_id = max(1, $categoryId); + return $this; + } + public function getCategory(): NewsCategory { + if($this->category === null && ($catId = $this->getCategoryId()) > 0) + $this->category = NewsCategory::byId($catId); + return $this->category; + } + public function setCategory(NewsCategory $category): self { + $this->category_id = $category->getId(); + return $this; + } + + public function getUserId(): int { + return $this->user_id < 1 ? -1 : $this->user_id; + } + public function setUserId(int $userId): self { + $this->user_id = $userId < 1 ? null : $userId; + return $this; + } + public function getUser(): ?User { + if($this->user === null && ($userId = $this->getUserId()) > 0) + $this->user = User::byId($userId); + return $this->user; + } + public function setUser(?User $user): self { + $this->user_id = $user === null ? null : $user->getId(); + return $this; + } + + public function getCommentSectionId(): int { + return $this->comment_section_id < 1 ? -1 : $this->comment_section_id; + } + public function hasCommentsSection(): bool { + return $this->getCommentSectionId() > 0; + } + public function getCommentSection() { + if($this->comments === null && ($sectionId = $this->getCommentSectionId()) > 0) + $this->comments = comments_category_info($sectionId); + return $this->comments; + } + // Temporary solution, should be a method of whatever getCommentSection returns + public function getCommentCount(): int { + if($this->commentCount < 0) + $this->commentCount = (int)DB::prepare(' + SELECT COUNT(`comment_id`) + FROM `msz_comments_posts` + WHERE `category_id` = :cat_id + AND `comment_deleted` IS NULL + ')->bind('cat_id', $this->getCommentSectionId())->fetchColumn(); + + return $this->commentCount; + } + + public function isFeatured(): bool { + return $this->post_is_featured !== 0; + } + public function setFeatured(bool $featured): self { + $this->post_is_featured = $featured ? 1 : 0; + return $this; + } + + public function getTitle(): string { + return $this->post_title; + } + public function setTitle(string $title): self { + $this->post_title = $title; + return $this; + } + + public function getText(): string { + return $this->post_text; + } + public function setText(string $text): self { + $this->post_text = $text; + return $this; + } + + public function getScheduledTime(): int { + return $this->post_scheduled === null ? -1 : $this->post_scheduled; + } + public function setScheduledTime(int $scheduled): self { + $time = ($time = $this->getCreatedTime()) < 0 ? time() : $time; + $this->post_scheduled = $scheduled < $time ? $time : $scheduled; + return $this; + } + public function isPublished(): bool { + return $this->getScheduledTime() < time(); + } + + public function getCreatedTime(): int { + return $this->post_created === null ? -1 : $this->post_created; + } + + public function getUpdatedTime(): int { + return $this->post_updated === null ? -1 : $this->post_updated; + } + public function isEdited(): bool { + return $this->getUpdatedTime() >= 0; + } + + public function getDeletedTime(): int { + return $this->post_deleted === null ? -1 : $this->post_deleted; + } + public function isDeleted(): bool { + return $this->getDeletedTime() >= 0; + } + public function setDeleted(bool $isDeleted): self { + $this->post_deleted = $isDeleted ? time() : null; + return $this; + } + + public function ensureCommentsSection(): void { + if($this->hasCommentsSection()) + return; + + $this->comments = comments_category_create("news-{$this->getId()}"); + + if($this->comments !== null) { + $this->comment_section_id = (int)$this->comments['category_id']; + DB::prepare('UPDATE `msz_news_posts` SET `comment_section_id` = :comment_section_id WHERE `post_id` = :post_id') + ->execute([ + 'comment_section_id' => $this->getCommentSectionId(), + 'post_id' => $this->getId(), + ]); + } + } + + public function save(): void { + $isInsert = $this->getId() < 1; + if($isInsert) { + $query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `post_is_featured`, `post_title`' + . ', `post_text`, `post_scheduled`, `post_deleted`) VALUES' + . ' (:category, :user, :featured, :title, :text, FROM_UNIXTIME(:scheduled), FROM_UNIXTIME(:deleted))'; + } else { + $query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `post_is_featured` = :featured' + . ', `post_title` = :title, `post_text` = :text, `post_scheduled` = FROM_UNIXTIME(:scheduled)' + . ', `post_deleted` = FROM_UNIXTIME(:deleted)' + . ' WHERE `post_id` = :post'; + } + + $savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE)) + ->bind('category', $this->category_id) + ->bind('user', $this->user_id) + ->bind('featured', $this->post_is_featured) + ->bind('title', $this->post_title) + ->bind('text', $this->post_text) + ->bind('scheduled', $this->post_scheduled) + ->bind('deleted', $this->post_deleted); + + if($isInsert) { + $this->post_id = $savePost->executeGetId(); + $this->post_created = time(); + } else { + $this->post_updated = time(); + $savePost->bind('post', $this->getId()) + ->execute(); + } + } + + private static function countQueryBase(): string { + return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf('COUNT(%s.`post_id`)', self::TABLE)); + } + public static function countAll(bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): int { + return (int)DB::prepare(self::countQueryBase() + . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)' + . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')) + ->bind('only_featured', $onlyFeatured ? 1 : 0) + ->fetchColumn(); + } + public static function countByCategory(NewsCategory $category, bool $includeScheduled = false, bool $includeDeleted = false): int { + return (int)DB::prepare(self::countQueryBase() + . ' WHERE `category_id` = :cat_id' + . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL')) + ->bind('cat_id', $category->getId()) + ->fetchColumn(); + } + + private static function byQueryBase(): string { + return sprintf(DB::QUERY_SELECT, self::TABLE, sprintf(self::SELECT, self::TABLE)); + } + public static function byId(int $postId): self { + $post = DB::prepare(self::byQueryBase() . ' WHERE `post_id` = :post_id') + ->bind('post_id', $postId) + ->fetchObject(self::class); + if(!$post) + throw new NewsPostNotFoundException; + return $post; + } + public static function bySearchQuery(string $query, bool $includeScheduled = false, bool $includeDeleted = false): array { + return DB::prepare( + self::byQueryBase() + . ' WHERE MATCH(`post_title`, `post_text`) AGAINST (:query IN NATURAL LANGUAGE MODE)' + . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') + . ' ORDER BY `post_id` DESC' + ) ->bind('query', $query) + ->fetchObjects(self::class); + } + public static function byCategory(NewsCategory $category, ?Pagination $pagination = null, bool $includeScheduled = false, bool $includeDeleted = false): array { + $postsQuery = self::byQueryBase() + . ' WHERE `category_id` = :cat_id' + . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') + . ' ORDER BY `post_id` DESC'; + + if($pagination !== null) + $postsQuery .= ' LIMIT :range OFFSET :offset'; + + $getPosts = DB::prepare($postsQuery) + ->bind('cat_id', $category->getId()); + + if($pagination !== null) + $getPosts->bind('range', $pagination->getRange()) + ->bind('offset', $pagination->getOffset()); + + return $getPosts->fetchObjects(self::class); + } + public static function all(?Pagination $pagination = null, bool $onlyFeatured = false, bool $includeScheduled = false, bool $includeDeleted = false): array { + $postsQuery = self::byQueryBase() + . ' WHERE IF(:only_featured, `post_is_featured` <> 0, 1)' + . ($includeScheduled ? '' : ' AND `post_scheduled` < NOW()') + . ($includeDeleted ? '' : ' AND `post_deleted` IS NULL') + . ' ORDER BY `post_id` DESC'; + + if($pagination !== null) + $postsQuery .= ' LIMIT :range OFFSET :offset'; + + $getPosts = DB::prepare($postsQuery) + ->bind('only_featured', $onlyFeatured ? 1 : 0); + + if($pagination !== null) + $getPosts->bind('range', $pagination->getRange()) + ->bind('offset', $pagination->getOffset()); + + return $getPosts->fetchObjects(self::class); + } +} diff --git a/src/Pagination.php b/src/Pagination.php index e0e58e18..213d44a4 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -10,9 +10,9 @@ final class Pagination { private int $range = 0; private int $offset = 0; - public function __construct(int $count, int $range, ?string $readParam = self::DEFAULT_PARAM) { - $this->count = $count; - $this->range = $range; + public function __construct(int $count, int $range = -1, ?string $readParam = self::DEFAULT_PARAM) { + $this->count = max(0, $count); + $this->range = $range < 0 ? $count : $range; if(!empty($readParam)) $this->readPage($readParam); diff --git a/src/Users/User.php b/src/Users/User.php index 7ce924d2..ab5cc0a5 100644 --- a/src/Users/User.php +++ b/src/Users/User.php @@ -53,7 +53,8 @@ class User { return static::get($createUser); } - public static function get(int $userId): ?User { + public static function get(int $userId): ?User { return self::byId($userId); } + public static function byId(int $userId): ?User { return DB::prepare(self::USER_SELECT . 'WHERE `user_id` = :user_id') ->bind('user_id', $userId) ->fetchObject(User::class); @@ -72,10 +73,12 @@ class User { ->fetchObject(User::class); } - public function hasUserId(): bool { + public function hasUserId(): bool { return $this->hasId(); } + public function getUserId(): int { return $this->getId(); } + public function hasId(): bool { return isset($this->user_id) && $this->user_id > 0; } - public function getUserId(): int { + public function getId(): int { return $this->user_id ?? 0; } diff --git a/src/Users/user_legacy.php b/src/Users/user_legacy.php index 112241a8..a5be0f00 100644 --- a/src/Users/user_legacy.php +++ b/src/Users/user_legacy.php @@ -3,20 +3,6 @@ // Never ever EVER use it for ANYTHING other than determining display colours, there's a small chance that it might not be accurate. // And even if it were, roles properties are aggregated and thus must all be accounted for. -define('MSZ_PERM_USER_EDIT_PROFILE', 1); -define('MSZ_PERM_USER_CHANGE_AVATAR', 1 << 1); -define('MSZ_PERM_USER_CHANGE_BACKGROUND', 1 << 2); -define('MSZ_PERM_USER_EDIT_ABOUT', 1 << 3); -define('MSZ_PERM_USER_EDIT_BIRTHDATE', 1 << 4); -define('MSZ_PERM_USER_EDIT_SIGNATURE', 1 << 5); - -define('MSZ_PERM_USER_MANAGE_USERS', 1 << 20); -define('MSZ_PERM_USER_MANAGE_ROLES', 1 << 21); -define('MSZ_PERM_USER_MANAGE_PERMS', 1 << 22); -define('MSZ_PERM_USER_MANAGE_REPORTS', 1 << 23); -define('MSZ_PERM_USER_MANAGE_WARNINGS', 1 << 24); -//define('MSZ_PERM_USER_MANAGE_BLACKLISTS', 1 << 25); // Replaced with General::PERM_MANAGE_BLACKLIST - define( 'MSZ_USERS_PASSWORD_HASH_ALGO', defined('PASSWORD_ARGON2ID') diff --git a/src/changelog.php b/src/changelog.php index d93f8dc8..6dc80f60 100644 --- a/src/changelog.php +++ b/src/changelog.php @@ -1,8 +1,4 @@ 'can-manage', 'title' => 'Can access the management panel.', - 'perm' => \Misuzu\General::PERM_CAN_MANAGE, + 'perm' => MSZ_PERM_GENERAL_CAN_MANAGE, ], [ 'section' => 'view-logs', 'title' => 'Can view audit logs.', - 'perm' => \Misuzu\General::PERM_VIEW_LOGS, + 'perm' => MSZ_PERM_GENERAL_VIEW_LOGS, ], [ 'section' => 'manage-emotes', 'title' => 'Can manage emoticons.', - 'perm' => \Misuzu\General::PERM_MANAGE_EMOTES, + 'perm' => MSZ_PERM_GENERAL_MANAGE_EMOTES, ], [ 'section' => 'manage-settings', 'title' => 'Can manage general Misuzu settings.', - 'perm' => \Misuzu\General::PERM_MANAGE_CONFIG, + 'perm' => MSZ_PERM_GENERAL_MANAGE_CONFIG, ], [ 'section' => 'tester', 'title' => 'Can use experimental features.', - 'perm' => \Misuzu\General::PERM_IS_TESTER, + 'perm' => MSZ_PERM_GENERAL_IS_TESTER, ], [ 'section' => 'manage-blacklist', 'title' => 'Can manage blacklistings.', - 'perm' => \Misuzu\General::PERM_MANAGE_BLACKLIST, + 'perm' => MSZ_PERM_GENERAL_MANAGE_BLACKLIST, ], ], ], diff --git a/src/news.php b/src/news.php deleted file mode 100644 index 7ace75b9..00000000 --- a/src/news.php +++ /dev/null @@ -1,327 +0,0 @@ -bind('id', $postId); - } - - $post->bind('title', $title); - $post->bind('text', $text); - $post->bind('category', $category); - $post->bind('user', $user); - $post->bind('featured', $featured ? 1 : 0); - $post->bind('scheduled', empty($scheduled) ? null : date('Y-m-d H:i:s', $scheduled)); - - return $post->execute() ? ($postId < 1 ? \Misuzu\DB::lastId() : $postId) : 0; -} - -function news_category_create(string $name, string $description, bool $isHidden, ?int $categoryId = null): int { - if($categoryId < 1) { - $category = \Misuzu\DB::prepare(' - INSERT INTO `msz_news_categories` - (`category_name`, `category_description`, `category_is_hidden`) - VALUES - (:name, :description, :hidden) - '); - } else { - $category = \Misuzu\DB::prepare(' - UPDATE `msz_news_categories` - SET `category_name` = :name, - `category_description` = :description, - `category_is_hidden` = :hidden - WHERE `category_id` = :id - '); - $category->bind('id', $categoryId); - } - - $category->bind('name', $name); - $category->bind('description', $description); - $category->bind('hidden', $isHidden ? 1 : 0); - - return $category->execute() ? ($categoryId < 1 ? \Misuzu\DB::lastId() : $categoryId) : 0; -} - -function news_categories_get( - int $offset, - int $take, - bool $includePostCount = false, - bool $featuredOnly = false, - bool $includeHidden = false, - bool $exposeScheduled = false, - bool $excludeDeleted = true -): array { - $getAll = $offset < 0 || $take < 1; - - if($includePostCount) { - $query = sprintf( - ' - SELECT - c.`category_id`, c.`category_name`, c.`category_is_hidden`, - c.`category_created`, - ( - SELECT COUNT(p.`post_id`) - FROM `msz_news_posts` as p - WHERE p.`category_id` = c.`category_id` %2$s %3$s %4$s - ) as `posts_count` - FROM `msz_news_categories` as c - %5$s - GROUP BY c.`category_id` - ORDER BY c.`category_id` DESC - %1$s - ', - $getAll ? '' : 'LIMIT :offset, :take', - $featuredOnly ? 'AND p.`post_is_featured` != 0' : '', - $exposeScheduled ? '' : 'AND p.`post_scheduled` < NOW()', - $excludeDeleted ? 'AND p.`post_deleted` IS NULL' : '', - $includeHidden ? '' : 'WHERE c.`category_is_hidden` = 0' - ); - } else { - $query = sprintf( - ' - SELECT - `category_id`, `category_name`, `category_is_hidden`, - `category_created` - FROM `msz_news_categories` - %2$s - ORDER BY `category_id` DESC - %1$s - ', - $getAll ? '' : 'LIMIT :offset, :take', - $includeHidden ? '' : 'WHERE c.`category_is_hidden` != 0' - ); - } - - $getCats = \Misuzu\DB::prepare($query); - - if(!$getAll) { - $getCats->bind('offset', $offset); - $getCats->bind('take', $take); - } - - return $getCats->fetchAll(); -} - -function news_categories_count(bool $includeHidden = false): int { - $countCats = \Misuzu\DB::prepare(sprintf(' - SELECT COUNT(`category_id`) - FROM `msz_news_categories` - %s - ', $includeHidden ? '' : 'WHERE `category_is_hidden` = 0')); - - return (int)$countCats->fetchColumn(); -} - -function news_category_get( - int $category, - bool $includePostCount = false, - bool $featuredOnly = false, - bool $exposeScheduled = false, - bool $excludeDeleted = true -): array { - if($includePostCount) { - $query = sprintf( - ' - SELECT - c.`category_id`, c.`category_name`, c.`category_description`, - c.`category_is_hidden`, c.`category_created`, - ( - SELECT COUNT(p.`post_id`) - FROM `msz_news_posts` as p - WHERE p.`category_id` = c.`category_id` %1$s %2$s %3$s - ) as `posts_count` - FROM `msz_news_categories` as c - WHERE c.`category_id` = :category - GROUP BY c.`category_id` - ', - $featuredOnly ? 'AND p.`post_is_featured` != 0' : '', - $exposeScheduled ? '' : 'AND p.`post_scheduled` < NOW()', - $excludeDeleted ? 'AND p.`post_deleted` IS NULL' : '' - ); - } else { - $query = ' - SELECT - `category_id`, `category_name`, `category_description`, - `category_is_hidden`, `category_created` - FROM `msz_news_categories` - WHERE `category_id` = :category - GROUP BY `category_id` - '; - } - - $getCategory = \Misuzu\DB::prepare($query); - $getCategory->bind('category', $category); - return $getCategory->fetch(); -} - -function news_posts_count( - ?int $category = null, - bool $featuredOnly = false, - bool $exposeScheduled = false, - bool $excludeDeleted = true -): int { - $hasCategory= $category !== null; - - $countPosts = \Misuzu\DB::prepare(sprintf( - ' - SELECT COUNT(`post_id`) - FROM `msz_news_posts` - WHERE %1$s %2$s %3$s %4$s - ', - $hasCategory ? '`category_id` = :category' : '1', - $featuredOnly ? 'AND `post_is_featured` != 0' : '', - $exposeScheduled ? '' : 'AND `post_scheduled` < NOW()', - $excludeDeleted ? 'AND `post_deleted` IS NULL' : '' - )); - - if($hasCategory) { - $countPosts->bind('category', $category); - } - - return (int)$countPosts->fetchColumn(); -} - -function news_posts_get( - int $offset, - int $take, - ?int $category = null, - bool $featuredOnly = false, - bool $exposeScheduled = false, - bool $excludeDeleted = true -): array { - $getAll = $offset < 0 || $take < 1; - $hasCategory = $category !== null; - - $getPosts = \Misuzu\DB::prepare(sprintf( - ' - SELECT - p.`post_id`, p.`post_is_featured`, p.`post_title`, p.`post_text`, p.`comment_section_id`, - p.`post_created`, p.`post_updated`, p.`post_deleted`, p.`post_scheduled`, - c.`category_id`, c.`category_name`, - u.`user_id`, u.`username`, - COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`, - ( - SELECT COUNT(`comment_id`) - FROM `msz_comments_posts` - WHERE `category_id` = `comment_section_id` - AND `comment_deleted` IS NULL - ) as `post_comments` - FROM `msz_news_posts` as p - LEFT JOIN `msz_news_categories` as c - ON p.`category_id` = c.`category_id` - LEFT JOIN `msz_users` as u - ON p.`user_id` = u.`user_id` - LEFT JOIN `msz_roles` as r - ON u.`display_role` = r.`role_id` - WHERE %5$s %2$s %3$s %4$s - ORDER BY p.`post_created` DESC - %1$s - ', - $getAll ? '' : 'LIMIT :offset, :take', - $featuredOnly ? 'AND p.`post_is_featured` != 0' : '', - $exposeScheduled ? '' : 'AND p.`post_scheduled` < NOW()', - $excludeDeleted ? 'AND p.`post_deleted` IS NULL' : '', - $hasCategory ? 'p.`category_id` = :category' : '1' - )); - - if($hasCategory) { - $getPosts->bind('category', $category); - } - - if(!$getAll) { - $getPosts->bind('take', $take); - $getPosts->bind('offset', $offset); - } - - return $getPosts->fetchAll(); -} - -function news_posts_search(string $query): array { - $searchPosts = \Misuzu\DB::prepare(' - SELECT - p.`post_id`, p.`post_is_featured`, p.`post_title`, p.`post_text`, p.`comment_section_id`, - p.`post_created`, p.`post_updated`, p.`post_deleted`, p.`post_scheduled`, - c.`category_id`, c.`category_name`, - u.`user_id`, u.`username`, - COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`, - ( - SELECT COUNT(`comment_id`) - FROM `msz_comments_posts` - WHERE `category_id` = `comment_section_id` - AND `comment_deleted` IS NULL - ) as `post_comments` - FROM `msz_news_posts` as p - LEFT JOIN `msz_news_categories` as c - ON p.`category_id` = c.`category_id` - LEFT JOIN `msz_users` as u - ON p.`user_id` = u.`user_id` - LEFT JOIN `msz_roles` as r - ON u.`display_role` = r.`role_id` - WHERE MATCH(`post_title`, `post_text`) - AGAINST (:query IN NATURAL LANGUAGE MODE) - AND p.`post_deleted` IS NULL - AND p.`post_scheduled` < NOW() - ORDER BY p.`post_created` DESC - '); - $searchPosts->bind('query', $query); - - return $searchPosts->fetchAll(); -} - -function news_post_comments_set(int $postId, int $sectionId): void { - \Misuzu\DB::prepare(' - UPDATE `msz_news_posts` - SET `comment_section_id` = :comment_section_id - WHERE `post_id` = :post_id - ')->execute([ - 'comment_section_id' => $sectionId, - 'post_id' => $postId, - ]); -} - -function news_post_get(int $postId): array { - $getPost = \Misuzu\DB::prepare(' - SELECT - p.`post_id`, p.`post_title`, p.`post_text`, p.`post_is_featured`, p.`post_scheduled`, - p.`post_created`, p.`post_updated`, p.`post_deleted`, p.`comment_section_id`, - c.`category_id`, c.`category_name`, - u.`user_id`, u.`username`, - COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour` - FROM `msz_news_posts` as p - LEFT JOIN `msz_news_categories` as c - ON p.`category_id` = c.`category_id` - LEFT JOIN `msz_users` as u - ON p.`user_id` = u.`user_id` - LEFT JOIN `msz_roles` as r - ON u.`display_role` = r.`role_id` - WHERE `post_id` = :post_id - '); - $getPost->bind(':post_id', $postId); - return $getPost->fetch(); -} diff --git a/src/perms.php b/src/perms.php index b8f38e73..361e851f 100644 --- a/src/perms.php +++ b/src/perms.php @@ -1,10 +1,48 @@ ['/changelog.php', ['d' => '']], 'changelog-tag' => ['/changelog.php', ['t' => '']], - 'news-index' => ['/news', ['page' => '']], - 'news-post' => ['/news/post.php', ['p' => '']], - 'news-post-comments' => ['/news/post.php', ['p' => ''], 'comments'], - 'news-category' => ['/news/category.php', ['c' => '', 'p' => '']], - 'news-feed-rss' => ['/news/feed.php/rss'], - 'news-category-feed-rss' => ['/news/feed.php/rss', ['c' => '']], - 'news-feed-atom' => ['/news/feed.php/atom'], - 'news-category-feed-atom' => ['/news/feed.php/atom', ['c' => '']], + 'news-index' => ['/news', ['p' => '']], + 'news-category' => ['/news/', ['p' => '']], + 'news-post' => ['/news/post/'], + 'news-post-comments' => ['/news/post/', [], 'comments'], + 'news-feed-rss' => ['/news.rss'], + 'news-category-feed-rss' => ['/news/.rss'], + 'news-feed-atom' => ['/news.atom'], + 'news-category-feed-atom' => ['/news/.atom'], 'forum-index' => ['/forum'], 'forum-leaderboard' => ['/forum/leaderboard.php', ['id' => '', 'mode' => '']], @@ -155,9 +155,8 @@ function url(string $name, array $variables = []): string { foreach($info[1] as $key => $value) { $value = url_variable($value, $variables); - if(empty($value) || ($key === 'page' && $value < 2)) { + if(empty($value) || ($key === 'page' && $value < 2)) continue; - } $url .= sprintf('%s=%s&', $key, $value); } @@ -181,16 +180,21 @@ function url_redirect(string $name, array $variables = []): void { } function url_variable(string $value, array $variables): string { - if(starts_with($value, '<') && ends_with($value, '>')) { + if(starts_with($value, '<') && ends_with($value, '>')) return $variables[trim($value, '<>')] ?? ''; - } - if(starts_with($value, '[') && ends_with($value, ']')) { + if(starts_with($value, '[') && ends_with($value, ']')) return constant(trim($value, '[]')); - } - if(starts_with($value, '{') && ends_with($value, '}')) { + if(starts_with($value, '{') && ends_with($value, '}')) return \Misuzu\CSRF::token(); + + // Hack that allows variables with file extensions + $pathInfo = pathinfo($value); + if($value !== $pathInfo['filename']) { + $fallback = url_variable($pathInfo['filename'], $variables); + if($fallback !== $pathInfo['filename']) + return $fallback . '.' . $pathInfo['extension']; } return $value; diff --git a/templates/_layout/comments.twig b/templates/_layout/comments.twig index febc7179..8651dc5f 100644 --- a/templates/_layout/comments.twig +++ b/templates/_layout/comments.twig @@ -152,7 +152,7 @@ {% endmacro %} {% macro comments_section(comments, category, user, perms) %} -
+
{% if user|default(null) is null %}
diff --git a/templates/manage/news/categories.twig b/templates/manage/news/categories.twig index f8421c83..995c502b 100644 --- a/templates/manage/news/categories.twig +++ b/templates/manage/news/categories.twig @@ -9,10 +9,10 @@ {% for cat in news_categories %}

- {{ cat.category_id }} - {{ cat.category_name }}, - {{ cat.category_is_hidden }}, - {{ cat.category_created }} + {{ cat.id }} + {{ cat.name }}, + {{ cat.isHidden }}, + {{ cat.createdTime|date('r') }}

{% endfor %} diff --git a/templates/manage/news/category.twig b/templates/manage/news/category.twig index 372ce382..2d078c4c 100644 --- a/templates/manage/news/category.twig +++ b/templates/manage/news/category.twig @@ -2,29 +2,29 @@ {% from 'macros.twig' import container_title %} {% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox %} -{% set is_new = category|length < 1 %} +{% set is_new = category is not defined %} {% block manage_content %} -
- {{ container_title(is_new ? 'New Category' : 'Editing ' ~ category.category_name) }} + + {{ container_title(is_new ? 'New Category' : 'Editing ' ~ category_info.name) }} {{ input_csrf() }} - {{ input_hidden('category[id]', category.category_id|default(0)) }} + {{ input_hidden('category[id]', category_info.id|default(0)) }} - + - + - +
Name{{ input_text('category[name]', '', category.category_name|default(), 'text', '', true) }}{{ input_text('category[name]', '', category_info.name|default(), 'text', '', true) }}
Description
Is Hidden{{ input_checkbox('category[hidden]', '', category.category_is_hidden|default(false)) }}{{ input_checkbox('category[hidden]', '', category_info.isHidden|default(false)) }}
diff --git a/templates/manage/news/post.twig b/templates/manage/news/post.twig index c0885cef..2cb3d59b 100644 --- a/templates/manage/news/post.twig +++ b/templates/manage/news/post.twig @@ -2,33 +2,33 @@ {% from 'macros.twig' import container_title %} {% from '_layout/input.twig' import input_hidden, input_csrf, input_text, input_checkbox, input_select %} -{% set is_new = post|length < 1 %} +{% set is_new = post_info is not defined %} {% block manage_content %} - - {{ container_title(is_new ? 'New Post' : 'Editing ' ~ post.post_title) }} + + {{ container_title(is_new ? 'New Post' : 'Editing ' ~ post_info.title) }} {{ input_csrf() }} - {{ input_hidden('post[id]', post.post_id|default(0)) }} + {{ input_hidden('post[id]', post_info.id|default(0)) }} - + - + - + - +
Name{{ input_text('post[title]', '', post.post_title|default(), 'text', '', true) }}{{ input_text('post[title]', '', post_info.title|default(), 'text', '', true) }}
Category{{ input_select('post[category]', categories, post.category_id|default(0), 'category_name', 'category_id') }}{{ input_select('post[category]', categories, post_info.categoryId|default(0), 'name', 'id') }}
Is Featured{{ input_checkbox('post[featured]', '', post.post_is_featured|default(false)) }}{{ input_checkbox('post[featured]', '', post_info.isFeatured|default(false)) }}
diff --git a/templates/manage/news/posts.twig b/templates/manage/news/posts.twig index fa811eb9..7d5d990d 100644 --- a/templates/manage/news/posts.twig +++ b/templates/manage/news/posts.twig @@ -9,16 +9,16 @@ {% for post in news_posts %}

- {{ post.post_id }} - Cat: {{ post.category_id }} - {{ post.post_is_featured }}, - {{ post.user_id }}, - {{ post.post_title }}, - {{ post.post_scheduled }}, - {{ post.post_created }}, - {{ post.post_updated }}, - {{ post.post_deleted }}, - {{ post.comment_section_id }} + {{ post.id }} + Cat: {{ post.categoryId }} + {{ post.isFeatured }}, + {{ post.user.id }}, + {{ post.title }}, + {{ post.scheduledTime|date('r') }}, + {{ post.createdTime|date('r') }}, + {{ post.updatedTime|date('r') }}, + {{ post.deletedTime|date('r') }}, + {{ post.commentSectionId }}

{% endfor %} diff --git a/templates/news/category.twig b/templates/news/category.twig index f99f797f..afe804bf 100644 --- a/templates/news/category.twig +++ b/templates/news/category.twig @@ -2,10 +2,10 @@ {% from 'macros.twig' import pagination, container_title %} {% from 'news/macros.twig' import news_preview %} -{% set title = category.category_name ~ ' :: News' %} -{% set manage_link = url('manage-news-category', {'category': category.category_id}) %} +{% set title = category_info.name ~ ' :: News' %} +{% set manage_link = url('manage-news-category', {'category': category_info.id}) %} {% set canonical_url = url('news-category', { - 'category': category.category_id, + 'category': category_info.id, 'page': news_pagination.page > 2 ? news_pagination.page : 0, }) %} @@ -13,12 +13,12 @@ { 'type': 'rss', 'title': '', - 'url': url('news-category-feed-rss', {'category': category.category_id}), + 'url': url('news-category-feed-rss', {'category': category_info.id}), }, { 'type': 'atom', 'title': '', - 'url': url('news-category-feed-atom', {'category': category.category_id}), + 'url': url('news-category-feed-atom', {'category': category_info.id}), }, ] %} @@ -30,36 +30,24 @@ {% endfor %}
- {{ pagination(news_pagination, url('news-category'), null, {'c':category.category_id}) }} + {{ pagination(news_pagination, url('news-category', {'category':category_info.id})) }}
- {{ container_title('News » ' ~ category.category_name) }} + {{ container_title('News » ' ~ category_info.name) }}
- {{ category.category_description|length > 0 ? category.category_description : '' }} + {{ category_info.description }}
- {% if featured|length > 0 %} -
- {{ container_title('Featured Posts') }} - -
- {% for featured_post in featured %} - {{ featured_post.post_title }} - {% endfor %} -
-
- {% endif %} - @@ -39,12 +39,12 @@
{% for category in categories %} - +
- {{ category.category_name }} + {{ category.name }}
- {{ category.posts_count }} post{{ category.posts_count == 1 ? '' : 's' }} + {{ category.postCount }} post{{ category.postCount == 1 ? '' : 's' }}
{% endfor %} diff --git a/templates/news/macros.twig b/templates/news/macros.twig index 864e31d9..8623c46c 100644 --- a/templates/news/macros.twig +++ b/templates/news/macros.twig @@ -1,44 +1,44 @@ {% macro news_preview(post) %} {% from 'macros.twig' import container_title, avatar %} -
+
- {% if post.user_id is not null %} + {% if post.user.id is not null %} {% endif %} - - {{ post.category_name }} + + {{ post.category.name }}
Posted -
-

{{ post.post_title }}

+

{{ post.title }}

- {{ post.post_text|first_paragraph|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }} + {{ post.text|first_paragraph|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
@@ -48,38 +48,38 @@ {% macro news_post(post) %} {% from 'macros.twig' import avatar %} -
+
{% endmacro %} diff --git a/templates/news/post.twig b/templates/news/post.twig index ca84664a..b410484a 100644 --- a/templates/news/post.twig +++ b/templates/news/post.twig @@ -3,12 +3,12 @@ {% from '_layout/comments.twig' import comments_section %} {% from 'news/macros.twig' import news_post %} -{% set title = post.post_title ~ ' :: News' %} -{% set canonical_url = url('news-post', {'post': post.post_id}) %} -{% set manage_link = url('manage-news-post', {'post': post.post_id}) %} +{% set title = post_info.title ~ ' :: News' %} +{% set canonical_url = url('news-post', {'post': post_info.id}) %} +{% set manage_link = url('manage-news-post', {'post': post_info.id}) %} {% block content %} - {{ news_post(post) }} + {{ news_post(post_info) }} {% if comments is defined %}