Moved news pages into the router and made news object oriented.

This commit is contained in:
flash 2020-05-16 22:35:11 +00:00
parent 4871df92f9
commit b66eb8ba76
47 changed files with 980 additions and 780 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsCategory;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
@ -8,14 +10,14 @@ if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_N
return;
}
$categoriesPagination = new Pagination(news_categories_count(true), 15);
$categoriesPagination = new Pagination(NewsCategory::countAll(true), 15);
if(!$categoriesPagination->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,

View file

@ -1,6 +1,9 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsCategoryNotFoundException;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_CATEGORIES)) {
@ -8,29 +11,40 @@ if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_N
return;
}
$category = [];
$categoryId = (int)($_GET['c'] ?? null);
$categoryId = (int)filter_input(INPUT_GET, 'c', FILTER_SANITIZE_NUMBER_INT);
if($categoryId > 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');

View file

@ -1,6 +1,10 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsPost;
use Misuzu\News\NewsPostNotFoundException;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_POSTS)) {
@ -8,50 +12,63 @@ if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_N
return;
}
$post = [];
$postId = (int)($_GET['p'] ?? null);
$categories = news_categories_get(0, 0, false, false, true);
$postId = (int)filter_input(INPUT_GET, 'p', FILTER_SANITIZE_NUMBER_INT);
if($postId > 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,
]);

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsPost;
require_once '../../../misuzu.php';
if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_NEWS_MANAGE_POSTS)) {
@ -8,18 +10,14 @@ if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_N
return;
}
$postsPagination = new Pagination(news_posts_count(null, false, true, false), 15);
$postsPagination = new Pagination(NewsPost::countAll(false, true, true), 15);
if(!$postsPagination->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,

View file

@ -1,5 +1,7 @@
<?php
namespace Misuzu;
require_once __DIR__ . '/index.php';
return;
//namespace Misuzu;
require_once '../misuzu.php';
@ -20,12 +22,9 @@ if(!empty($feedMode) && in_array($feedMode, ['rss', 'atom'])) {
$location = empty($categoryId) ? url("news-feed-{$feedMode}") : url("news-category-feed-{$feedMode}", ['category' => $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);

View file

@ -1,34 +1,2 @@
<?php
namespace Misuzu;
require_once '../../misuzu.php';
$categoryId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
$category = news_category_get($categoryId, true);
if(empty($category)) {
echo render_error(404);
return;
}
$categoryPagination = new Pagination($category['posts_count'], 5);
if(!$categoryPagination->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';

View file

@ -1,76 +1,2 @@
<?php
namespace Misuzu;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\Parsers\Parser;
require_once '../../misuzu.php';
$feedMode = trim($_SERVER['PATH_INFO'] ?? '', '/');
switch($feedMode) {
case 'rss':
$feedSerializer = new RssFeedSerializer;
break;
case 'atom':
$feedSerializer = new AtomFeedSerializer;
break;
}
if(!isset($feedSerializer)) {
echo render_error(400);
return;
}
$categoryId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;
if(!empty($categoryId)) {
$category = news_category_get($categoryId);
if(empty($category)) {
echo render_error(404);
return;
}
}
$posts = news_posts_get(0, 10, $category['category_id'] ?? null, empty($category));
if(!$posts) {
echo render_error(404);
return;
}
$feed = (new Feed)
->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';

View file

@ -1,31 +1,2 @@
<?php
namespace Misuzu;
require_once '../../misuzu.php';
$categories = news_categories_get(0, 0, true);
$newsPagination = new Pagination(news_posts_count(null, true), 5, 'page');
if(!$newsPagination->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';

View file

@ -1,33 +1,2 @@
<?php
namespace Misuzu;
require_once '../../misuzu.php';
$postId = !empty($_GET['p']) && is_string($_GET['p']) ? (int)$_GET['p'] : 0;
$post = news_post_get($postId);
if(!$post) {
echo render_error(404);
return;
}
if($post['comment_section_id'] === null) {
$commentsInfo = comments_category_create("news-{$post['post_id']}");
if($commentsInfo) {
$post['comment_section_id'] = $commentsInfo['category_id'];
news_post_comments_set(
$post['post_id'],
$post['comment_section_id'] = $commentsInfo['category_id']
);
}
} else {
$commentsInfo = comments_category_info($post['comment_section_id']);
}
Template::render('news.post', [
'post' => $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';

View file

@ -1,6 +1,8 @@
<?php
namespace Misuzu;
use Misuzu\News\NewsPost;
require_once '../misuzu.php';
$searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
@ -8,7 +10,7 @@ $searchQuery = !empty($_GET['q']) && is_string($_GET['q']) ? $_GET['q'] : '';
if(!empty($searchQuery)) {
$forumTopics = forum_topic_listing_search($searchQuery, user_session_current('user_id', 0));
$forumPosts = forum_post_search($searchQuery);
$newsPosts = news_posts_search($searchQuery);
$newsPosts = NewsPost::bySearchQuery($searchQuery);
$findUsers = DB::prepare(sprintf(
'

View file

@ -129,7 +129,6 @@ class Colour {
public function getCSS(): string {
if($this->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();
}
}

View file

@ -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,

View file

@ -1,51 +1,4 @@
<?php
/**********************
* GLOBAL PERMISSIONS *
**********************/
define('MSZ_PERM_FORUM_MANAGE_FORUMS', 1);
define('MSZ_PERM_FORUM_VIEW_LEADERBOARD', 2);
/*************************
* PER-FORUM PERMISSIONS *
*************************/
define('MSZ_FORUM_PERM_LIST_FORUM', 1); // can see stats, but will get error when trying to view
define('MSZ_FORUM_PERM_VIEW_FORUM', 1 << 1);
define('MSZ_FORUM_PERM_CREATE_TOPIC', 1 << 10);
//define('MSZ_FORUM_PERM_DELETE_TOPIC', 1 << 11); // use MSZ_FORUM_PERM_DELETE_ANY_POST instead
define('MSZ_FORUM_PERM_MOVE_TOPIC', 1 << 12);
define('MSZ_FORUM_PERM_LOCK_TOPIC', 1 << 13);
define('MSZ_FORUM_PERM_STICKY_TOPIC', 1 << 14);
define('MSZ_FORUM_PERM_ANNOUNCE_TOPIC', 1 << 15);
define('MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC', 1 << 16);
define('MSZ_FORUM_PERM_BUMP_TOPIC', 1 << 17);
define('MSZ_FORUM_PERM_PRIORITY_VOTE', 1 << 18);
define('MSZ_FORUM_PERM_CREATE_POST', 1 << 20);
define('MSZ_FORUM_PERM_EDIT_POST', 1 << 21);
define('MSZ_FORUM_PERM_EDIT_ANY_POST', 1 << 22);
define('MSZ_FORUM_PERM_DELETE_POST', 1 << 23);
define('MSZ_FORUM_PERM_DELETE_ANY_POST', 1 << 24);
// shorthands, never use these to SET!!!!!!!
define('MSZ_FORUM_PERM_SET_READ', MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM);
define(
'MSZ_FORUM_PERM_SET_WRITE',
MSZ_FORUM_PERM_CREATE_TOPIC
| MSZ_FORUM_PERM_MOVE_TOPIC
| MSZ_FORUM_PERM_LOCK_TOPIC
| MSZ_FORUM_PERM_STICKY_TOPIC
| MSZ_FORUM_PERM_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_CREATE_POST
| MSZ_FORUM_PERM_EDIT_POST
| MSZ_FORUM_PERM_EDIT_ANY_POST
| MSZ_FORUM_PERM_DELETE_POST
| MSZ_FORUM_PERM_DELETE_ANY_POST
| MSZ_FORUM_PERM_BUMP_TOPIC
| MSZ_FORUM_PERM_PRIORITY_VOTE
);
define('MSZ_FORUM_TYPE_DISCUSSION', 0);
define('MSZ_FORUM_TYPE_CATEGORY', 1);
define('MSZ_FORUM_TYPE_LINK', 2);

View file

@ -1,6 +1,44 @@
<?php
define('MSZ_FORUM_PERMS_GENERAL', 'forum');
define('MSZ_FORUM_PERM_LIST_FORUM', 1); // can see stats, but will get error when trying to view
define('MSZ_FORUM_PERM_VIEW_FORUM', 1 << 1);
define('MSZ_FORUM_PERM_CREATE_TOPIC', 1 << 10);
//define('MSZ_FORUM_PERM_DELETE_TOPIC', 1 << 11); // use MSZ_FORUM_PERM_DELETE_ANY_POST instead
define('MSZ_FORUM_PERM_MOVE_TOPIC', 1 << 12);
define('MSZ_FORUM_PERM_LOCK_TOPIC', 1 << 13);
define('MSZ_FORUM_PERM_STICKY_TOPIC', 1 << 14);
define('MSZ_FORUM_PERM_ANNOUNCE_TOPIC', 1 << 15);
define('MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC', 1 << 16);
define('MSZ_FORUM_PERM_BUMP_TOPIC', 1 << 17);
define('MSZ_FORUM_PERM_PRIORITY_VOTE', 1 << 18);
define('MSZ_FORUM_PERM_CREATE_POST', 1 << 20);
define('MSZ_FORUM_PERM_EDIT_POST', 1 << 21);
define('MSZ_FORUM_PERM_EDIT_ANY_POST', 1 << 22);
define('MSZ_FORUM_PERM_DELETE_POST', 1 << 23);
define('MSZ_FORUM_PERM_DELETE_ANY_POST', 1 << 24);
// shorthands, never use these to SET!!!!!!!
define('MSZ_FORUM_PERM_SET_READ', MSZ_FORUM_PERM_LIST_FORUM | MSZ_FORUM_PERM_VIEW_FORUM);
define(
'MSZ_FORUM_PERM_SET_WRITE',
MSZ_FORUM_PERM_CREATE_TOPIC
| MSZ_FORUM_PERM_MOVE_TOPIC
| MSZ_FORUM_PERM_LOCK_TOPIC
| MSZ_FORUM_PERM_STICKY_TOPIC
| MSZ_FORUM_PERM_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_GLOBAL_ANNOUNCE_TOPIC
| MSZ_FORUM_PERM_CREATE_POST
| MSZ_FORUM_PERM_EDIT_POST
| MSZ_FORUM_PERM_EDIT_ANY_POST
| MSZ_FORUM_PERM_DELETE_POST
| MSZ_FORUM_PERM_DELETE_ANY_POST
| MSZ_FORUM_PERM_BUMP_TOPIC
| MSZ_FORUM_PERM_PRIORITY_VOTE
);
define('MSZ_FORUM_PERM_MODES', [
MSZ_FORUM_PERMS_GENERAL,
]);

View file

@ -1,11 +0,0 @@
<?php
namespace Misuzu;
final class General {
public const PERM_CAN_MANAGE = 0x00000001;
public const PERM_VIEW_LOGS = 0x00000002;
public const PERM_MANAGE_EMOTES = 0x00000004;
public const PERM_MANAGE_CONFIG = 0x00000008;
public const PERM_IS_TESTER = 0x00000010;
public const PERM_MANAGE_BLACKLIST = 0x00000020;
}

View file

@ -5,6 +5,8 @@ use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\News\NewsPost;
final class HomeHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request): void {
@ -17,7 +19,7 @@ final class HomeHandler extends Handler {
];
}
$news = news_posts_get(0, 5, null, true);
$featuredNews = NewsPost::all(new Pagination(5), true);
$stats = DB::query('
SELECT
@ -95,7 +97,7 @@ final class HomeHandler extends Handler {
'online_users' => $onlineUsers,
'birthdays' => $birthdays,
'featured_changelog' => $changelog,
'featured_news' => $news,
'featured_news' => $featuredNews,
'linked_data' => $linkedData ?? null,
]);
}

View file

@ -0,0 +1,202 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
use Misuzu\Feeds\RssFeedSerializer;
use Misuzu\News\NewsCategory;
use Misuzu\News\NewsPost;
use Misuzu\News\NewsCategoryNotFoundException;
use Misuzu\News\NewsPostNotException;
use Misuzu\Parsers\Parser;
final class NewsHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) {
$categories = NewsCategory::all();
$newsPagination = new Pagination(NewsPost::countAll(true), 5);
if(!$newsPagination->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);
}
}

View file

@ -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() {

143
src/News/NewsCategory.php Normal file
View file

@ -0,0 +1,143 @@
<?php
namespace Misuzu\News;
use ArrayAccess;
use Misuzu\DB;
use Misuzu\Pagination;
class NewsCategoryException extends NewsException {};
class NewsCategoryNotFoundException extends NewsCategoryException {};
class NewsCategory implements ArrayAccess {
// Database fields
private $category_id = -1;
private $category_name = '';
private $category_description = '';
private $category_is_hidden = false;
private $category_created = null;
private $postCount = -1;
private const TABLE = 'news_categories';
private const SELECT = '%1$s.`category_id`, %1$s.`category_name`, %1$s.`category_description`, %1$s.`category_is_hidden`'
. ', UNIX_TIMESTAMP(%1$s.`category_created`) AS `category_created`';
public function __construct() {}
public function getId(): int {
return $this->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) {}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Misuzu\News;
use Exception;
class NewsException extends Exception {}

287
src/News/NewsPost.php Normal file
View file

@ -0,0 +1,287 @@
<?php
namespace Misuzu\News;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Users\User;
class NewsPostException extends NewsException {};
class NewsPostNotFoundException extends NewsPostException {};
class NewsPost {
// Database fields
private $post_id = -1;
private $category_id = -1;
private $user_id = null;
private $comment_section_id = null;
private $post_is_featured = false;
private $post_title = '';
private $post_text = '';
private $post_scheduled = null;
private $post_created = null;
private $post_updated = null;
private $post_deleted = null;
private $category = null;
private $user = null;
private $comments = null;
private $commentCount = -1;
private const TABLE = 'news_posts';
private const SELECT = '%1$s.`post_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_section_id`'
. ', %1$s.`post_is_featured`, %1$s.`post_title`, %1$s.`post_text`'
. ', UNIX_TIMESTAMP(%1$s.`post_scheduled`) AS `post_scheduled`'
. ', UNIX_TIMESTAMP(%1$s.`post_created`) AS `post_created`'
. ', UNIX_TIMESTAMP(%1$s.`post_updated`) AS `post_updated`'
. ', UNIX_TIMESTAMP(%1$s.`post_deleted`) AS `post_deleted`';
public function __construct() {}
public function getId(): int {
return $this->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);
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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')

View file

@ -1,8 +1,4 @@
<?php
define('MSZ_PERM_CHANGELOG_MANAGE_CHANGES', 1);
define('MSZ_PERM_CHANGELOG_MANAGE_TAGS', 1 << 1);
//define('MSZ_PERM_CHANGELOG_MANAGE_ACTIONS', 1 << 2); Deprecated, actions are hardcoded now
define('MSZ_CHANGELOG_ACTION_ADD', 1);
define('MSZ_CHANGELOG_ACTION_REMOVE', 2);
define('MSZ_CHANGELOG_ACTION_UPDATE', 3);

View file

@ -1,15 +1,6 @@
<?php
require_once 'Users/validation.php';
define('MSZ_PERM_COMMENTS_CREATE', 1);
//define('MSZ_PERM_COMMENTS_EDIT_OWN', 1 << 1);
//define('MSZ_PERM_COMMENTS_EDIT_ANY', 1 << 2);
define('MSZ_PERM_COMMENTS_DELETE_OWN', 1 << 3);
define('MSZ_PERM_COMMENTS_DELETE_ANY', 1 << 4);
define('MSZ_PERM_COMMENTS_PIN', 1 << 5);
define('MSZ_PERM_COMMENTS_LOCK', 1 << 6);
define('MSZ_PERM_COMMENTS_VOTE', 1 << 7);
define('MSZ_COMMENTS_VOTE_INDIFFERENT', 0);
define('MSZ_COMMENTS_VOTE_LIKE', 1);
define('MSZ_COMMENTS_VOTE_DISLIKE', -1);

View file

@ -1,6 +1,6 @@
<?php
function manage_get_menu(int $userId): array {
if(!perms_check_user(MSZ_PERMS_GENERAL, $userId, \Misuzu\General::PERM_CAN_MANAGE)) {
if(!perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_CAN_MANAGE)) {
return [];
}
@ -10,19 +10,19 @@ function manage_get_menu(int $userId): array {
],
];
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, \Misuzu\General::PERM_VIEW_LOGS)) {
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_VIEW_LOGS)) {
$menu['General']['Logs'] = url('manage-general-logs');
}
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, \Misuzu\General::PERM_MANAGE_EMOTES)) {
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_EMOTES)) {
$menu['General']['Emoticons'] = url('manage-general-emoticons');
}
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, \Misuzu\General::PERM_MANAGE_CONFIG)) {
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_CONFIG)) {
$menu['General']['Settings'] = url('manage-general-settings');
}
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, \Misuzu\General::PERM_MANAGE_BLACKLIST)) {
if(perms_check_user(MSZ_PERMS_GENERAL, $userId, MSZ_PERM_GENERAL_MANAGE_BLACKLIST)) {
$menu['General']['IP Blacklist'] = url('manage-general-blacklist');
}
@ -155,32 +155,32 @@ function manage_perms_list(array $rawPerms): array {
[
'section' => '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,
],
],
],

View file

@ -1,327 +0,0 @@
<?php
define('MSZ_PERM_NEWS_MANAGE_POSTS', 1);
define('MSZ_PERM_NEWS_MANAGE_CATEGORIES', 1 << 1);
function news_post_create(
string $title,
string $text,
int $category,
int $user,
bool $featured = false,
?int $scheduled = null,
?int $postId = null
): int {
if($postId < 1) {
$post = \Misuzu\DB::prepare('
INSERT INTO `msz_news_posts`
(`category_id`, `user_id`, `post_is_featured`, `post_title`, `post_text`, `post_scheduled`)
VALUES
(:category, :user, :featured, :title, :text, COALESCE(:scheduled, CURRENT_TIMESTAMP))
');
} else {
$post = \Misuzu\DB::prepare('
UPDATE `msz_news_posts`
SET `category_id` = :category,
`user_id` = :user,
`post_is_featured` = :featured,
`post_title` = :title,
`post_text` = :text,
`post_scheduled` = COALESCE(:scheduled, `post_scheduled`)
WHERE `post_id` = :id
');
$post->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();
}

View file

@ -1,10 +1,48 @@
<?php
define('MSZ_PERMS_GENERAL', 'general');
define('MSZ_PERM_GENERAL_CAN_MANAGE', 0x00000001);
define('MSZ_PERM_GENERAL_VIEW_LOGS', 0x00000002);
define('MSZ_PERM_GENERAL_MANAGE_EMOTES', 0x00000004);
define('MSZ_PERM_GENERAL_MANAGE_CONFIG', 0x00000008);
define('MSZ_PERM_GENERAL_IS_TESTER', 0x00000010);
define('MSZ_PERM_GENERAL_MANAGE_BLACKLIST', 0x00000020);
define('MSZ_PERMS_USER', 'user');
define('MSZ_PERM_USER_EDIT_PROFILE', 0x00000001);
define('MSZ_PERM_USER_CHANGE_AVATAR', 0x00000002);
define('MSZ_PERM_USER_CHANGE_BACKGROUND', 0x00000004);
define('MSZ_PERM_USER_EDIT_ABOUT', 0x00000008);
define('MSZ_PERM_USER_EDIT_BIRTHDATE', 0x00000010);
define('MSZ_PERM_USER_EDIT_SIGNATURE', 0x00000020);
define('MSZ_PERM_USER_MANAGE_USERS', 0x00100000);
define('MSZ_PERM_USER_MANAGE_ROLES', 0x00200000);
define('MSZ_PERM_USER_MANAGE_PERMS', 0x00400000);
define('MSZ_PERM_USER_MANAGE_REPORTS', 0x00800000);
define('MSZ_PERM_USER_MANAGE_WARNINGS', 0x01000000);
//define('MSZ_PERM_USER_MANAGE_BLACKLISTS', 0x02000000); // Replaced with MSZ_PERM_MANAGE_BLACKLIST
define('MSZ_PERMS_CHANGELOG', 'changelog');
define('MSZ_PERM_CHANGELOG_MANAGE_CHANGES', 0x00000001);
define('MSZ_PERM_CHANGELOG_MANAGE_TAGS', 0x00000002);
//define('MSZ_PERM_CHANGELOG_MANAGE_ACTIONS', 0x00000004); Deprecated, actions are hardcoded now
define('MSZ_PERMS_NEWS', 'news');
define('MSZ_PERM_NEWS_MANAGE_POSTS', 0x00000001);
define('MSZ_PERM_NEWS_MANAGE_CATEGORIES', 0x00000002);
define('MSZ_PERMS_FORUM', 'forum');
define('MSZ_PERM_FORUM_MANAGE_FORUMS', 0x00000001);
define('MSZ_PERM_FORUM_VIEW_LEADERBOARD', 0x00000002);
define('MSZ_PERMS_COMMENTS', 'comments');
define('MSZ_PERM_COMMENTS_CREATE', 0x00000001);
//define('MSZ_PERM_COMMENTS_EDIT_OWN', 0x00000002);
//define('MSZ_PERM_COMMENTS_EDIT_ANY', 0x00000004);
define('MSZ_PERM_COMMENTS_DELETE_OWN', 0x00000008);
define('MSZ_PERM_COMMENTS_DELETE_ANY', 0x00000010);
define('MSZ_PERM_COMMENTS_PIN', 0x00000020);
define('MSZ_PERM_COMMENTS_LOCK', 0x00000040);
define('MSZ_PERM_COMMENTS_VOTE', 0x00000080);
define('MSZ_PERM_MODES', [
MSZ_PERMS_GENERAL, MSZ_PERMS_USER, MSZ_PERMS_CHANGELOG,

View file

@ -29,14 +29,14 @@ define('MSZ_URLS', [
'changelog-date' => ['/changelog.php', ['d' => '<date>']],
'changelog-tag' => ['/changelog.php', ['t' => '<tag>']],
'news-index' => ['/news', ['page' => '<page>']],
'news-post' => ['/news/post.php', ['p' => '<post>']],
'news-post-comments' => ['/news/post.php', ['p' => '<post>'], 'comments'],
'news-category' => ['/news/category.php', ['c' => '<category>', 'p' => '<page>']],
'news-feed-rss' => ['/news/feed.php/rss'],
'news-category-feed-rss' => ['/news/feed.php/rss', ['c' => '<category>']],
'news-feed-atom' => ['/news/feed.php/atom'],
'news-category-feed-atom' => ['/news/feed.php/atom', ['c' => '<category>']],
'news-index' => ['/news', ['p' => '<page>']],
'news-category' => ['/news/<category>', ['p' => '<page>']],
'news-post' => ['/news/post/<post>'],
'news-post-comments' => ['/news/post/<post>', [], 'comments'],
'news-feed-rss' => ['/news.rss'],
'news-category-feed-rss' => ['/news/<category>.rss'],
'news-feed-atom' => ['/news.atom'],
'news-category-feed-atom' => ['/news/<category>.atom'],
'forum-index' => ['/forum'],
'forum-leaderboard' => ['/forum/leaderboard.php', ['id' => '<id>', 'mode' => '<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;

View file

@ -152,7 +152,7 @@
{% endmacro %}
{% macro comments_section(comments, category, user, perms) %}
<div class="comments">
<div class="comments" id="comments">
<div class="comments__input">
{% if user|default(null) is null %}
<div class="comments__notice">

View file

@ -9,10 +9,10 @@
{% for cat in news_categories %}
<p>
<a href="{{ url('manage-news-category', {'category': cat.category_id}) }}" class="input__button">{{ cat.category_id }}</a>
{{ cat.category_name }},
{{ cat.category_is_hidden }},
{{ cat.category_created }}
<a href="{{ url('manage-news-category', {'category': cat.id}) }}" class="input__button">{{ cat.id }}</a>
{{ cat.name }},
{{ cat.isHidden }},
{{ cat.createdTime|date('r') }}
</p>
{% endfor %}

View file

@ -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 %}
<form method="post" action="{{ url('manage-news-category', {'category': category.category_id|default(0)}) }}" class="container">
{{ container_title(is_new ? 'New Category' : 'Editing ' ~ category.category_name) }}
<form method="post" action="{{ url('manage-news-category', {'category': category_info.id|default(0)}) }}" class="container">
{{ 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)) }}
<table style="color:inherit">
<tr>
<td>Name</td>
<td>{{ input_text('category[name]', '', category.category_name|default(), 'text', '', true) }}</td>
<td>{{ input_text('category[name]', '', category_info.name|default(), 'text', '', true) }}</td>
</tr>
<tr>
<td>Description</td>
<td><textarea name="category[description]" required class="input__textarea">{{ category.category_description|default() }}</textarea></td>
<td><textarea name="category[description]" required class="input__textarea">{{ category_info.description|default() }}</textarea></td>
</tr>
<tr>
<td>Is Hidden</td>
<td>{{ input_checkbox('category[hidden]', '', category.category_is_hidden|default(false)) }}</td>
<td>{{ input_checkbox('category[hidden]', '', category_info.isHidden|default(false)) }}</td>
</tr>
</table>

View file

@ -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 %}
<form method="post" action="{{ url('manage-news-post', {'post': post.post_id|default(0)}) }}" class="container">
{{ container_title(is_new ? 'New Post' : 'Editing ' ~ post.post_title) }}
<form method="post" action="{{ url('manage-news-post', {'post': post_info.id|default(0)}) }}" class="container">
{{ 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)) }}
<table style="color:inherit">
<tr>
<td>Name</td>
<td>{{ input_text('post[title]', '', post.post_title|default(), 'text', '', true) }}</td>
<td>{{ input_text('post[title]', '', post_info.title|default(), 'text', '', true) }}</td>
</tr>
<tr>
<td>Category</td>
<td>{{ input_select('post[category]', categories, post.category_id|default(0), 'category_name', 'category_id') }}</td>
<td>{{ input_select('post[category]', categories, post_info.categoryId|default(0), 'name', 'id') }}</td>
</tr>
<tr>
<td>Is Featured</td>
<td>{{ input_checkbox('post[featured]', '', post.post_is_featured|default(false)) }}</td>
<td>{{ input_checkbox('post[featured]', '', post_info.isFeatured|default(false)) }}</td>
</tr>
<tr>
<td colspan="2"><textarea name="post[text]" required class="input__textarea">{{ post.post_text|default() }}</textarea></td>
<td colspan="2"><textarea name="post[text]" required class="input__textarea">{{ post_info.text|default() }}</textarea></td>
</tr>
</table>

View file

@ -9,16 +9,16 @@
{% for post in news_posts %}
<p>
<a href="{{ url('manage-news-post', {'post': post.post_id}) }}" class="input__button">{{ post.post_id }}</a>
<a href="{{ url('manage-news-category', {'category': post.category_id}) }}" class="input__button">Cat: {{ post.category_id }}</a>
{{ 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 }}
<a href="{{ url('manage-news-post', {'post': post.id}) }}" class="input__button">{{ post.id }}</a>
<a href="{{ url('manage-news-category', {'category': post.categoryId}) }}" class="input__button">Cat: {{ post.categoryId }}</a>
{{ 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 }}
</p>
{% endfor %}

View file

@ -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 %}
<div class="container" style="padding: 4px; display: {{ news_pagination.pages > 1 ? 'block' : 'none' }}">
{{ pagination(news_pagination, url('news-category'), null, {'c':category.category_id}) }}
{{ pagination(news_pagination, url('news-category', {'category':category_info.id})) }}
</div>
</div>
<div class="news__sidebar">
<div class="container news__list">
{{ container_title('News » ' ~ category.category_name) }}
{{ container_title('News » ' ~ category_info.name) }}
<div class="container__content">
{{ category.category_description|length > 0 ? category.category_description : '' }}
{{ category_info.description }}
</div>
</div>
{% if featured|length > 0 %}
<div class="container news__list">
{{ container_title('Featured Posts') }}
<div class="container__content">
{% for featured_post in featured %}
<a class="news__list__item" href="{{ url('news-post', {'post': featured_post.post_id}) }}">{{ featured_post.post_title }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<div class="container">
{{ container_title('Feeds') }}
<div class="news__feeds">
<a href="{{ url('news-category-feed-atom', {'category': category.category_id}) }}" class="news__feed">
<a href="{{ url('news-category-feed-atom', {'category': category_info.id}) }}" class="news__feed">
<div class="news__feed__icon">
<i class="fas fa-rss"></i>
</div>
@ -67,7 +55,7 @@
Atom
</div>
</a>
<a href="{{ url('news-category-feed-rss', {'category': category.category_id}) }}" class="news__feed">
<a href="{{ url('news-category-feed-rss', {'category': category_info.id}) }}" class="news__feed">
<div class="news__feed__icon">
<i class="fas fa-rss"></i>
</div>

View file

@ -29,7 +29,7 @@
{% endfor %}
<div class="container" style="padding: 4px; display: {{ news_pagination.pages > 1 ? 'block' : 'none' }}">
{{ pagination(news_pagination, url('news-index'), null, null, 'page') }}
{{ pagination(news_pagination, url('news-index')) }}
</div>
</div>
@ -39,12 +39,12 @@
<div class="container__content">
{% for category in categories %}
<a class="news__list__item news__list__item--kvp" href="{{ url('news-category', {'category': category.category_id}) }}">
<a class="news__list__item news__list__item--kvp" href="{{ url('news-category', {'category': category.id}) }}">
<div class="news__list__name">
{{ category.category_name }}
{{ category.name }}
</div>
<div class="news__list__value">
{{ category.posts_count }} post{{ category.posts_count == 1 ? '' : 's' }}
{{ category.postCount }} post{{ category.postCount == 1 ? '' : 's' }}
</div>
</a>
{% endfor %}

View file

@ -1,44 +1,44 @@
{% macro news_preview(post) %}
{% from 'macros.twig' import container_title, avatar %}
<div class="container news__preview" style="{% if post.user_colour is not null %}{{ post.user_colour|html_colour }}{% endif %}">
<div class="container news__preview" style="{% if post.user is not null %}--user-colour: {{ post.user.colour }}{% endif %}">
<div class="news__preview__info">
<div class="news__preview__info__background"></div>
<div class="news__preview__info__content">
{% if post.user_id is not null %}
{% if post.user.id is not null %}
<div class="news__preview__user">
<a class="news__preview__avatar" href="{{ url('user-profile', {'user': post.user_id}) }}">
{{ avatar(post.user_id, 60, post.username) }}
<a class="news__preview__avatar" href="{{ url('user-profile', {'user': post.user.id}) }}">
{{ avatar(post.user.id, 60, post.user.username) }}
</a>
<div class="news__preview__user__details">
<a class="news__preview__username" href="{{ url('user-profile', {'user': post.user_id}) }}">{{ post.username }}</a>
<a class="news__preview__username" href="{{ url('user-profile', {'user': post.user.id}) }}">{{ post.user.username }}</a>
</div>
</div>
{% endif %}
<a class="news__preview__category" href="{{ url('news-category', {'category': post.category_id}) }}">
{{ post.category_name }}
<a class="news__preview__category" href="{{ url('news-category', {'category': post.category.id}) }}">
{{ post.category.name }}
</a>
<div class="news__preview__date">
Posted
<time datetime="{{ post.post_created|date('c') }}" title="{{ post.post_created|date('r') }}">
{{ post.post_created|time_diff }}
<time datetime="{{ post.createdTime|date('c') }}" title="{{ post.createdTime|date('r') }}">
{{ post.createdTime|time_diff }}
</time>
</div>
</div>
</div>
<div class="news__preview__content markdown">
<h1>{{ post.post_title }}</h1>
<h1>{{ post.title }}</h1>
<div class="news__preview__text">
{{ 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 }}
</div>
<div class="news__preview__links">
<a href="{{ url('news-post', {'post': post.post_id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.post_id}) }}" class="news__preview__link">
{{ post.post_comments < 1 ? 'No' : post.post_comments|number_format }} comment{{ post.post_comments != 1 ? 's' : '' }}
<a href="{{ url('news-post', {'post': post.id}) }}" class="news__preview__link">Continue reading</a>
<a href="{{ url('news-post-comments', {'post': post.id}) }}" class="news__preview__link">
{{ post.commentCount < 1 ? 'No' : post.commentCount|number_format }} comment{{ post.commentCount != 1 ? 's' : '' }}
</a>
</div>
</div>
@ -48,38 +48,38 @@
{% macro news_post(post) %}
{% from 'macros.twig' import avatar %}
<div class="container news__post" style="{% if post.user_colour is not null %}{{ post.user_colour|html_colour('--accent-colour') }}{% endif %}">
<div class="container news__post" style="{% if post.user is not null %}--accent-colour: {{ post.user.colour }}{% endif %}">
<div class="news__post__info">
<div class="news__post__info__background"></div>
<div class="news__post__info__content">
{% if post.user_id is not null %}
{% if post.user is not null %}
<div class="news__post__user">
<a class="news__post__avatar" href="{{ url('user-profile', {'user': post.user_id}) }}">
{{ avatar(post.user_id, 60, post.username) }}
<a class="news__post__avatar" href="{{ url('user-profile', {'user': post.user.id}) }}">
{{ avatar(post.user.id, 60, post.user.username) }}
</a>
<div class="news__post__user__details">
<a class="news__post__username" href="{{ url('user-profile', {'user': post.user_id}) }}">{{ post.username }}</a>
<a class="news__post__username" href="{{ url('user-profile', {'user': post.user.id}) }}">{{ post.user.username }}</a>
</div>
</div>
{% endif %}
<a class="news__post__category" href="{{ url('news-category', {'category': post.category_id}) }}">
{{ post.category_name }}
<a class="news__post__category" href="{{ url('news-category', {'category': post.category.id}) }}">
{{ post.category.name }}
</a>
<div class="news__post__date">
Posted
<time datetime="{{ post.post_created|date('c') }}" title="{{ post.post_created|date('r') }}">
{{ post.post_created|time_diff }}
<time datetime="{{ post.createdTime|date('c') }}" title="{{ post.createdTime|date('r') }}">
{{ post.createdTime|time_diff }}
</time>
</div>
{% if post.post_updated|date('U') > post.post_created|date('U') %}
{% if post.isEdited %}
<div class="news__post__date">
Updated
<time datetime="{{ post.post_updated|date('c') }}" title="{{ post.post_updated|date('r') }}">
{{ post.post_updated|time_diff }}
<time datetime="{{ post.updatedTime|date('c') }}" title="{{ post.updatedTime|date('r') }}">
{{ post.updatedTime|time_diff }}
</time>
</div>
{% endif %}
@ -87,8 +87,8 @@
</div>
<div class="news__post__text markdown">
<h1>{{ post.post_title }}</h1>
{{ post.post_text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
<h1>{{ post.title }}</h1>
{{ post.text|parse_text(constant('\\Misuzu\\Parsers\\Parser::MARKDOWN'))|raw }}
</div>
</div>
{% endmacro %}

View file

@ -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 %}
<div class="container">